Агрегатталган 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 MB ордуна ~50 MB алат.
- Мамычалуу окуу. Эгер стратегия бир гана
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.