← 記事一覧に戻る
March 16, 2026
読了時間: 5分

集約Parquetキャッシュ:マルチタイムフレーム・バックテストを数百倍高速化する方法

集約Parquetキャッシュ:マルチタイムフレーム・バックテストを数百倍高速化する方法
#algotrading
#backtest
#multi-timeframe
#parquet
#optimization
#caching

マルチタイムフレーム戦略は複数の時間足を同時に使用します:日足でトレンド方向を判断し、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)のみです。壁の色を試すたびに家全体を建て直すようなものです。

アイデア:一度計算し、保存し、何度も再利用

重要な観察:時間足とインジケーターは分足データとインジケーターのパラメーターにのみ依存し、戦略パラメーターには依存しない。必要なインジケーターのセットを固定すれば、一度計算して保存できます。

スキーム:

ステップ11回):
  分足 -> 時間足リサンプリング -> インジケーター計算 -> Parquetファイル

ステップ2(何度も):
  Parquetファイル -> 異なるパラメーターの戦略 -> 結果

分足からの時間足エミュレーション

リアルタイム時間足エミュレーションの可視化

分足の完全なアーカイブがあります。これからあらゆる上位時間足を正確に再現できます。ただし注意点があります:標準のresampleでは期間ごとに1行が得られます(1時間に1行、4時間に1行など)。これは分足ごとのバックテストには使えません — 毎分のインジケーター値を知る必要があります。

そのため、各分足に対して上位時間足の値をエミュレートし、ボットがリアルタイムでデータを見る方法をモデル化します:

  1. ボットが次の分足を受信
  2. 上位時間足の現在の(未確定の)バーを更新 — High、Low、Close、Volumeを再計算
  3. すべての確定バーと現在の部分バーでインジケーターを再計算
  4. 期間が終了 — バーが確定し新しいバーが始まる

このアプローチにより、バックテストがリアルタイムのボットとまったく同じデータを見ることが保証されます。未来を覗くことはありません — 各分足はその時点で利用可能だったデータだけで厳密に処理されます。

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_4hma_50_4hのみを使用する場合、parquetはそれらの列のみを読み取り、残りをスキップします。
  • 型の保存。 データ型(float64、int64、string)はロスレスで保存されます — 読み込み時の文字列パースが不要です。
  • 読み取り速度。 parquetをpandasに読み込むのは数十ミリ秒で、CSVより桁違いに高速です。

キャッシュの拡張:新しいインジケーターの追加

戦略に新しいインジケーター(RSI、MACD、ボリンジャーバンド)が必要な場合は、単に:

  1. 同じ分足データから新しいインジケーターのみを再計算
  2. 列を既存のparquetファイルに追加
  3. 以前に計算された列はそのまま
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つの問題を同時に解決します:

  1. 正確性。 RunningCandleBufferによる分足からの時間足エミュレーションは、バックテストがリアルタイムのボットと同じデータを見ることを保証します — 未来を覗くことも人為的な遅延もありません。

  2. 速度。 事前計算された時間足とインジケーターにより、数千のパラメーター組み合わせを数日ではなく数分でテストできます。

アイデアはシンプルです:一度計算 — 何度も再利用。分足はソースデータです。それ以外はすべて派生物であり、事前計算してキャッシュできます。Parquetはこのキャッシュをコンパクトで高速かつ便利にします。

分足から秒足やミリ秒足へのアダプティブ・ドリルダウンによる約定シミュレーションの精度向上については、アダプティブ・ドリルダウン:可変粒度バックテストの記事をご覧ください。


参考リンク

  1. Apache Parquet — data storage format
  2. pandas — working with parquet
  3. Lopez de Prado — Advances in Financial Machine Learning
  4. 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.}
}
blog.disclaimer

MarketMaker.cc Team

クオンツ・リサーチ&戦略

Telegramで議論する
Newsletter

市場の先を行く

ニュースレターを購読して、独占的なAI取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。