← 返回文章列表
March 16, 2026
5 分鐘閱讀

聚合 Parquet 快取:如何將多時間框架回測加速數百倍

聚合 Parquet 快取:如何將多時間框架回測加速數百倍
#演算法交易
#回測
#多時間框架
#parquet
#最佳化
#快取
Part 3 of 10 · Collection
High-Performance Backtest Engines

多時間框架策略同時使用多個時間框架:日線確定趨勢方向,小時線確定入場點,5分鐘線確定執行時機。每個時間框架需要自己的指標:移動平均線、振盪器、支撐阻力位。

單次回測很簡單——從分鐘資料重新計算時間框架,計算指標,執行策略。但在大規模最佳化時——需要測試數千種參數組合——每次迭代重新計算時間框架和指標就成了瓶頸。對兩年的分鐘資料進行一次遍歷意味著處理超過一百萬根K線,重複一千次是極其浪費的。

解決方案:一次預計算所有內容並快取到 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步(一次性):
  分钟级K线 → 时间框架重采样 → 指标计算 → Parquet 文件

第2步(多次):
  Parquet 文件 → 使用不同参数的策略 → 结果

從分鐘級K線模擬時間框架

Real-time timeframe emulation visualization

我們擁有完整的分鐘級K線存檔。從中可以精確重現任何更高時間框架。但有一個細節:使用標準 resample 時,每個週期只得到一行(每小時一行,每4小時一行等)。這不適用於逐分鐘回測——我們需要知道每分鐘的指標值。

因此,我們為每根分鐘K線模擬更高時間框架的值,模擬機器人在即時環境中看到的資料:

  1. 機器人接收下一根分鐘K線
  2. 更新當前(未收盤的)更高時間框架K線——重新計算最高價、最低價、收盤價、成交量
  3. 基於所有已收盤K線加上當前部分K線重新計算指標
  4. 當週期結束時——K線定型並開始新的K線

這種方法保證回測看到的資料與機器人即時看到的完全一致。不會窺探未來——每根分鐘K線嚴格按照該時刻可用的資料進行處理。

class RunningCandleBuffer:
    """
    使用1分钟K线模拟更高时间框架K线的实时更新。
    """
    def __init__(self, period_seconds: int):
        self.period = period_seconds  # 日线为86400,1小时为3600
        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。每收到一根分鐘K線,所有緩衝區都會更新,我們就能獲得每個時間框架的當前狀態——就像機器人在即時執行一樣。

Parquet 快取結構

預計算的結果是一個 parquet 檔案,其中每行對應一根分鐘K線,列包含:

timestamp              — 分钟K线时间戳
open, high, low,       — 分钟K线 OHLCV
close, volume

close_5m               — 该时刻模拟的5分钟K线收盘价
close_1h               — 模拟的1小时K线收盘价
close_4h               — 模拟的4小时K线收盘价
close_1d               — 模拟的日线收盘价

ma_20_1h               — 1小时的 MA(20),在该分钟重新计算
ma_50_1h               — 1小时的 MA(50)
ma_20_4h               — 4小时的 MA(20)
ma_50_4h               — 4小时的 MA(50)
ma_6_1d                — 日线的 MA(6)
ma_12_1d               — 日线的 MA(12)

cross_ma_1h            — 1小时 MA 交叉信号('buy'/'sell'/None)
cross_ma_4h            — 4小时 MA 交叉信号
cross_ma_1d            — 日线 MA 交叉信号

separation_1h          — 1小时 MA 偏离度(百分比)
separation_4h          — 4小时 MA 偏离度(百分比)
separation_1d          — 日线 MA 偏离度(百分比)

每個值反映了對應分鐘K線時刻指標的真實狀態——考慮了更高時間框架的未收盤K線。

預計算:構建快取

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:
    """
    一次遍历所有分钟K线。
    返回包含模拟时间框架和指标的 DataFrame。
    """
    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-based optimization speedup comparison

現在最佳化變成了這樣:

cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")

for params in parameter_grid:
    result = run_strategy(cache, params)

策略直接使用預構建的列——無需重複遍歷百萬根K線,無需重新計算MA,無需模擬時間框架。只需從 DataFrame 讀取資料並檢查入場/出場條件。

為什麼選擇 Parquet

Parquet 是一種列式資料儲存格式,非常適合此任務:

  • 壓縮。 Parquet 將數值資料壓縮 5-10 倍。110萬行30列的快取僅需約50 MB,而 CSV 格式需要約500 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")

總結:方法對比

樸素方法 聚合快取
時間框架重取樣 每次迭代 一次
指標計算 每次迭代 一次
單次迭代時間 數分鐘 不到一秒
1000次迭代 數天 數分鐘
記憶體消耗 載入1分鐘資料 + 重新計算 單個 DataFrame
回測-實盤一致性 取決於實現 有保證(模擬 = 即時)

結論

聚合 parquet 快取方法同時解決了兩個問題:

  1. 正確性。 通過 RunningCandleBuffer 從分鐘K線模擬時間框架,保證回測看到的資料與機器人即時看到的一致——不會窺探未來,也不會有人為延遲。

  2. 速度。 預計算的時間框架和指標使得在數分鐘內測試數千種參數組合成為可能,而非數天。

思路很簡單:一次計算——多次使用。分鐘K線是源資料。其他一切都是衍生資料,可以預先計算並快取。Parquet 使這個快取緊湊、快速且便於使用。

關於如何通過從分鐘到秒和毫秒的自適應下鑽來提高成交模擬精度,請參閱文章 自適應下鑽:從分鐘到毫秒的可變粒度回測


參考連結

  1. Apache Parquet — 資料儲存格式
  2. pandas — 使用 parquet
  3. Lopez de Prado — Advances in Financial Machine Learning
  4. 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 = {如何从分钟级K线预计算时间框架和指标,保存为 parquet 文件,并在大规模策略测试中使用,避免重复计算。}
}
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

Authors

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。