Look-Ahead Bias: Bagaimana Kesalahan Satu Bar Menciptakan Sharpe 15 dari Noise Murni
Bagian dari seri "Backtests Without Illusions".
📄 Artikel ini berkembang menjadi sebuah makalah penelitian. Tiga kebocoran look-ahead halus diuji secara terkontrol terhadap ground truth yang diketahui (4,000 riwayat simulasi). Baca makalahnya online (versi interaktif + PDF) di lookahead.marketmaker.cc, kode dan data di github.com/suenot/lookahead-inflation.
Beberapa minggu lalu benchmark pencarian parameter kami berbohong kepada kami, dan kami hampir tidak menyadarinya.
Engine-nya tampak bersih. Logika closed-bar, pemisahan rolling walk-forward yang jujur, pencarian Sobol/QMC di seluruh ruang parameter, jendela test yang di-hold-out. Pencarian menemukan konfigurasi yang tampak bagus in-sample. Satu-satunya masalah: out-of-sample, hampir semuanya negatif. Kami mengira strategi tersebut memang lemah.
Lalu kami menemukan satu baris. Sinyal diputuskan pada penutupan bar i, tetapi fill dibukukan pada bar i yang sama alih-alih pada open bar berikutnya. Satu off-by-one pada indeks eksekusi. Kami memindahkan fill ke open[i+1] — satu-satunya harga yang benar-benar bisa Anda transaksikan setelah melihat penutupan bar-i — dan hasil out-of-sample berbalik tanda. Pencarian Sobol berubah dari kerugian menjadi keuntungan. Tidak ada yang berubah dari strateginya. Kami hanya berhenti berdagang di masa lalu.
Itulah look-ahead bias, dan bagian yang meresahkan adalah betapa kecil kesalahannya dan betapa besar konsekuensinya. Artikel ini adalah audit-diri terkontrol: kami membangun simulator dengan ground truth yang diketahui secara konstruksi, menyuntikkan kebocoran halus satu per satu, dan mengukur secara tepat berapa banyak masing-masing menggelembungkan backtest. Headline-nya: dengan tanpa edge nyata sama sekali, fill same-bar menciptakan Sharpe tahunan +14.8 dari noise murni.
Apa sebenarnya look-ahead bias itu

Look-ahead bias adalah titik mana pun dalam pipeline Anda di mana sebuah keputusan atau pengukuran menggunakan informasi yang tidak akan tersedia, secara real time, pada saat digunakan. Contoh buku teksnya kasar — menggunakan laba tahunan penuh sebuah saham pada bulan Januari, atau restatement yang belum dipublikasikan. Itu mudah dikenali. Yang lolos dari code review itu halus, dan bersembunyi di tiga tempat:
- Eksekusi — Anda memutuskan pada bar
idan fill pada bari(atau menggunakan high/low bariuntuk stop pada bar yang sama yang menghasilkan sinyal). Anda bertransaksi pada harga yang berkorelasi dengan hal yang memicu Anda. - Normalisasi — Anda melakukan z-score, min-max, atau skala fitur lainnya menggunakan statistik yang dihitung di atas seluruh seri, termasuk masa depan. Scaler-nya "tahu" test set.
- Indikator / fitur — Anda melakukan smoothing atau filtering dengan window yang berpusat (atau dengan cara lain mengintip ke depan), sehingga nilai pada bar
isudah mengandung sepotong bari+1.
Ketiganya adalah bentuk dari apa yang disebut literatur machine learning sebagai leakage (kebocoran): kontaminasi training/evaluasi dengan informasi dari masa depan target (Kaufman et al., 2012; Kapoor & Narayanan, 2023). Dalam keuangan, rujukan kanoniknya adalah Advances in Financial Machine Learning karya López de Prado (2018) — purged cross-validation, embargo, bahaya-bahaya backtesting. Disiplin point-in-time ini setidaknya sudah ada sejak Fama & French (1992), yang dengan sengaja menunda data akuntansi enam bulan agar variabelnya diketahui sebelum return yang dijelaskannya.
Pertanyaan yang dijawab artikel ini bersifat kuantitatif: bukan "apakah kebocoran itu buruk" (semua orang setuju) tetapi "berapa poin Sharpe yang dihasilkan setiap bentuknya, dan mana yang berbahaya?" Tanpa angka Anda tidak bisa bernalar tentang itu. Anda tidak bisa membedakan apakah penggelembungan +0.3 itu noise atau penggelembungan +14 itu bukti nyata.
Simulator dengan ground truth yang diketahui

