Агрегатталған Parquet кэші: мультитаймфрейм бэктестерді жүздеген есе қалай жылдамдату керек
Мультитаймфрейм стратегиясы бірнеше таймфреймді бір мезгілде пайдаланады: күндізгі тренд бағытын анықтайды, сағаттық кіру нүктелерін белгілейді, ал 5 минуттық орындау уақытын дәл көрсетеді. Әрбір таймфреймге өз индикаторлары керек: жылжымалы орташалар, осцилляторлар, деңгейлер.
Бір бэктест үшін бәрі қарапайым — минуттық деректерден таймфреймдерді қайта есептеу, индикаторларды есептеу, стратегияны іске қосу. Бірақ жаппай оптимизация кезінде — параметрлердің мыңдаған комбинацияларын тестілеу қажет болғанда — әр итерацияда таймфреймдер мен индикаторларды қайта есептеу тар жол болады. Екі жылдық минуттық деректерден бір рет өту миллионнан астам барды өңдеуді білдіреді, ал мұны мың рет қайталау — ысырап.
Шешім: бәрін бір рет алдын ала есептеп, parquet файлында кэштеу.
Мәселе: оптимизация кезіндегі артық есептеулер
Әдеттегі мультитаймфрейм бэктест конвейері мынадай көрінеді:
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)
Әрбір итерацияда деректер бірдей болса да, 1-3 қадамдар қайта есептеледі. Тек стратегияның шектік параметрлері өзгереді (4-қадам). Бұл қабырға түсін ғана өзгерткің келгенде әр жолы бүкіл үйді қайта салғанмен бірдей.
Идея: бір рет есептеу, сақтау, көп рет қайта пайдалану
Негізгі байқау: таймфреймдер мен индикаторлар тек минуттық деректер мен индикатор параметрлеріне тәуелді, стратегия параметрлеріне емес. Егер қажетті индикаторлар жиынын бекітсек, оларды бір рет есептеп сақтай аламыз.
Сызба:
Step 1 (once):
Minute candles -> Timeframe resampling -> Indicator computation -> Parquet file
Step 2 (many times):
Parquet file -> Strategy with different parameters -> Result
Минуттық шамдардан таймфреймдерді эмуляциялау

Бізде минуттық шамдардың толық архиві бар. Одан кез келген жоғары таймфреймді дәл қалпына келтіре аламыз. Бірақ бір нюанс бар: стандартты resample арқылы біз кезең сайын бір жол аламыз (сағат сайын бір жол, 4 сағат сайын бір жол және т.б.). Бұл минут сайынғы бэктестінг үшін жарамайды — бізге индикатор мәнін әр минут сайын білу керек.
Сондықтан біз әрбір минуттық шам үшін жоғары таймфрейм мәндерін эмуляциялаймыз, ботты нақты уақытта деректерді қалай көретінін модельдей отырып:
- Бот келесі минуттық шамды алады
- Жоғары таймфреймнің ағымдағы (жабылмаған) барын жаңартады — High, Low, Close, Volume қайта есептеледі
- Барлық жабылған барлар мен ағымдағы жартылай бар бойынша индикаторды қайта есептейді
- Кезең аяқталған кезде — бар аяқталады және жаңасы басталады
Бұл тәсіл бэктест бот нақты уақытта көретін дәл сол деректерді көретінін кепілдейді. Болашаққа қарау жоқ — әрбір минуттық шам сол сәтте қолжетімді болатын деректермен қатаң түрде өңделеді.
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]
Әрбір жоғары таймфрейм үшін жеке RunningCandleBuffer жасалады. Әрбір минуттық шамда барлық буферлер жаңартылады, бұл бізге әр таймфреймнің ағымдағы күйін береді — бот нақты уақытта жұмыс істеп жатқандай.
Parquet кэшінің құрылымы
Алдын ала есептеудің нәтижесі — әрбір жол бір минуттық шамға сәйкес келетін жалғыз parquet файлы, ал бағандар мыналарды қамтиды:
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
Әрбір мән сәйкес минуттық шам сәтіндегі индикатордың нақты күйін көрсетеді — жоғары таймфреймдердің жабылмаған барларын ескере отырып.
Precompute: кэшті құру
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")
Оптимизация кезінде кэшті пайдалану

