Cache Parquet Teragregasi: Cara Mempercepat Backtest Multi-Timeframe Ratusan Kali Lipat
Strategi multi-timeframe menggunakan beberapa timeframe secara bersamaan: timeframe harian menentukan arah tren, timeframe per jam mengidentifikasi titik masuk, dan timeframe 5 menit menentukan waktu eksekusi secara presisi. Setiap timeframe memerlukan indikatornya sendiri: moving average, osilator, level.
Untuk satu backtest, semuanya mudah — hitung ulang timeframe dari data per menit, hitung indikator, jalankan strategi. Namun selama optimasi massal — ketika Anda perlu menguji ribuan kombinasi parameter — menghitung ulang timeframe dan indikator pada setiap iterasi menjadi bottleneck. Satu kali iterasi melalui data per menit selama dua tahun berarti memproses lebih dari satu juta bar, dan mengulangnya seribu kali adalah pemborosan.
Solusinya: pra-komputasi semuanya sekali dan simpan dalam file parquet.
Masalah: Perhitungan Redundan Selama Optimasi
Pipeline backtest multi-timeframe yang umum terlihat seperti ini:
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)
Pada setiap iterasi, langkah 1-3 dihitung ulang meskipun datanya sama. Hanya parameter ambang batas strategi yang berubah (langkah 4). Seperti membangun ulang seluruh rumah setiap kali Anda hanya ingin mencoba warna dinding yang berbeda.
Ide: Hitung Sekali, Simpan, Gunakan Berkali-kali
Pengamatan kunci: timeframe dan indikator hanya bergantung pada data per menit dan parameter indikator, bukan pada parameter strategi. Jika kita menetapkan kumpulan indikator yang diperlukan, kita dapat menghitungnya sekali dan menyimpannya.
Skemanya:
Langkah 1 (sekali):
Candle per menit -> Resampling timeframe -> Komputasi indikator -> File Parquet
Langkah 2 (berkali-kali):
File Parquet -> Strategi dengan parameter berbeda -> Hasil
Emulasi Timeframe dari Candle Per Menit

Kita memiliki arsip lengkap candle per menit. Dari sana, kita dapat mereproduksi secara akurat timeframe yang lebih tinggi. Namun ada nuansa: dengan resample standar, kita mendapatkan satu baris per periode (satu baris per jam, satu per 4 jam, dll.). Ini tidak cocok untuk backtest menit-per-menit — kita perlu mengetahui nilai indikator di setiap menit.
Oleh karena itu, kita mengemulasikan nilai timeframe yang lebih tinggi untuk setiap candle per menit, memodelkan bagaimana bot melihat data secara real time:
- Bot menerima candle per menit berikutnya
- Memperbarui bar timeframe yang lebih tinggi saat ini (yang belum tertutup) — menghitung ulang High, Low, Close, Volume
- Menghitung ulang indikator di semua bar yang sudah tertutup ditambah bar parsial saat ini
- Ketika periode berakhir — bar difinalisasi dan bar baru dimulai
Pendekatan ini menjamin bahwa backtest melihat data yang persis sama dengan yang dilihat bot secara real time. Tidak ada melihat ke depan — setiap candle per menit diproses secara ketat dengan data yang tersedia pada saat itu.
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 yang terpisah dibuat untuk setiap timeframe yang lebih tinggi. Pada setiap candle per menit, semua buffer diperbarui, memberi kita status terkini setiap timeframe — seolah-olah bot berjalan secara real time.
Struktur Cache Parquet
Hasil pra-komputasi adalah satu file parquet di mana setiap baris sesuai dengan satu candle per menit, dan kolom berisi:
timestamp — stempel waktu candle per menit
open, high, low, — OHLCV candle per menit
close, volume
close_5m — Close dari candle 5m yang diemulasikan saat ini
close_1h — Close dari candle 1h yang diemulasikan
close_4h — Close dari candle 4h yang diemulasikan
close_1d — Close dari candle harian yang diemulasikan
ma_20_1h — MA(20) pada 1h, dihitung ulang pada menit ini
ma_50_1h — MA(50) pada 1h
ma_20_4h — MA(20) pada 4h
ma_50_4h — MA(50) pada 4h
ma_6_1d — MA(6) pada Daily
ma_12_1d — MA(12) pada Daily
cross_ma_1h — Sinyal persilangan MA pada 1h ('buy'/'sell'/None)
cross_ma_4h — Sinyal persilangan MA pada 4h
cross_ma_1d — Sinyal persilangan MA pada Daily
separation_1h — Divergensi MA dalam % pada 1h
separation_4h — Divergensi MA dalam % pada 4h
separation_1d — Divergensi MA dalam % pada Daily
Setiap nilai mencerminkan status aktual indikator pada saat candle per menit yang sesuai — dengan memperhitungkan bar timeframe yang lebih tinggi yang belum tertutup.
Pra-komputasi: Membangun Cache
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")
Menggunakan Cache Selama Optimasi