Untuk mengukur penggelembungan Anda perlu tahu kebenarannya. Data nyata tidak pernah memberi tahu Anda kebenarannya — ia memberi Anda satu realisasi dan tidak ada oracle. Jadi kami membangun pasar sintetis di mana kami yang menentukan edge-nya.
Proses pembangkit data ini bersifat kausal secara ketat dan tidak eksplosif:
Di sini adalah drift laten persisten yang eksogen (sebuah AR(1) dengan ), dan return bar memiliki drift kecil yang diketahui satu bar sebelumnya. Karena tidak bergantung pada return masa lalu, tidak ada feedback dan tidak ada yang meledak. Parameter adalah dial untuk seberapa besar edge nyata yang ada:
- — null: tidak ada edge sama sekali. Sharpe backtest positif apa pun adalah 100% artefak.
- — edge nyata yang dapat diperdagangkan: aturan momentum yang jujur benar-benar menghasilkan uang.
Strateginya sengaja dibuat sederhana — aturan tanda momentum. Fitur-nya adalah jumlah trailing- dari return ( bar), dan posisinya adalah tandanya:
csum = np.concatenate(([0.0], np.cumsum(r))) # csum[k] = sum r[0..k-1]
mom = np.full(n, np.nan)
tt = np.arange(L - 1, n)
mom[tt] = csum[tt + 1] - csum[tt - L + 1]
signal = np.sign(mom) # position for the next bar
Fitur momentum ini adalah kendaraan sempurna untuk mempelajari kebocoran same-bar, karena ia memiliki properti yang dimiliki bersama indikator nyata: ia secara mekanis mengandung bar saat ini. mom[t] mencakup r[t]. Jadi jika Anda membukukan r[t] sebagai trade Anda, Anda sebagian bertaruh pada kuantitas yang sudah ada di dalam sinyal Anda sendiri. Itulah kebocorannya, dibuat konkret.
Setup: (volatilitas per-bar 1%), fee satu arah 0.00045 (round-trip 0.09%, sesuai dengan engine kami), Sharpe ditahunkan dengan (bar per jam), 4,000 riwayat independen dari 4,000 bar masing-masing. Semuanya di-seed dan deterministik.
Pipeline yang jujur (satu-satunya yang dapat diperdagangkan)
Putuskan pada penutupan bar t, dapatkan return bar berikutnya, bayar fee pada perubahan posisi:
def sharpe(sig, ret_booked):
dpos = np.abs(np.diff(np.concatenate(([0.0], sig))))
pnl = sig * ret_booked - FEE_ONEWAY * dpos
return pnl.mean() / pnl.std() * np.sqrt(8760)
honest = sharpe(signal[idx], r[idx + 1]) # earn r[t+1]: tradable
Tiga kebocoran, masing-masing satu perubahan bedah
same_bar = sharpe(signal[idx], r[idx])
z_full = (mom - mom[valid].mean()) / mom[valid].std()
norm_full = sharpe(np.sign(z_full[idx]), r[idx + 1])
z_sm = (mom[:-2] + mom[1:-1] + mom[2:]) / 3.0 # uses t-1, t, t+1
indicator = sharpe(np.sign(z_sm[idx]), r[idx + 1])
Setiap kebocoran hanya berjarak satu baris dari pipeline yang jujur. Itulah inti persoalannya: ini bukan kesalahan eksotis, ini adalah jenis kesalahan yang lolos review.
Hasil: magnitudo setiap kebocoran

