Yig'ilgan Parquet Kesh: Ko'p Vaqt Oralig'idagi Backtestlarni Yuzlab Marta Tezlashtirish
Ko'p vaqt oralig'idagi (multi-timeframe) strategiya bir vaqtning o'zida bir nechta vaqt oralig'idan foydalanadi: kunlik trend yo'nalishini belgilaydi, soatlik kirish nuqtalarini aniqlaydi, 5 daqiqalik esa bajarish vaqtini aniq belgilaydi. Har bir vaqt oralig'i o'z indikatorlarini talab qiladi: harakatlanuvchi o'rtachalar, ostsillyatorlar, darajalar.
Yagona backtest uchun hamma narsa oddiy — daqiqalik ma'lumotlardan vaqt oraliqlarini qayta hisoblash, indikatorlarni hisoblash, strategiyani ishga tushirish. Ammo ommaviy optimallashtirish paytida — parametrlarning minglab kombinatsiyalarini sinash kerak bo'lganda — har bir iteratsiyada vaqt oraliqlari va indikatorlarni qayta hisoblash tor joyga aylanadi. Ikki yillik daqiqalik ma'lumotlar bo'yicha bitta o'tish million martadan ortiq barni qayta ishlashni anglatadi, buni ming marta takrorlash esa isrofgarchilikdir.
Yechim: hamma narsani bir marta oldindan hisoblab, parquet faylida keshlash.
Muammo: Optimallashtirish Paytidagi Ortiqcha Hisob-kitoblar
Odatiy ko'p vaqt oralig'idagi backtest quvur liniyasi (pipeline) quyidagicha ko'rinadi:
for params in parameter_grid:
df_1m = load_candles("ETHUSDT", "1m", start, end)
df_5m = resample_ohlcv(df_1m, "5m")
df_1h = resample_ohlcv(df_1m, "1h")
df_4h = resample_ohlcv(df_1m, "4h")
df_1d = resample_ohlcv(df_1m, "D")
ma_1h = compute_ma(df_1h["close"], length=params["ma_1h_len"])
ma_4h = compute_ma(df_4h["close"], length=params["ma_4h_len"])
ma_1d = compute_ma(df_1d["close"], length=params["ma_1d_len"])
result = run_strategy(df_1m, ma_1h, ma_4h, ma_1d, params)
Har bir iteratsiyada, ma'lumotlar bir xil bo'lsa-da, 1-3-qadamlar qayta hisoblanadi. Faqat strategiyaning chegara parametrlari o'zgaradi (4-qadam). Bu devor rangini o'zgartirmoqchi bo'lganingizda har safar butun uyni qayta qurishga o'xshaydi.
G'oya: Bir Marta Hisoblash, Saqlash, Ko'p Marta Qayta Foydalanish
Asosiy kuzatuv: vaqt oraliqlari va indikatorlar faqat daqiqalik ma'lumotlarga va indikator parametrlariga bog'liq, strategiya parametrlariga emas. Agar kerakli indikatorlar to'plamini belgilasak, ularni bir marta hisoblab saqlashimiz mumkin.
Sxema:
Step 1 (once):
Minute candles -> Timeframe resampling -> Indicator computation -> Parquet file
Step 2 (many times):
Parquet file -> Strategy with different parameters -> Result
Daqiqalik Shamlardan Vaqt Oraliqlarini Emulyatsiya Qilish