Sekarang optimasi terlihat seperti ini:
cache = pd.read_parquet("cache_ETHUSDT_2024_2026.parquet")
for params in parameter_grid:
result = run_strategy(cache, params)
Strategi bekerja dengan kolom yang sudah dibangun sebelumnya — tidak ada iterasi berulang melalui satu juta bar, tidak ada perhitungan ulang MA, tidak ada emulasi timeframe. Cukup membaca dari DataFrame dan memeriksa kondisi masuk/keluar.
Mengapa Parquet
Parquet adalah format penyimpanan data kolumnar, optimal untuk tugas ini:
- Kompresi. Parquet mengompres data numerik 5-10x. Cache dengan 1,1 juta baris dan 30 kolom membutuhkan ~50 MB alih-alih ~500 MB dalam CSV.
- Pembacaan kolumnar. Jika strategi hanya menggunakan
ma_20_4hdanma_50_4h, parquet hanya membaca kolom-kolom tersebut, melewatkan sisanya. - Pelestarian tipe. Tipe data (float64, int64, string) dipertahankan tanpa kehilangan — tidak perlu mengurai string saat memuat.
- Kecepatan baca. Memuat parquet ke pandas membutuhkan puluhan milidetik, jauh lebih cepat daripada CSV.
Memperluas Cache: Menambahkan Indikator Baru
Jika strategi memerlukan indikator baru (RSI, MACD, Bollinger Bands), cukup:
- Hitung ulang hanya indikator baru dari data per menit yang sama
- Tambahkan kolom ke file parquet yang ada
- Semua kolom yang sebelumnya dihitung tetap tidak berubah
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")
Ringkasan: Perbandingan Pendekatan
| Pendekatan Naif | Cache Teragregasi | |
|---|---|---|
| Resampling timeframe | Setiap iterasi | Sekali |
| Komputasi indikator | Setiap iterasi | Sekali |
| Waktu per iterasi | Menit | Kurang dari satu detik |
| 1000 iterasi | Hari | Menit |
| Konsumsi memori | Muat 1m + hitung ulang | Satu DataFrame |
| Paritas backtest-live | Bergantung pada implementasi | Terjamin (emulasi = real-time) |
Kesimpulan
Pendekatan cache parquet teragregasi memecahkan dua masalah sekaligus:
-
Kebenaran. Emulasi timeframe dari candle per menit melalui RunningCandleBuffer menjamin bahwa backtest melihat data yang sama dengan yang dilihat bot secara real time — tidak ada melihat ke depan dan tidak ada penundaan buatan.
-
Kecepatan. Timeframe dan indikator yang telah dihitung sebelumnya memungkinkan pengujian ribuan kombinasi parameter dalam hitungan menit alih-alih hari.
Idenya sederhana: hitung sekali — gunakan berkali-kali. Candle per menit adalah data sumber. Segalanya yang lain adalah turunan dan dapat dihitung sebelumnya serta di-cache. Parquet membuat cache ini ringkas, cepat, dan mudah digunakan.
Untuk lebih lanjut tentang cara meningkatkan akurasi simulasi pengisian dengan adaptive drill-down dari menit ke detik dan milidetik, lihat artikel Adaptive drill-down: backtest dengan granularitas variabel.
Tautan Berguna
- Apache Parquet — format penyimpanan data
- pandas — bekerja dengan parquet
- Lopez de Prado — Advances in Financial Machine Learning
- Ernest Chan — Quantitative Trading
Kutipan
@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/id/blog/post/parquet-cache-multitimeframe-backtest},
description = {Cara pra-komputasi timeframe dan indikator dari candle per menit, menyimpannya ke parquet, dan menggunakannya untuk pengujian strategi massal tanpa perhitungan ulang yang tidak perlu.}
}
Penulis
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.