Dijalankan pada 4,000 seed, berikut Sharpe tahunan yang dilaporkan setiap pipeline, di bawah null (tanpa edge) dan di bawah edge nyata (, disetel agar Sharpe jujurnya adalah angka yang masuk akal +1.57):
| Pipeline | Null (tanpa edge) | Edge nyata |
|---|---|---|
| Jujur (kebenarannya) | −0.74 | +1.57 |
| Fill same-bar | +14.79 | +15.85 |
| Intipan indikator (1 bar) | +4.76 | +6.62 |
| Normalisasi whole-series | −0.84 | +1.46 |
Interval kepercayaan 95% di antara seed berada di ±0.05 atau lebih ketat di setiap sel; uji-t berpasangan pada penggelembungan sangat signifikan secara astronomis di mana efeknya nyata (t > 400, p ≈ 0).
Baca kolom null terlebih dahulu, karena itu adalah eksperimen paling bersih yang mungkin: tidak ada edge, sehingga pipeline yang jujur dengan benar merugi (−0.74, drag dari membayar fee untuk memperdagangkan noise). Sekarang lihat apa yang dilakukan kebocoran-kebocoran itu terhadap ketiadaan yang sama itu:
- Fill same-bar: −0.74 → +14.79. Strategi tanpa daya prediksi sama sekali, memperdagangkan noise acak, melaporkan Sharpe tahunan hampir 15. Ini bukan bias halus; ini adalah fabrikasi. Mekanismenya persis seperti yang kami bangun: fitur momentum mengandung
r[t], jadi membukukanr[t]berarti bertaruh pada sinyal Anda sendiri. - Intipan indikator: −0.74 → +4.76. Membiarkan smoother melihat satu bar ke masa depan menciptakan Sharpe mendekati 5 dari noise, karena nilai yang di-smooth pada
tsekarang berkorelasi denganr[t+1]yang akan segera Anda dapatkan. - Normalisasi whole-series: −0.74 → −0.84. Praktis tidak ada penggelembungan. Ini adalah temuan yang jujur dan tidak terduga (lebih lanjut di bawah).
Kolom edge menyampaikan pesan yang lebih berbahaya. Ketika edge nyata memang ada (jujur +1.57), kebocoran-kebocoran itu tidak hanya menambahkan konstanta — mereka mendorong Sharpe terukur hingga +15.85 dan +6.62, jauh di atas +1.57 yang benar-benar bisa Anda perdagangkan. Jadi angka terukur tidak bisa membedakan skill dari kebocoran. Angka +6 yang bocor dan +6 yang jujur terlihat identik di laporan. Anda baru tahu mana yang sebenarnya setelah menyalurkan modal.
Kebocorannya adalah gradien, bukan sakelar

Keberatan yang wajar: "membukukan seluruh bar sinyal adalah kesalahan ekstrem yang tidak realistis." Jadi kami menyapu dosis — fraksi dari bar sinyal yang ditangkap oleh kebocoran, dari 0 (jujur) hingga 1 (same-bar penuh):
| Fraksi tangkapan | Sharpe Null | Sharpe Edge |
|---|---|---|
| 0.00 (jujur) | −0.74 | +1.57 |
| 0.25 | +3.90 | +6.41 |
| 0.50 | +9.86 | +12.20 |
| 1.00 (kebocoran penuh) | +14.79 | +15.85 |
Menangkap hanya seperempat dari bar sinyal membawa strategi tanpa edge dari −0.74 ke +3.90. Anda tidak perlu off-by-one penuh untuk tertipu; fill yang sedikit terlalu menguntungkan — sedikit slippage optimistis pada bar sinyal, stop intrabar yang diperiksa terhadap bar yang memicunya — sudah cukup untuk melewati sebagian besar ambang batas "layak digunakan". Penggelembungannya mulus dan monoton terhadap seberapa banyak masa kini yang Anda izinkan diri Anda perdagangkan.
Seberapa sering ini meloloskan strategi merugi ke produksi?
Angka yang seharusnya mengkhawatirkan seorang praktisi adalah tingkat false-deployment: seberapa sering kebocoran membuat konfigurasi yang benar-benar merugi lolos ambang batas yang akan Anda gunakan untuk meluncurkannya. Menggunakan "Sharpe tahunan ≥ 1.0" sebagai kriteria deploy, di bawah null:
- Fill same-bar: 68% strategi tanpa edge tampak layak deploy dan benar-benar merugi. Dua dari tiga konfigurasi noise murni akan lolos gerbang Sharpe-≥-1 dan merugi saat live. (Tingkat ini terdefinisi dengan bersih di sini karena kebocorannya murni pada eksekusi — padanan jujurnya adalah sinyal yang sama dengan fill yang jujur.)
- Intipan indikator: ini juga mendorong hampir semua konfigurasi tanpa edge melewati ambang deploy (99.9% lolos Sharpe ≥ 1) — ia akan melambaikan noise langsung ke produksi.
- Normalisasi whole-series: 12% lolos ambang batas — pada dasarnya sama dengan tingkat dasar noise, tanpa premi kebocoran.
Taksonomi, dan cara mendeteksi masing-masing
Ketiga kebocoran ini tidak sama berbahayanya, dan perbedaannya bersifat instruktif.
1. Kebocoran eksekusi (yang mahal)