Bizda daqiqalik shamlarning to'liq arxivi mavjud. Undan biz istalgan yuqori vaqt oralig'ini aniq tiklashimiz mumkin. Ammo bir nozik jihat bor: standart resample bilan biz har davr uchun bitta qator olamiz (har soat uchun bitta qator, har 4 soat uchun bitta va h.k.). Bu daqiqama-daqiqa backtesting uchun ishlamaydi — bizga indikator qiymatini har daqiqada bilish kerak.
Shu sababli biz botning ma'lumotlarni real vaqtda qanday ko'rishini modellashtirib, har bir daqiqalik sham uchun yuqori vaqt oralig'i qiymatlarini emulyatsiya qilamiz:
- Bot navbatdagi daqiqalik shamni oladi
- Yuqori vaqt oralig'ining joriy (yopilmagan) barini yangilaydi — High, Low, Close, Volume qayta hisoblanadi
- Barcha yopilgan barlar va joriy qisman bar bo'yicha indikatorni qayta hisoblaydi
- Davr tugaganda — bar yakunlanadi va yangisi boshlanadi
Bu yondashuv backtest bot real vaqtda ko'radigan xuddi shu ma'lumotlarni ko'rishini kafolatlaydi. Kelajakka qarash yo'q — har bir daqiqalik sham o'sha paytda mavjud bo'lgan ma'lumotlar bilan qat'iy tarzda qayta ishlanadi.
class RunningCandleBuffer:
"""
Emulates real-time updates of a higher timeframe bar
using 1-minute candles.
"""
def __init__(self, period_seconds: int):
self.period = period_seconds # 86400 for Daily, 3600 for 1h
self.closed_bars = []
self.current_bar = None
def update(self, timestamp, open_, high, low, close, volume):
bar_start = self._align_to_period(timestamp)
if self.current_bar is None or bar_start != self.current_bar['start']:
if self.current_bar is not None:
self.closed_bars.append(self.current_bar)
self.current_bar = {
'start': bar_start,
'open': open_, 'high': high,
'low': low, 'close': close,
'volume': volume,
}
else:
self.current_bar['high'] = max(self.current_bar['high'], high)
self.current_bar['low'] = min(self.current_bar['low'], low)
self.current_bar['close'] = close
self.current_bar['volume'] += volume
return self.closed_bars + [self.current_bar]
Har bir yuqori vaqt oralig'i uchun alohida RunningCandleBuffer yaratiladi. Har bir daqiqalik shamda barcha buferlar yangilanadi, bu bizga har bir vaqt oralig'ining joriy holatini beradi — xuddi bot real vaqtda ishlayotgandek.
Parquet Keshining Tuzilishi
Oldindan hisoblashning natijasi — har bir qator bitta daqiqalik shamga mos keladigan yagona parquet fayli bo'lib, ustunlarda quyidagilar mavjud:
timestamp — minute candle timestamp
open, high, low, — minute candle OHLCV
close, volume
close_5m — Close of the emulated 5m candle at this moment
close_1h — Close of the emulated 1h candle
close_4h — Close of the emulated 4h candle
close_1d — Close of the emulated daily candle
ma_20_1h — MA(20) on 1h, recalculated at this minute
ma_50_1h — MA(50) on 1h
ma_20_4h — MA(20) on 4h
ma_50_4h — MA(50) on 4h
ma_6_1d — MA(6) on Daily
ma_12_1d — MA(12) on Daily
cross_ma_1h — MA crossover signal on 1h ('buy'/'sell'/None)
cross_ma_4h — MA crossover signal on 4h
cross_ma_1d — MA crossover signal on Daily
separation_1h — MA divergence in % on 1h
separation_4h — MA divergence in % on 4h
separation_1d — MA divergence in % on Daily
Har bir qiymat tegishli daqiqalik sham paytidagi indikatorning haqiqiy holatini aks ettiradi — yuqori vaqt oraliqlarining yopilmagan barlarini hisobga olgan holda.
Precompute: Keshni Qurish
def precompute_cache(
df_1m: pd.DataFrame,
timeframes: dict[str, int], # {"5m": 300, "1h": 3600, "4h": 14400, "D": 86400}
indicators: dict, # {"ma_20": 20, "ma_50": 50}
) -> pd.DataFrame:
"""
Single pass through all minute candles.
Returns a DataFrame with emulated timeframes and indicators.
"""
buffers = {tf: RunningCandleBuffer(secs) for tf, secs in timeframes.items()}
n = len(df_1m)
result = {}
for tf_name, buf in buffers.items():
closes = np.zeros(n)
ma_values = {name: np.full(n, np.nan) for name in indicators}
for i in range(n):
row = df_1m.iloc[i]
bars = buf.update(
df_1m.index[i],
row['open'], row['high'], row['low'], row['close'], row['volume']
)
all_closes = [b['close'] for b in bars]
closes[i] = all_closes[-1]
for ind_name, length in indicators.items():
if len(all_closes) >= length:
ma_values[ind_name][i] = np.mean(all_closes[-length:])
result[f'close_{tf_name}'] = closes
for ind_name in indicators:
result[f'{ind_name}_{tf_name}'] = ma_values[ind_name]
cache_df = pd.DataFrame(result, index=df_1m.index)
cache_df = pd.concat([df_1m[['open', 'high', 'low', 'close', 'volume']], cache_df], axis=1)
return cache_df
cache = precompute_cache(
df_1m,
timeframes={"5m": 300, "1h": 3600, "4h": 14400, "D": 86400},
indicators={"ma_20": 20, "ma_50": 50, "ma_6": 6, "ma_12": 12},
)
cache.to_parquet("cache_ETHUSDT_2024_2026.parquet")
Optimallashtirish Paytida Keshdan Foydalanish

