Çok Zaman Dilimli Backtestlerde Look-Ahead Olmadığını Kanıtlamak: Geleceği Bozun, Geçmişin Onu Göremediğini Kanıtlayın
"Yanılsamasız Backtestler" dizisinin bir parçasıdır.
Önceki makale bir teşhis testiyle sona eriyordu: tek-bar kaydırma testi (one-bar shift test). Her fill'i bir bar geciktirin; performans çökerse, geçmişte işlem yapıyordunuz demektir. Bu test, look-ahead'in yürütmede (execution) gizlendiği tek-zaman-dilimli bir strateji için yeterlidir — kararı i barında verdiniz ve i barında fill aldınız.
Çok zaman dilimli stratejilerin sızıntı yapabileceği ikinci, daha sessiz bir nokta daha vardır ve kaydırma testi oraya ulaşamaz. 1 dakikalık bir bar üzerindeki bir karar, 1 saatlik bir trende başvurduğunda, sızıntı artık fill'de değildir — henüz oluşumu tamamlanmamış bir üst zaman dilimi barından hesaplanan gösterge (indicator) içindedir. Bu makale tam olarak bu sızıntıyla ve motorunuzda bu sızıntının olmadığını — koda bakarak değil, mekanik olarak — nasıl kanıtlayacağınızla ilgilidir.
Sonunda ortaya koyduğumuz iddia güçlü: multi-TF motorumuz, gerçek ETHUSDT 1 dakikalık verinin 86,400 barı üzerinde 25 parite ve sızıntı kontrolünün 25'ini de geçti; buna, gelecekteki her barı bozan ve tek bir geçmiş kararın bile kıpırdamadığını doğrulayan geleceği-kaydıran (shifted-future) bir probe da dahil. Ve motor, canlı botun kanonik çekirdeğiyle bit-bit özdeş olduğu için, bu kanıt yalnızca backtestin değil, canlı sistemin de bir özelliğidir.
Çok zaman dilimli mayın tarlası

Çok zaman dilimli (multi-TF) bir strateji, yavaş saatlere başvururken hızlı bir saat üzerinde karar verir. Motorumuz somut bir örnektir: üst-zaman-dilimi (HTF) trendi ve orta-zaman-dilimi (MTF) trendinin girişleri kapı gibi süzdüğü (gate), alt-zaman-dilimi (LTF) kesişiminin ise girişleri tetiklediği üçlü-ZD bir momentum kuralı. 1 dakikalık temel saatte, bir giriş yalnızca 1 dakikalık bir HMA/HMA3 kesişimi 1 saatlik trendin ve 15 dakikalık trendin yönüyle uyuştuğunda tetiklenir; bir çıkış ise üst-zaman-dilimi trendi pozisyona karşı yön değiştirdiğinde veya LTF geri kesiştiğinde tetiklenir.
Tehlike yapısaldır. Bu üst-zaman-dilimi değerlerinin her biri, her 1 dakikalık barda — saatte 60 kez — yanıtlanmak zorundadır ve her yanıt yalnızca canlı bir botun o anda bilebileceği şeyi kullanmalıdır. Bar başına yapılan bu 180 küsur okumadan (üç zaman dilimi, giriş ve çıkış trendleri, ayrım kapıları) bir tekinin bile henüz basılmamış bir bara dokunduğu an, elinizde look-ahead var demektir. Off-by-one (bir-kaydırma) hatası için yüzey alanı devasadır ve bir yürütme sızıntısının aksine, kendini fill'i kaydırarak ele vermez.
Oluşum halindeki bir barın henüz nihai kapanışı yoktur
İşte tam tuzak burada. Saatin 10:37 olduğunu ve az önce kapanan 1 dakikalık bar üzerinde karar verdiğinizi varsayın. Kuralınız 1 saatlik trendi istiyor. 10:00–11:00 aralığını kapsayan 1 saatlik mum hâlâ oluşum halinde — nihai kapanışı 10:59:59'a kadar bilinmeyecek. 10:37'de gerçekte ne biliyorsunuz? Yalnızca o mumun o ana kadarki cari kapanışını (running close) — yani 10:37 barının kapanışını. Mumun nihai kapanışı 23 dakika sonra, gelecekte gerçekleşecek.
Naif multi-TF backtest, tamamen masum görünen bir şey yapar: tüm 1 dakikalık seriyi baştan, tek seferde 1 saatliğe yeniden örnekler (resample) ve ardından her 1 dakikalık bar için "1 saatlik kapanışı" okur. Ama 10:00 ile 11:00 arasındaki her 1 dakikalık bar için okuduğu değer, nihai 10:00–11:00 kapanışıdır — gerçek zamanda, saat bitene kadar var olmayan bir sayı. Saat içindeki her karar, sessizce 59 dakika ileriye, geleceğe teslim edilir. Ve bu küçük bir sızıntı değil: üst-zaman-dilimi kapanışı, ticaretini yapmak üzere olduğunuz yakın-gelecekteki 1 dakikalık getirilerin neredeyse en güçlü olası tahmincisidir, dolayısıyla onu sızdırmak cevap anahtarını okumaya çok yakındır. Bu, taksonomideki gösterge-sızıntısı (indicator-leakage) kanalının büyütülmüş halidir: burada gözetleme tek bir bar değil, tam bir HTF periyoduna kadar sürebilir.
Önceki makaledeki tek-bar kaydırma testi bunu yakalamaz. Fill'i bir bar geciktirebilirsiniz ama yeniden örneklenmiş 1 saatlik seri yine de kirlenmiş olur — sızıntı işlemi ne zaman yaptığınızda değil, göstergenin nasıl inşa edildiğinde yaşar.
Canlı botun kuralı, birebir yeniden üretildi: closed-bar semantiği

