← Kembali ke artikel
March 16, 2026
5 menit baca

Cache Parquet Teragregasi: Cara Mempercepat Backtest Multi-Timeframe Ratusan Kali Lipat

Cache Parquet Teragregasi: Cara Mempercepat Backtest Multi-Timeframe Ratusan Kali Lipat
#algotrading
#backtest
#multi-timeframe
#parquet
#optimasi
#caching

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

Visualisasi emulasi timeframe real-time

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:

  1. Bot menerima candle per menit berikutnya
  2. Memperbarui bar timeframe yang lebih tinggi saat ini (yang belum tertutup) — menghitung ulang High, Low, Close, Volume
  3. Menghitung ulang indikator di semua bar yang sudah tertutup ditambah bar parsial saat ini
  4. 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

Perbandingan peningkatan kecepatan optimasi berbasis cache

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_4h dan ma_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:

  1. Hitung ulang hanya indikator baru dari data per menit yang sama
  2. Tambahkan kolom ke file parquet yang ada
  3. 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:

  1. 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.

  2. 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

  1. Apache Parquet — format penyimpanan data
  2. pandas — bekerja dengan parquet
  3. Lopez de Prado — Advances in Financial Machine Learning
  4. 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.}
}
Penafian: Informasi yang disediakan dalam artikel ini hanya untuk tujuan edukasi dan informasi serta tidak merupakan nasihat keuangan, investasi, atau trading. Trading mata uang kripto mengandung risiko kerugian yang signifikan.

Penulis

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

Selangkah Lebih Maju dari Pasar

Berlangganan newsletter kami untuk wawasan AI trading eksklusif, analisis pasar, dan pembaruan platform.

Kami menghormati privasi Anda. Berhenti berlangganan kapan saja.