Endi optimallashtirish quyidagicha ko'rinadi:
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")
for params in parameter_grid:
result = run_strategy(cache, params)
Strategiya oldindan tayyorlangan ustunlar bilan ishlaydi — million barlarni takroran aylanib chiqish yo'q, harakatlanuvchi o'rtachalarni qayta hisoblash yo'q, vaqt oralig'ini emulyatsiya qilish yo'q. Faqat DataFrame'dan o'qish va kirish/chiqish shartlarini tekshirish.
Nega Aynan Parquet
Parquet — bu vazifa uchun optimal bo'lgan ustunli ma'lumotlarni saqlash formati:
- Siqish. Parquet raqamli ma'lumotlarni 5-10 marta siqadi. 1,1 million qator va 30 ustunli kesh CSV formatidagi ~500 MB o'rniga ~50 MB egallaydi.
- Ustunli o'qish. Agar strategiya faqat
ma_20_4hvama_50_4hdan foydalansa, parquet faqat shu ustunlarni o'qib, qolganini o'tkazib yuboradi. - Turlarni saqlash. Ma'lumot turlari (float64, int64, string) yo'qotishsiz saqlanadi — yuklashda satrlarni tahlil qilish (parse) shart emas.
- O'qish tezligi. Parquetni pandas'ga yuklash o'nlab millisekund vaqt oladi, bu CSV'dan bir tartibga tezroq.
Keshni Kengaytirish: Yangi Indikatorlar Qo'shish
Agar strategiyaga yangi indikator kerak bo'lsa (RSI, MACD, Bollinger tasmalari), shunchaki:
- O'sha daqiqalik ma'lumotlardan faqat yangi indikatorni qayta hisoblang
- Ustunlarni mavjud parquet fayliga qo'shing
- Oldin hisoblangan barcha ustunlar o'zgarishsiz qoladi
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")
rsi_cols = compute_rsi_for_timeframes(df_1m, timeframes, length=14)
cache = pd.concat([cache, rsi_cols], axis=1)
cache.to_parquet("cache_ETHUSDT_2024_2026.parquet")
Xulosa: Yondashuvlarni Taqqoslash
| Sodda Yondashuv | Yig'ilgan Kesh | |
|---|---|---|
| Vaqt oralig'ini qayta namunalash | Har bir iteratsiya | Bir marta |
| Indikatorni hisoblash | Har bir iteratsiya | Bir marta |
| Iteratsiya boshiga sarflanadigan vaqt | Daqiqalar | Bir soniyadan kam |
| 1000 iteratsiya | Kunlar | Daqiqalar |
| Xotira sarfi | 1m yuklash + qayta hisoblash | Yagona DataFrame |
| Backtest-live moslik | Amalga oshirishga bog'liq | Kafolatlangan (emulyatsiya = real vaqt) |
Xulosa
Yig'ilgan parquet kesh yondashuvi ikkita muammoni bir vaqtning o'zida hal qiladi:
-
To'g'rilik. RunningCandleBuffer orqali daqiqalik shamlardan vaqt oraliqlarini emulyatsiya qilish backtest bot real vaqtda ko'radigan xuddi shu ma'lumotlarni ko'rishini kafolatlaydi — kelajakka qarash va sun'iy kechikishlar yo'q.
-
Tezlik. Oldindan hisoblangan vaqt oraliqlari va indikatorlar parametrlarning minglab kombinatsiyalarini kunlar o'rniga daqiqalarda sinash imkonini beradi.
G'oya oddiy: bir marta hisoblash — ko'p marta qayta foydalanish. Daqiqalik shamlar manba ma'lumotdir. Qolgan hamma narsa hosila bo'lib, oldindan hisoblanishi va keshlanishi mumkin. Parquet bu keshni ixcham, tez va qulay qiladi.
Bajarilishni simulyatsiya qilish aniqligini daqiqalardan soniya va millisekundlargacha moslashuvchan chuqurlashtirish (drill-down) orqali qanday yaxshilash mumkinligi haqida ko'proq ma'lumot uchun Moslashuvchan chuqurlashtirish: o'zgaruvchan granulyarlikdagi backtest maqolasiga qarang.
Foydali Havolalar
- Apache Parquet — data storage format
- pandas — working with parquet
- Lopez de Prado — Advances in Financial Machine Learning
- Ernest Chan — Quantitative Trading
Iqtibos
@article{soloviov2026parquetcache,
author = {Soloviov, Eugen},
title = {Aggregated Parquet Cache: How to Speed Up Multi-Timeframe Backtests by Hundreds of Times},
year = {2026},
url = {https://marketmaker.cc/ru/blog/post/parquet-cache-multitimeframe-backtest},
description = {How to precompute timeframes and indicators from minute candles, save them to parquet, and use them for mass strategy testing without redundant recalculations.}
}
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.