Doğru kural, canlı botun zaten çalıştırdığı kuraldır. Kod tabanımızda bu, canlı tick simülatöründen birebir alınmış küçük bir sınıf olan RunningCandleBuffer'dır. Temel barları sabit periyotlu mumlara akıtır ve her temel barda HTF göstergesini çok spesifik bir diziden hesaplar:
all_closes = np.array(self.closes + [self.current_close], dtype=np.float64)
Bunu harfiyen okuyun. self.closes, zaten kapanmış mumların nihai kapanışlarıdır — bir mum yalnızca yeni bir periyot sınırı geçildiğinde eklenir ve saklanan değeri, periyot içindeki son temel barın kapanışıdır (candle_buffer.py:39–44). Oluşum halindeki mum tam olarak tek bir sayıyla katkıda bulunur: running (akan) kapanış olan self.current_close — en son temel barın kapanışı, base_close[i]. Bu, tanım gereği i barında bilinen bir niceliktir. Oluşum halindeki mumun nihai kapanışı asla kullanılmaz, çünkü henüz mevcut değildir.
Yani 10:37'deki HTF göstergesi şunu görür: [..., close(9:00 candle), close(10:37 so far)]. 10:38 bastığında, son slot close(10:38) olarak güncellenir. 11:00 geçildiğinde, close(10:59) yeni kapanan mumun nihai değeri olur ve yeni bir oluşum-halinde slot açılır. Saat içindeki hiçbir kararda nihai 10:00–11:00 kapanışına dokunulmaz. İşte closed-bar semantiği budur: kapalı mumlar tamamlanmış kapanışlarla katkıda bulunur, oluşum halindeki mum ise yalnızca kendi cari (running) kapanışıyla katkıda bulunur.
Hızlı motorumuz (engine_multitf.py), bunun vektörleştirilmiş, numba-derlemeli bir yeniden uygulamasıdır. Büyüyen bir listeye sahip bir Python döngüsü yerine, her temel bar i için kaç mumun tamamen kapandığını (n_closed[i]) önceden hesaplar ve HMA/HMA3 penceresini [closed candle closes…, base_close[i]] olarak düzenler — running kapanış son slota sabitlenmiş halde (engine_multitf.py:168–169). Bu, üç zaman dilimi boyunca yönlü ayrım kapılarıyla birlikte hız için açılmış aynı matematiktir. Sözleşme (contract) açıktır: i barındaki değer yalnızca base_close[0..i]'ye bağlıdır.
İşte iddia budur. Makalenin geri kalanı, bunu nasıl kanıtladığımızdır — çünkü bir docstring'deki iddianın hiçbir kıymeti yoktur.
Parite gerekli ama yeterli değil
Akan bir sınıfı açık döngülere dönüştüren vektörleştirilmiş bir numba motoru, tam olarak off-by-one hatalarının üreyeceği yerdir. Bu yüzden ilk kapı, gerçek bir dilim üzerinde — ETHUSDT 1 dakikalık, Ocak–Şubat 2024, 86,400 bar — kanonik referansa karşı bit-bit bir parite testidir.
İki bağımsız referansa karşı iki bağımsız şeyi kontrol ediyoruz:
- Göstergeler ve kesişimler — bar bar çalıştırılan
RunningCandleBuffer'a karşı. Her zaman dilimi için canlı sınıfı tüm 86,400 barın üzerinde tekrar oynatıyor ve kesişim olaylarını — barı, yönü, ayrımı — tam bir eşleşme için karşılaştırıyoruz, buna ek olarak HMA/HMA3 değerlerini kayan nokta toleransına kadar (referansnp.dotkullanıyor, motor ise açık döngüler, bu yüzden toplama sırası ~1e-15 mertebesinde farklılaşıyor). Kesişimler tam olarak örtüşüyor: HTF'de (1h, HMA uzunluğu 21) 408 kesişim, MTF'de (15m/14) 2,792, LTF'de (1m/50) 3,691. Tek bir kesişim olayı bile barda veya yönde farklılık göstermiyor. - İşlemler — referansın kendi kesişimleri tarafından yönlendirilen, ticaret kurallarının bağımsız, saf Python bir simülasyonuna karşı. Bu, canlı backtest'in döngüsünü — giriş trendleri girişleri kapı gibi süzer, bir çıkış-trend tersine dönüşü veya karşıt bir LTF kesişimi pozisyonu kapatır,
open[i+1]'de fill alınır, %0.09 gidiş-dönüş ücreti, son barda zorunlu kapanış — motorun numba makinelerinden hiçbiri olmadan yeniden üretir. Ardından işlem işlem karşılaştırıyoruz: giriş/çıkış barları, taraflar, giriş/çıkış fiyatları, PnL, çıkış nedenleri ve pozisyondaki toplam aktif süre.
Testteki ayrım eşikleri masum varsayılan değerler değildir. Zor bir köşeyi zorlamak üzere seçilmişlerdir — eşleşen giriş eşiğinden daha yüksek ayarlanmış bir MTF çıkış eşiği — bu da referansın bir tersine dönüş olarak ele aldığı "pozisyon açıkken çıkış trendinin ilk tanımı" dalını devreye sokar. Parite yalnızca kolay yolda değil, köşe durumlarında da geçerli olmak zorundadır.
Alan alan, işlemler özdeştir: ikili (dual) yapılandırma için 466 işlem, üçlü (triple) için 211 işlem, toplam PnL 1e-12'ye kadar eşleşiyor ve her işlemin alanları toleransa kadar eşit. Hiçbir kodu paylaşmayan iki uygulama — derlenmiş vektörleştirilmiş bir motor ve üçüncü bir uygulamanın kesişimleri üzerinde çalışan naif bir Python döngüsü — aynı işlemleri son ondalık basamağa kadar üretiyor.
Bu güçlü bir sonuç, ama look-ahead olmadığının bir kanıtı değildir. Parite, hızlı motorun referansı sadakatle yeniden ürettiğini söyler. Eğer referansın kendisi sızdırıyorsa — RunningCandleBuffer gözetliyorsa — parite bu sızıntıyı sadakatle yeniden üretir ve testi geçer. Uygulamalar arasındaki mutabakat size onların aynı olduğunu söyler, nedensel (causal) olduklarını değil. Nedensellik için farklı türde bir teste ihtiyacınız var: motora doğrudan, geçmişin geleceği görüp göremediğini soran bir test.
Geleceği-kaydıran probe: asıl kanıt

