集約Parquetキャッシュ:マルチタイムフレーム・バックテストを数百倍高速化する方法
マルチタイムフレーム戦略は複数の時間足を同時に使用します:日足でトレンド方向を判断し、1時間足でエントリーポイントを特定し、5分足で約定タイミングを絞り込みます。各時間足には独自のインジケーター(移動平均、オシレーター、水準)が必要です。
単一のバックテストではすべてが簡単です — 分足データから時間足を再計算し、インジケーターを算出し、戦略を実行します。しかし大量最適化の場合 — 数千のパラメーター組み合わせをテストする必要がある場合 — 毎回のイテレーションで時間足とインジケーターを再計算することがボトルネックになります。2年間の分足データを1回通すだけで100万本以上のバーを処理する必要があり、それを1000回繰り返すのは無駄です。
解決策:すべてを一度計算し、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)のみです。壁の色を試すたびに家全体を建て直すようなものです。
アイデア:一度計算し、保存し、何度も再利用
重要な観察:時間足とインジケーターは分足データとインジケーターのパラメーターにのみ依存し、戦略パラメーターには依存しない。必要なインジケーターのセットを固定すれば、一度計算して保存できます。
スキーム:
ステップ1(1回):
分足 -> 時間足リサンプリング -> インジケーター計算 -> Parquetファイル
ステップ2(何度も):
Parquetファイル -> 異なるパラメーターの戦略 -> 結果
分足からの時間足エミュレーション

分足の完全なアーカイブがあります。これからあらゆる上位時間足を正確に再現できます。ただし注意点があります:標準のresampleでは期間ごとに1行が得られます(1時間に1行、4時間に1行など)。これは分足ごとのバックテストには使えません — 毎分のインジケーター値を知る必要があります。
そのため、各分足に対して上位時間足の値をエミュレートし、ボットがリアルタイムでデータを見る方法をモデル化します:
- ボットが次の分足を受信
- 上位時間足の現在の(未確定の)バーを更新 — 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ファイルで、各行が1本の分足に対応し、列には以下が含まれます:
timestamp — 分足のタイムスタンプ
open, high, low, — 分足のOHLCV
close, volume
close_5m — この時点でのエミュレートされた5m足のClose
close_1h — エミュレートされた1h足のClose
close_4h — エミュレートされた4h足のClose
close_1d — エミュレートされた日足のClose
ma_20_1h — 1hのMA(20)、この分で再計算
ma_50_1h — 1hのMA(50)
ma_20_4h — 4hのMA(20)
ma_50_4h — 4hのMA(50)
ma_6_1d — 日足のMA(6)
ma_12_1d — 日足のMA(12)
cross_ma_1h — 1hのMAクロスオーバーシグナル ('buy'/'sell'/None)
cross_ma_4h — 4hのMAクロスオーバーシグナル
cross_ma_1d — 日足のMAクロスオーバーシグナル
separation_1h — 1hのMA乖離率(%)
separation_4h — 4hのMA乖離率(%)
separation_1d — 日足のMA乖離率(%)
各値は、対応する分足の時点でのインジケーターの実際の状態を反映しています — 上位時間足の未確定バーを考慮しています。
事前計算:キャッシュの構築
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)
戦略は事前構築された列で動作します — 100万バーの繰り返し通過、MA再計算、時間足エミュレーションは不要です。DataFrameからの読み取りとエントリー/イグジット条件のチェックのみです。
なぜParquetなのか
Parquetはカラムナ型データ保存フォーマットで、このタスクに最適です:
- 圧縮。 Parquetは数値データを5-10倍圧縮します。30列で110万行のキャッシュは、CSVの約500 MBではなく約50 MBになります。
- カラムナ読み取り。 戦略が
ma_20_4hとma_50_4hのみを使用する場合、parquetはそれらの列のみを読み取り、残りをスキップします。 - 型の保存。 データ型(float64、int64、string)はロスレスで保存されます — 読み込み時の文字列パースが不要です。
- 読み取り速度。 parquetをpandasに読み込むのは数十ミリ秒で、CSVより桁違いに高速です。
キャッシュの拡張:新しいインジケーターの追加
戦略に新しいインジケーター(RSI、MACD、ボリンジャーバンド)が必要な場合は、単に:
- 同じ分足データから新しいインジケーターのみを再計算
- 列を既存の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")
まとめ:アプローチの比較
| 素朴なアプローチ | 集約キャッシュ | |
|---|---|---|
| 時間足リサンプリング | 毎回のイテレーション | 1回 |
| インジケーター計算 | 毎回のイテレーション | 1回 |
| イテレーションあたりの時間 | 数分 | 1秒未満 |
| 1000回のイテレーション | 数日 | 数分 |
| メモリ消費 | 1mをロード + 再計算 | 単一のDataFrame |
| バックテスト-ライブの一致 | 実装に依存 | 保証(エミュレーション = リアルタイム) |
結論
集約Parquetキャッシュのアプローチは2つの問題を同時に解決します:
-
正確性。 RunningCandleBufferによる分足からの時間足エミュレーションは、バックテストがリアルタイムのボットと同じデータを見ることを保証します — 未来を覗くことも人為的な遅延もありません。
-
速度。 事前計算された時間足とインジケーターにより、数千のパラメーター組み合わせを数日ではなく数分でテストできます。
アイデアはシンプルです:一度計算 — 何度も再利用。分足はソースデータです。それ以外はすべて派生物であり、事前計算してキャッシュできます。Parquetはこのキャッシュをコンパクトで高速かつ便利にします。
分足から秒足やミリ秒足へのアダプティブ・ドリルダウンによる約定シミュレーションの精度向上については、アダプティブ・ドリルダウン:可変粒度バックテストの記事をご覧ください。
参考リンク
- Apache Parquet — data storage format
- pandas — working with parquet
- Lopez de Prado — Advances in Financial Machine Learning
- Ernest Chan — Quantitative Trading
Citation
@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.}
}
MarketMaker.cc Team
クオンツ・リサーチ&戦略