Енді оптимизация мынадай көрінеді:
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")
for params in parameter_grid:
result = run_strategy(cache, params)
Стратегия дайын бағандармен жұмыс істейді — миллион барды қайта-қайта аралап шығу жоқ, MA-ны қайта есептеу жоқ, таймфрейм эмуляциясы жоқ. Тек DataFrame-нен оқу және кіру/шығу шарттарын тексеру.
Неге Parquet
Parquet — бұл осы тапсырма үшін оңтайлы бағаналық деректерді сақтау форматы:
- Сығымдау. Parquet сандық деректерді 5-10 есе сығымдайды. 1,1 миллион жол мен 30 бағаны бар кэш CSV-дегі ~500 МБ орнына ~50 МБ алады.
- Бағаналық оқу. Егер стратегия тек
ma_20_4hменma_50_4h-ты пайдаланса, parquet тек сол бағандарды оқиды, қалғанын өткізіп жібереді. - Түрлерді сақтау. Деректер түрлері (float64, int64, string) жоғалтусыз сақталады — жүктеу кезінде жолдарды талдаудың қажеті жоқ.
- Оқу жылдамдығы. Parquet-ты pandas-қа жүктеу бірнеше ондаған миллисекундты алады, бұл CSV-ден бір ретті шамамен жылдамырақ.
Кэшті кеңейту: жаңа индикаторларды қосу
Егер стратегияға жаңа индикатор қажет болса (RSI, MACD, Bollinger Bands), жай ғана:
- Тек жаңа индикаторды сол минуттық деректерден қайта есептеу
- Бағандарды бар parquet файлына қосу
- Бұрын есептелген барлық бағандар өзгеріссіз қалады
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")
Қорытынды: тәсілдерді салыстыру
| Қарапайым тәсіл | Агрегатталған кэш | |
|---|---|---|
| Таймфреймді ресемплдеу | Әр итерация | Бір рет |
| Индикаторды есептеу | Әр итерация | Бір рет |
| Итерацияға кеткен уақыт | Минуттар | Бір секундтан аз |
| 1000 итерация | Күндер | Минуттар |
| Жад шығыны | 1m жүктеу + қайта есептеу | Жалғыз DataFrame |
| Бэктест-live параллельдігі | Іске асыруға байланысты | Кепілденген (эмуляция = нақты уақыт) |
Қорытынды
Агрегатталған parquet кэші тәсілі екі мәселені бір мезгілде шешеді:
-
Дұрыстық. RunningCandleBuffer арқылы минуттық шамдардан таймфреймдерді эмуляциялау бэктест бот нақты уақытта көретін дәл сол деректерді көретінін кепілдейді — болашаққа қарау жоқ және жасанды кідірістер жоқ.
-
Жылдамдық. Алдын ала есептелген таймфреймдер мен индикаторлар параметрлердің мыңдаған комбинацияларын күндер орнына минуттарда тестілеуге мүмкіндік береді.
Идея қарапайым: бір рет есептеу — көп рет қайта пайдалану. Минуттық шамдар — бастапқы дерек. Қалғанның бәрі туынды және оны алдын ала есептеп, кэштеуге болады. Parquet бұл кэшті ықшам, жылдам және ыңғайлы етеді.
Толтыруды симуляциялау дәлдігін минуттардан секундтар мен миллисекундтарға дейінгі адаптивті бұрғылаумен (drill-down) қалай жақсартуға болатыны туралы толығырақ Адаптивті бұрғылау: айнымалы дәлдіктегі бэктест мақаласын қараңыз.
Пайдалы сілтемелер
- Apache Parquet — data storage format
- pandas — working with parquet
- Lopez de Prado — Advances in Financial Machine Learning
- Ernest Chan — Quantitative Trading
Дәйексөз
@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.