Gejala: harga fill berkorelasi dengan sinyal karena keduanya berasal dari bar yang sama. Magnitudo: sangat besar (+15 dari noise pada dosis penuh, +3.9 pada dosis seperempat). Mengapa ini yang terburuk: sinyal Anda, hampir menurut definisi, dibangun dari pergerakan harga terkini, sehingga return bar sinyal adalah persis hal yang paling berkorelasi dengan fitur Anda. Membukukannya hampir sama dengan melihat jawabannya.
Deteksi — uji pergeseran satu bar. Ini adalah diagnostik paling berharga dalam artikel ini. Ambil backtest Anda dan geser setiap fill satu bar lebih lambat (putuskan pada i, fill pada open[i+1]). Jika hasilnya nyaris tidak bergerak, eksekusi Anda jujur. Jika hasilnya runtuh atau berbalik tanda, Anda sedang berdagang di masa lalu. Ini persis yang terjadi pada pencarian Sobol kami: geser fill-nya, dan OOS yang "menguntungkan" ternyata merugi — atau lebih tepatnya, hubungan yang sebenarnya muncul begitu kebocoran itu dihilangkan.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Kebocoran indikator / fitur (yang diam-diam)
Gejala: sebuah indikator pada bar i bergantung pada data dari i+1 atau setelahnya — moving average berpusat, filter tanpa delay kausal, label puncak/lembah yang butuh bar masa depan untuk konfirmasi, transformasi gaya Heikin-Ashi yang diberi candle masa depan. Magnitudo: besar (+4.8 dari noise). Mengapa ini bersembunyi: kebocorannya terkubur di dalam panggilan library. scipy.signal.filtfilt bersifat zero-phase — dan zero-phase berarti non-kausal. Fitur "bar ini adalah maksimum lokal" tidak dapat diketahui sampai bar berikutnya tercetak.
Deteksi: untuk setiap indikator, tanyakan apa indeks tertinggi yang dibacanya? Jika menghitung nilai pada t pernah menyentuh t+1, itu non-kausal. Hitung indikator pada window kausal expanding/rolling dan verifikasi bahwa nilai pada bar t identik terlepas dari apakah bar setelah t ada di array atau tidak. (Implementasi HMA/ADX kami lolos uji ini: setiap output pada t hanya membaca input pada ≤ t.)
3. Kebocoran normalisasi (yang spesifik-kanal)
Gejala: sebuah scaler (StandardScaler, min-max, z-score global) di-fit pada seluruh dataset, termasuk test set. Peringatan ML kanonik tentang ini eksplisit — Hastie, Tibshirani & Friedman's Elements of Statistical Learning §7.10.2 ("the wrong and right way to do cross-validation"), dan panduan common-pitfalls milik scikit-learn sendiri (tautan): "the average should be the average of the train subset, not the average of all the data."
Magnitudo dalam uji kami: ≈ nol (−0.74 → −0.84). Ini adalah hasil yang mengejutkan dan jujur, dan layak dipahami daripada sekadar dihafal.
Mengapa ini tidak menggelembungkan? Karena strategi kami hanya menggunakan fitur melalui tanda-nya (ambang batas nol). Penskalaan standar deviasi tidak pernah mengubah tanda, dan pemusatan mean-global hanya menggeser titik potong nol sedikit. Jadi standardisasi whole-series dari aturan tanda murni hampir tidak berbahaya.
Jangan menggeneralisasi ini secara berlebihan. Kebocoran normalisasi bersifat spesifik-kanal. Begitu strategi Anda menggunakan magnitudo fitur — position sizing yang proporsional terhadap z-score, ambang batas entry non-nol yang dipilih dengan melihat distribusi yang diskalakan, jaringan saraf yang mengkonsumsi input yang distandardisasi — scaler yang sadar-masa-depan itu mulai berpengaruh, dan pengaruhnya makin besar semakin statistik global berbeda dari statistik kausal. Hasil kami bukan berarti "kebocoran normalisasi itu aman." Ini adalah "magnitudo kebocoran bergantung pada kanal tempat kuantitas yang bocor masuk ke dalam keputusan, dan Anda harus mengukurnya alih-alih mengasumsikannya." Aturan tanda adalah satu kasus di mana kebocoran khusus ini murah.
Di mana ini terhubung
Look-ahead bias adalah mata rantai pertama dalam rangkaian yang telah didokumentasikan seri ini:
- Ia mengontaminasi input ke validasi. Backtest yang bocor akan lolos mulus melewati pemisahan walk-forward dan terlihat seperti plateau luas alih-alih puncak overfit — kebocorannya konsisten di semua fold, sehingga cross-validation tidak dapat menangkapnya. Kebocoran adalah mode kegagalan yang hulu dari overfitting, dan tidak ada validasi jujur di hilir yang akan menyelamatkan Anda.
- Ia berinteraksi dengan pencarian parameter: pencarian ribuan trial pada data yang bocor akan menemukan konfigurasi yang mengeksploitasi kebocoran itu paling agresif. "Pemenangnya" adalah pelanggar terburuk.
- Ia adalah alasan mengapa paritas backtest-live menyimpang. Kebocoran adalah penjelasan paling bersih untuk kesenjangan 30–50% antara backtest dan bot, karena trading live adalah, secara mekanis, satu-satunya tempat di mana Anda tidak bisa mengintip.
Disiplin yang menangkap semua ini sama dengan yang selama bertahun-tahun didesak oleh literatur akademik: perlakukan backtest sebagai eksperimen statistik dengan batas informasi yang ketat. Bailey, Borwein, López de Prado & Zhu menunjukkan betapa mudahnya overfitting menciptakan performa palsu (2014); protokol backtesting Arnott, Harvey & Markowitz (2019) mengkodifikasikan kebersihan prosesnya. Look-ahead bias adalah batas paling dasar dari semuanya — batas dalam waktu — dan yang paling murah untuk dilanggar secara tidak sengaja.
Poin Penting