Look-ahead'in tanımı işlemseldir (operational), o yüzden onu işlemsel olarak test edin. Look-ahead şu anlama gelir: geçmiş bir karar, gelecekteki veriye bağımlıdır. Bunun karşıt-tersi, çalıştırabileceğiniz bir testtir: geleceği değiştirirseniz ve herhangi bir geçmiş karar kıpırdarsa, geçmiş geleceği okuyordu demektir. O halde geleceği değiştirin — acımasızca — ve geçmişi izleyin.
Serinin %60'ında bir kesim noktası j seçin (86,400 barın 51,840'ıncısı). j'den itibaren her barı bozun: tüm gelecekteki kapanışları ve açılışları 1.05 ile çarpın. Bozulmuş veri üzerinde her üç zaman dilimi için tüm sinyal yığınını yeniden hesaplayın. Ardından j'den kesinlikle önceki her şeyin, bozulmamış çalıştırmayla bit bit özdeş olduğunu doğrulayın:
j = int(n * 0.6) # bar 51,840
cl2 = cl.copy(); cl2[j:] *= 1.05 # shove the future up 5%
op2 = op.copy(); op2[j:] *= 1.05
base = [precompute_tf_signals(cl, ts, p, L) for (p, L) in tf_params]
pert = [precompute_tf_signals(cl2, ts, p, L) for (p, L) in tf_params]
for s0, s1 in zip(base, pert):
assert eq_nan(s0.hma[:j], s1.hma[:j]) # HMA identical, NaNs included
assert eq_nan(s0.hma3[:j], s1.hma3[:j])
assert np.array_equal(s0.cross[:j], s1.cross[:j]) # every cross event
assert np.array_equal(s0.sep[:j], s1.sep[:j]) # every separation
"Yakın" değil. "Tolerans dahilinde" değil. np.array_equal, NaN'ların NaN'larla eşleşmesi zorunlu olacak şekilde — 51,840 geçmiş bardaki her HMA değeri, her HMA3 değeri, her kesişim bayrağı ve her ayrım aynı float olmak zorunda. Ardından işlemler üzerinde aynı doğrulama: çıkışı j'den önce gerçekleşen her işlem, alan alan değişmemiş olmalı. Geleceğe yapılan %5'lik bir itiş, tek bir geçmiş HMA'yı on ikinci ondalık basamakta bile kıpırdatırsa, geçmiş bir karar geleceğe başvurmuş demektir ve probe başarısız olur.
Motorumuz bunu geçiyor — her üç zaman dilimi için de, hem ikili hem de üçlü işlem simülasyonları için de. 34,560 gelecek barı bozmak, 51,840 geçmiş barı ve aralarında kapanan her işlemi tam olarak oldukları gibi bırakıyor. Bu, uygulamalar arasındaki bir mutabakat değil; zaman içindeki bilgi sınırının tuttuğunun doğrudan bir gösterimi.
Başarısız olamayan bir test hiçbir şey kanıtlamaz
Yukarıdaki probe'u hiçbir şey kanıtlamadan geçmenin bir yolu var: eğer bozma işlemi bir no-op olsaydı — geleceği 1.05 ile çarpmak hiçbir yerde hiçbir şeyi değiştirmeseydi — o zaman "geçmiş değişmedi" önermesi önemsizce doğru ve tamamen bilgilendirici olmazdı. Başarısız olamayan bir testte yeşil bir onay, hiç test olmamasından daha kötüdür, çünkü sahte bir güven üretir. Bu yüzden probe, ona ısırık kazandıran iki doğrulama daha taşır.
Gelecek gerçekten değişmelidir. Bozmanın [j, n) aralığının bir yerinde kesişimleri gerçekten değiştirdiğini doğruluyoruz:
assert not np.array_equal(s0.cross[j:], s1.cross[j:]) # probe has teeth
Şimdi sonuç anlamlı: geleceği yeniden yazan aynı %5'lik itiş, geçmişi bit bit özdeş bıraktı. Bozma gerçek, ileriye doğru yayılıyor ve kesim noktasında tam olarak duruyor. Tek taraflı bir sızıntı — geçmişin geleceği okuması — j'nin ötesine geriye doğru sızardı; sızmıyor.
Sınır tam olarak geçerli bardadır — ne bir erken, ne bir geç. Daha ince bir başarısızlık, nedensel ama bayat (stale) bir motor olurdu: geçerli barın running kapanışını yok sayıp bir bar geç tepki verir (sızıntı yok ama canlı ticarette olmayacak bir gecikme vardır) ya da bir bar erken tepki verir (tek-bar sızıntısı). Bu yüzden tek bir barı j (1.02 ile) bozuyoruz ve aynı anda iki şeyi doğruluyoruz: [0, j) geçmişi dokunulmamış kalıyor ve hma[j] anında tepki veriyor.
cl3 = cl.copy(); cl3[j] *= 1.02 # nudge exactly one bar
s3 = precompute_tf_signals(cl3, ts, p_ltf, L_ltf)
assert eq_nan(s0.hma[:j], s3.hma[:j]) # nothing before j moves
assert s0.hma[j] != s3.hma[j] # bar j reacts on the same bar
Bu, sınırı tam olarak sabitler. Oluşum halindeki mumun running kapanışı, göstergeye j barında sıfır gecikmeyle ve sıfır öngörüyle girer: j barı kendi kapanışını anında görür, daha önceki hiçbir bar ise onu hiç görmez. Closed-bar semantiğinin tam olarak üzerinde durması gereken bıçak sırtı budur ve test, motorun orada durduğunu doğrular.
İşte tam kapı — bu motor ile uydurma bir backtest arasında duran 25 kontrolün tamamı:
| Grup | Her kontrolün doğruladığı şey | Sayı |
|---|---|---|
| Göstergeler ve kesişimler (×3 zaman dilimi) | RunningCandleBuffer'a karşı tam kesişim olayları; kesişimlerdeki ayrım; HMA/HMA3 değerleri (rtol 1e-9) |
9 |
| İşlemler (ikili + üçlü) | işlem sayısı; alan alan özdeş; toplam PnL 1e-12'ye kadar; pozisyondaki aktif süre | 8 |
| Geleceği-kaydıran probe (ikili + üçlü) | geçmiş sinyaller bit bit değişmemiş; probe'un ısırığı var (gelecek gerçekten değişti); j'den önceki işlemler değişmemiş; tek-bar bozması sınırlı (localized) |
8 |
| Toplam | 25 |
İlk iki grup, hızlı motorun canlı referansın ta kendisi olduğunu kanıtlar. Üçüncüsü ise referansın nedensel olduğunu kanıtlar. Üçüne de ihtiyacınız var: sızdıran hızlı bir motor, nedensel olup sızdıran bir motor ve nedensel olup geciken bir motor üç farklı başarısızlıktır ve kapı her birini eler.
Probe neden zaman-dilimi-bağımsız
Geleceği-kaydıran probe'un zarafeti, bir sızıntının nerede yaşayacağını bilmemesi ya da umursamamasıdır. Zaman dilimlerinden, yeniden örneklemeden veya mum sınırlarından hiç bahsetmez. Yalnızca şunu sorar: geleceği bozmak geçmişi kıpırdatıyor mu? Bu da onu, fill'i kaydırma testinin kaçırdığı multi-TF sızıntısı için tam olarak doğru araç yapar.
Naif tüm-seriyi-yeniden-örnekleme hatasını doğrudan ele alalım. Eğer 1 saatlik akış, tüm seri baştan yeniden örneklenerek inşa edilseydi, j-1000 barında (j'den sonra kapanan saatin iyice içinde) okunan "1 saatlik kapanış", nihai kapanışı j'de ve ötesindeki barlara bağlı olan bir mumun nihai kapanışı olurdu. Geleceği 1.05 ile çarpın ve bu nihai kapanış değişir — dolayısıyla j-1000'deki HTF göstergesi değişir, j-1000'deki kapı değişir ve geçmiş bir karar kıpırdar. Probe, HTF akışında anında, kesim noktasından bin adım önceki bir barda ışık yakardı.
Motorumuzun HTF akışı kıpırdamaz, çünkü j-1000'de oluşum halindeki mum yalnızca base_close[j-1000]'e — geçmiş bir kapanışa — katkıda bulunur ve mumun nihai kapanışına sınır geçilene kadar asla başvurulmaz. Probe, mekanizmaya karşı kördür ve yine de hatayı yakalar; bu tam olarak bir kanıttan beklediğiniz şeydir: davranışı kısıtlar (hiçbir geçmiş karar gelecekteki veriye bağımlı değildir), uygulamayı (yeniden örneklemeyi doğru indeksledik mi?) denetlemek yerine. Ticareti yapan davranıştır; uygulama ise ona uymasını umduğunuz şeydir.
Backtest ve canlı, tek bir gerçeği paylaşır

Bunun tipik bir backtest denetiminden daha önemli olmasının bir nedeni daha var. Motorun karşısında kanıtlandığı referans — RunningCandleBuffer — backtest'i iyi göstermek için yazılmış bir test fixture'ı değildir. Bu, üretimde çalışan tick simülatöründen birebir alınmış, canlı botun kendi mum mantığıdır. Probe'un doğruladığı closed-bar kuralı, canlı botun bar bar yürüttüğü kuraldır.
Yani parite kapısı çifte görev yapar. Hızlı motorun referansla eşleştiğini kanıtlar ve referans canlı çekirdeğin ta kendisi olduğu için, hızlı motorun canlıyla eşleştiğini de kanıtlar. Önceki makale, bir sızıntının backtest-canlı parite açığı için en temiz açıklama olduğu konusunda uyarmıştı — canlı bot, mekanik olarak gözetleyemeyeceğiniz tek yerdir, bu yüzden gözetleyen bir backtest, canlıya geçtiği anda ayrışır. Burada bu açık, yapı gereği kapanmıştır: backtest ve bot tek bir mum tamponunu, tek bir kesişim kuralını, "i barında bilinen" tanımının tekini paylaşır. Aramanın optimize ettiği sayı, botun gerçekten ticaretini yapabileceği sayıdır.
Look-ahead'i varsaymak yerine kanıtlamanın bütün amacı budur. Bir multi-TF araması binlerce yapılandırma çalıştırır; motor sızdırıyor olsaydı, arama sızıntıyı en agresif şekilde istismar eden yapılandırmayı bulur ve size uydurma bir kazanan sunardı — taksonominin saf gürültüden 15'lik bir Sharpe olarak ölçtüğü başarısızlık modu. Probe, kazananı sermayeye bağlamadan önce onun gerçek olduğuna güvenmenizi sağlayan şeydir.
Probe'un kanıtladığı ve kanıtlamadığı şeyler
Test konusundaki titizlik iki yönlü işler, bu yüzden kapsamı konusunda kesin olalım. Geleceği-kaydıran probe tek, spesifik bir özelliği kanıtlar: bu veri üzerinde, j barında veya öncesindeki hiçbir sinyal ya da işlem kararı, j'den sonraki hiçbir bara bağımlı değildir — zaman içindeki bilgi sınırı, gösterge, kesişim ve işlem yolu boyunca tutar. Bu, tam olarak ortadan kaldırmaya giriştiğimiz multi-TF sızıntısıdır ve bir kod incelemesinin ortaya koyamayacağı özelliktir.
Stratejinin bir edge'e (avantaja) sahip olduğunu kanıtlamaz. Kusursuz nedensel bir motor dürüstçe para kaybedebilir ve probe bu konuda sessizdir — olması gerektiği gibi; sızıntı olmadığını kanıtlamak ile gerçek bir edge bulmak ayrı işlerdir ve bunları birbirine karıştırmak, sızdıran backtestlerin nasıl devreye alındığıdır. Zamansal olmayan yanlılıkları kapsamaz: enstrümandaki hayatta kalma yanlılığı (survivorship), probe'u yalnızca motor iyi göründükten sonra çalıştırmaktan kaynaklanan seçim yanlılığı ya da fazla cömert bir ücret modeli. Ve tek başına, canlı fill'lerin backtest fill'leriyle eşleştiğini de kanıtlamaz — slippage ve gecikme (latency), probe'un göremeyeceği gerçek açıklardır, çünkü probe yürütme mekânı üzerinde değil karar yolu üzerinde çalışır. O açığı kapatan şey, motorun mum çekirdeğinin canlı botunkiyle bit-özdeş olduğu ayrı gerçeğidir.
Probe'un kendi tasarımıyla ilgili dürüst bir uyarı: tek bir j'de (serinin %60'ında) kesim yapıyor. Doğruladığı özellik j'ye göre tekdüzedir (uniform) — 51,840 barında özel bir şey yoktur — bu yüzden tek bir kesim, yapısal bir özelliğin adil bir testidir, ama paranoyak bir versiyon j'yi seri boyunca tarardı. İyi seçilmiş tek bir kesim artı tek-bar lokalizasyon kontrolünü yeterli buluyoruz, çünkü 34,560 barlık bir gelecek bozmasından saklanıp başka bir kesimde ortaya çıkan bir sızıntının çok garip bir hata olması gerekirdi. Buradaki amaç, kanıtınızın sınırlarını bilmektir, tek bir testin evrensel bir niceleyici olduğunu iddia etmek değil.
Çıkarımlar
- Multi-timeframe stratejiler, fill üzerinden değil, oluşum halindeki bar üzerinden sızdırır. Tamamlanmamış bir HTF periyodu içindeki bir karar, yalnızca o mumun running kapanışını (en son temel kapanış) kullanmalıdır, asla nihai kapanışını değil. Tüm-seriyi-yeniden-örnekleme backtestleri, periyot içi her kararı tam bir HTF periyodu kadar geleceğe teslim eder.
- Tek-bar kaydırma testi bu sızıntıya ulaşamaz. Yürütme look-ahead'ini yakalar; multi-TF sızıntısı ise göstergenin nasıl inşa edildiğinde yaşar. Farklı bir probe'a ihtiyacınız var.
- Canlı kuralı birebir yeniden üretin, ardından kanıtlayın. Botun
RunningCandleBufferclosed-bar semantiğini vektörleştirilmiş bir motor olarak yeniden inşa ettik ve onu 25 kontrolün arkasına kapattık: referansa karşı tam kesişimler (408 / 2,792 / 3,691), alan alan özdeş işlemler (466 ikili, 211 üçlü), 1e-12'ye kadar PnL. - Parite gerekli ama yeterli değildir. Bir referansla eşleşmek, aynı olduğunuzu kanıtlar, nedensel olduğunuzu değil. Sadakatle yeniden üretilmiş sızdıran bir referans, yine de sızdırır.
- Geleceği-kaydıran probe asıl kanıttır.
j'de veya sonrasındaki her barı bozun;j'den önceki her sinyal ve işlemin bit bit değişmediğini doğrulayın. Gelecek geçmişi kıpırdatabiliyorsa, elinizde look-ahead var demektir. - Probe'a ısırık kazandırın. Geleceğin gerçekten değiştiğini (bozmanın bir no-op olmadığını) ve tek-bar bir dürtmenin aynı barda tepki verdiğini (sızıntı yok, gecikme yok) doğrulayın. Başarısız olamayan bir test hiçbir şey kanıtlamaz.
- Backtest ve canlı tek bir çekirdeği paylaştığında, kanıt aktarılır. Motor, canlı botun mum mantığıyla bit-özdeş olduğu için, look-ahead'in yokluğu canlı sistemin de bir özelliğidir — ve backtest-canlı parite açığı yapı gereği kapanır.
Önceki makale, tek satırlık bir sızıntının nasıl 15'lik bir Sharpe uydurduğunu gösterdi. Bu makale ise tam tersi bir disiplini gösteriyor: "sızıntılar sizi nasıl kandırır" değil, belirli bir motorun sızdırmadığını gerçek veri üzerinde ve mekanik olarak nasıl kanıtlarsınız. Geleceği bozun. Geçmiş kıpırdamıyorsa, şu anda ticaret yapıyorsunuzdur.
Yazarlar
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.