- Look-ahead bias secara kuantitatif sangat besar dan secara kualitatif tak terlihat. Satu kesalahan eksekusi satu-bar mengubah Sharpe dari −0.74 (noise murni, merugi dengan benar) menjadi +14.79. Kesalahannya satu baris; konsekuensinya adalah track record fabrikasi.
- Ini adalah gradien. Menangkap bahkan 25% dari bar sinyal menghasilkan +3.90 dari ketiadaan. Anda tidak butuh bug yang mencolok — sedikit terlalu optimis pada fill Anda saja sudah cukup.
- Angka terukur tidak bisa membedakan skill dari kebocoran. Ketika edge nyata ada, kebocoran menggelembungkan laporan jauh melampaui kebenaran yang dapat diperdagangkan. Satu-satunya pertahanan adalah proses, bukan metrik.
- Uji pergeseran satu bar adalah diagnostik tercepat Anda. Geser setiap fill satu bar lebih lambat. Jika performa runtuh, Anda sedang berdagang di masa lalu.
- Magnitudo kebocoran bersifat spesifik-kanal. Intipan eksekusi dan indikator sangat merusak; normalisasi whole-series dari aturan tanda hampir gratis. Ukur kebocoran melalui kanal tempat ia benar-benar masuk — jangan berasumsi.
Studi terkontrol lengkap — ketiga kebocoran, sapuan dosis, analisis false-deployment, metode formal, dan setiap angka yang dapat direproduksi dari satu skrip deterministik — ada di makalah pendamping di lookahead.marketmaker.cc, dengan kode dan data di github.com/suenot/lookahead-inflation.
Strategi dalam eksperimen null kami tidak memiliki edge sama sekali. Ia tetap menunjukkan Sharpe 15. Jika backtest Anda terlihat terlalu bagus, hal pertama yang harus dicurigai bukanlah kejeniusan Anda — melainkan jam Anda.
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.