मल्टी-Timeframe Backtests में No Look-Ahead साबित करना: भविष्य को Perturb करें, साबित करें कि अतीत उसे देख नहीं सकता
"Backtests Without Illusions" सीरीज़ का हिस्सा।
पिछला लेख एक diagnostic पर खत्म हुआ था: one-bar shift test। हर fill को एक bar देर से कर दो; अगर performance ध्वस्त हो जाए, तो तुम अतीत में ट्रेड कर रहे थे। यह टेस्ट single-timeframe strategy के लिए काफी है, जहाँ look-ahead execution में छिपता है — तुमने bar i पर फैसला किया और bar i पर ही fill किया।
मल्टी-timeframe strategies के लीक होने की एक दूसरी, ज़्यादा शांत जगह होती है, और shift test वहाँ तक नहीं पहुँचता। जब 1-minute bar पर लिया गया फैसला किसी 1-hour trend से पूछताछ करता है, तो लीक अब fill में नहीं रहता — वह indicator में होता है, जो एक ऐसे higher-timeframe bar से compute होता है जिसका बनना अभी पूरा ही नहीं हुआ है। यह लेख उसी लीक के बारे में है, और इस बारे में कि — कोड को घूरकर नहीं, बल्कि mechanically — कैसे साबित किया जाए कि तुम्हारे engine में यह लीक नहीं है।
अंत में जो दावा हम करते हैं वह मज़बूत है: हमारे multi-TF engine ने असली ETHUSDT 1m data के 86,400 bars पर 25 में से 25 parity और leakage checks पास किए, जिसमें एक shifted-future probe भी शामिल है जो भविष्य के हर bar को perturb करता है और पुष्टि करता है कि अतीत का एक भी फैसला हिलता नहीं। और चूँकि यह engine live bot के canonical core से bit-identical है, यह प्रमाण सिर्फ backtest की नहीं, बल्कि live सिस्टम की भी विशेषता है।
मल्टी-Timeframe माइनफ़ील्ड

एक मल्टी-timeframe (multi-TF) strategy एक तेज़ घड़ी पर फैसला लेती है जबकि धीमी घड़ियों से पूछताछ करती है। हमारा engine इसका एक ठोस उदाहरण है: एक triple-TF momentum rule जिसमें एक higher-timeframe (HTF) trend और एक mid-timeframe (MTF) trend entries को gate करते हैं, और एक lower-timeframe (LTF) cross उन्हें trigger करता है। 1m base clock पर, एक entry तभी फायर होती है जब 1m HMA/HMA3 cross, 1h trend और 15m trend की दिशा से सहमत हो; एक exit तब फायर होता है जब कोई higher-timeframe trend position के विरुद्ध पलट जाए, या LTF वापस cross कर जाए।
खतरा संरचनात्मक है। इनमें से हर higher-timeframe value का जवाब हर 1m bar पर देना होता है — यानी प्रति घंटा 60 बार — और हर जवाब में सिर्फ वही इस्तेमाल होना चाहिए जो एक live bot उस पल पता होता। जैसे ही उन करीब 180 per-bar reads (तीन timeframes, entry और exit trends, separation gates) में से एक भी किसी ऐसे bar को छूती है जो अभी प्रिंट ही नहीं हुआ, वहाँ look-ahead आ जाता है। off-by-one के लिए surface area बहुत बड़ा है, और एक execution leak के उलट, यह fill को शिफ्ट करके अपना पता नहीं देता।
बनती हुई candle का अभी कोई final close नहीं होता
यहाँ है वह ठीक-ठीक trap। मान लो अभी 10:37 है और तुम उस 1m bar पर फैसला ले रहे हो जो अभी-अभी close हुआ है। तुम्हारा rule 1h trend माँगता है। 10:00–11:00 को कवर करने वाली 1h candle अभी भी बन रही है — उसका final close 10:59:59 तक पता नहीं चलेगा। असल में 10:37 पर तुम्हें क्या पता है? सिर्फ उस candle का अब तक का running close, जो कि 10:37 bar का close है। candle का final close 23 मिनट आगे भविष्य में है।
naive multi-TF backtest एक ऐसा काम करता है जो बिल्कुल निर्दोष लगता है: यह पूरी 1m series को एक बार आगे resample कर लेता है 1h में, और फिर, हर 1m bar के लिए, "1h close" पढ़ता है। लेकिन 10:00 और 11:00 के बीच हर 1m bar के लिए जो value यह पढ़ता है वह final 10:00–11:00 close है — एक ऐसा नंबर जो, real time में, घंटा खत्म होने तक अस्तित्व में ही नहीं आता। घंटे के अंदर का हर फैसला चुपचाप भविष्य के 59 मिनटों को सौंप दिया जाता है। और यह कोई छोटा-मोटा लीक नहीं है: higher-timeframe close लगभग सबसे मज़बूत predictor है उन निकट-भविष्य के 1m returns का जिन्हें तुम अभी ट्रेड करने वाले हो, इसलिए इसे लीक करना लगभग answer key पढ़ने जैसा है। यह taxonomy वाला indicator-leakage चैनल है, बस बढ़ा हुआ रूप: यहाँ झाँकना एक bar भर का नहीं, बल्कि पूरे एक HTF period तक का है।
prequel वाला one-bar shift test इसे नहीं पकड़ता। तुम fill को एक bar देर से शिफ्ट कर सकते हो और फिर भी resampled 1h series दूषित रहेगी — लीक इस बात में रहता है कि indicator कैसे बनाया गया, न कि तुमने transact कब किया।
Live Bot का rule, बिल्कुल वैसा ही reproduce किया गया: Closed-Bar Semantics

सही rule वही है जो live bot पहले से चला रहा है। हमारे codebase में यह एक छोटी class है, RunningCandleBuffer, जो live tick simulator से जस-की-तस निकाली गई है। यह base bars को fixed-period candles में stream करती है और, हर base bar पर, HTF indicator को एक बहुत specific array से compute करती है:
all_closes = np.array(self.closes + [self.current_close], dtype=np.float64)
इसे शब्दशः पढ़ो। self.closes उन candles के final closes हैं जो पहले ही बंद हो चुकी हैं — एक candle तभी append होती है जब कोई नई period boundary पार होती है, और उसकी stored value उस period के अंदर आखिरी base bar का close होती है (candle_buffer.py:39–44)। बनती हुई candle ठीक एक नंबर का योगदान देती है, self.current_close, जो running close है — यानी सबसे हाल के base bar का close, base_close[i]। यह वह मात्रा है जो परिभाषा से bar i पर पता होती है। बनती हुई candle का final close कभी इस्तेमाल नहीं होता, क्योंकि वह अभी अस्तित्व में ही नहीं है।
तो 10:37 पर HTF indicator देखता है [..., close(9:00 candle), close(10:37 so far)]। जब 10:38 प्रिंट होता है, तो आखिरी slot अपडेट होकर close(10:38) बन जाता है। जब 11:00 पार होता है, तो close(10:59) नई बंद हुई candle की final value बन जाता है और एक ताज़ा forming slot खुल जाता है। घंटे के अंदर कोई भी फैसला final 10:00–11:00 close को कभी नहीं छूता। यही closed-bar semantics है: बंद हो चुकी candles finished closes देती हैं, बनती हुई candle सिर्फ अपना running close देती है।
हमारा fast engine (engine_multitf.py) इसी का एक vectorized, numba-compiled पुनर्कार्यान्वयन है। एक बढ़ती हुई list वाले Python loop की जगह, यह हर base bar i के लिए पहले से यह compute कर लेता है कि कितनी candles पूरी तरह बंद हो चुकी हैं (n_closed[i]), और HMA/HMA3 window को [closed candle closes…, base_close[i]] के रूप में सजाता है — running close को आख़िरी slot में टिकाकर (engine_multitf.py:168–169)। यह वही गणित है, बस speed के लिए तीन timeframes पर directional separation gates के साथ unroll किया हुआ। contract स्पष्ट है: bar i की value सिर्फ base_close[0..i] पर निर्भर करती है।
यही वह दावा है। बाकी लेख इस बात का है कि हम इसे कैसे साबित करते हैं, क्योंकि docstring में लिखा दावा किसी काम का नहीं होता।
Parity ज़रूरी है, लेकिन काफी नहीं
एक vectorized numba engine जो एक streaming class को explicit loops में unroll करता है, बिल्कुल वही जगह है जहाँ off-by-ones पनपते हैं। इसलिए पहला gate एक असली slice पर canonical reference के मुक़ाबले एक bit-for-bit parity test है — ETHUSDT 1m, जनवरी–फ़रवरी 2024, 86,400 bars।
हम दो independent references के मुक़ाबले दो independent चीज़ों को जाँचते हैं:
- Indicators और crosses बनाम
RunningCandleBufferजो bar-by-bar चलाया गया। हर timeframe के लिए हम सभी 86,400 bars पर live class को replay करते हैं और cross events की तुलना करते हैं — bar, दिशा, separation — एक exact match के लिए, साथ ही HMA/HMA3 values को floating-point tolerance तक (referencenp.dotइस्तेमाल करता है, engine explicit loops, इसलिए summation order ~1e-15 पर अलग पड़ता है)। crosses बिल्कुल मेल खाते हैं: HTF (1h, HMA length 21) पर 408 crosses, MTF (15m/14) पर 2,792, LTF (1m/50) पर 3,691। एक भी cross event bar या दिशा में अलग नहीं है। - Trades बनाम trading rules का एक independent pure-Python simulation, जो reference के अपने crosses से चलाया गया। यह live backtest के loop को दोहराता है — entry trends entries को gate करते हैं, exit-trend reversal या opposing LTF cross close करता है,
open[i+1]पर fills, 0.09% round-trip fee, आख़िरी bar पर force-close — engine की numba machinery के बिना। फिर हम trade-by-trade तुलना करते हैं: entry/exit bars, sides, entry/exit prices, PnL, exit reasons, और position में कुल active-time।
टेस्ट में separation thresholds कोई निर्दोष defaults नहीं हैं। इन्हें जान-बूझकर एक असहज कोने पर मारने के लिए चुना गया है — एक MTF exit threshold जो मिलते-जुलते entry threshold से ऊँचा सेट किया गया है — जो "position खुली होने के दौरान exit trend की पहली परिभाषा" वाली branch को force करता है, जिसे reference reversal मानता है। parity को कोनों वाले cases पर भी टिकना है, सिर्फ आसान रास्ते पर नहीं।
field-by-field, trades बिल्कुल एक जैसे हैं: dual configuration के लिए 466 trades, triple के लिए 211 trades, कुल PnL 1e-12 तक मेल खाता है और हर trade की fields tolerance तक बराबर हैं। दो implementations जिनमें एक भी code साझा नहीं है — एक compiled vectorized engine और एक तीसरे implementation के crosses पर चलने वाला naive Python loop — आख़िरी दशमलव तक एक ही trades produce करते हैं।
यह एक मज़बूत नतीजा है, और यह look-ahead न होने का प्रमाण नहीं है। parity यह बताती है कि fast engine reference को ईमानदारी से reproduce करता है। अगर reference खुद लीक करता — अगर RunningCandleBuffer झाँकता — तो parity उस लीक को भी ईमानदारी से reproduce करती और पास हो जाती। implementations के बीच सहमति यह बताती है कि वे एक जैसे हैं, यह नहीं कि वे causal हैं। causality के लिए एक अलग तरह का टेस्ट चाहिए, जो engine से सीधे पूछे कि क्या अतीत भविष्य को देख सकता है।
Shifted-Future Probe: असली प्रमाण

look-ahead की परिभाषा operational है, इसलिए इसे operationally टेस्ट करो। look-ahead का मतलब है एक अतीत का फैसला भविष्य के data पर निर्भर करता है। इसका contrapositive एक टेस्ट है जिसे तुम चला सकते हो: अगर तुम भविष्य को बदल दो और कोई अतीत का फैसला हिल जाए, तो अतीत भविष्य को पढ़ रहा था। तो भविष्य को बदलो — बेरहमी से — और अतीत को देखो।
series के 60% पर एक cut point j चुनो (86,400 में से bar 51,840)। j से आगे के हर bar को perturb करो: सभी future closes और opens को 1.05 से multiply करो। perturbed data पर तीनों timeframes के लिए पूरा signal stack फिर से compute करो। फिर assert करो कि j से सख़्ती से पहले का सब कुछ अनperturbed run के bitwise identical है:
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
"लगभग बराबर" नहीं। "tolerance के भीतर" नहीं। np.array_equal, जिसमें NaNs को NaNs से मेल खाना ज़रूरी है — 51,840 अतीत के bars पर हर HMA value, हर HMA3 value, हर cross flag, और हर separation एक ही float होनी चाहिए। फिर trades पर वही assertion: जिस भी trade का exit j से पहले पड़ता है वह field-for-field अपरिवर्तित होना चाहिए। अगर भविष्य को दिया गया 5% का धक्का बारहवें दशमलव में भी अतीत का एक भी HMA हिला दे, तो इसका मतलब है किसी अतीत के फैसले ने भविष्य से पूछताछ की, और probe फेल हो जाता है।
हमारा engine इसे पास करता है — तीनों timeframes के लिए, और dual व triple दोनों trade simulations के लिए। 34,560 future bars को perturb करने पर 51,840 past bars बचते हैं, और उनके बीच बंद हुआ हर trade बिल्कुल वैसा ही रहता है जैसा था। यह implementations के बीच सहमति नहीं है; यह इस बात का सीधा प्रदर्शन है कि समय में information boundary कायम है।
एक टेस्ट जो फेल ही नहीं हो सकता, वह कुछ साबित नहीं करता
ऊपर वाले probe को पास करने का एक तरीका है जो कुछ भी साबित नहीं करता: अगर perturbation एक no-op होता — अगर भविष्य को 1.05 से multiply करने से कहीं भी कुछ भी न बदलता — तो "अतीत अपरिवर्तित है" trivially सच होता और बिल्कुल भी सूचनाप्रद नहीं होता। ऐसे टेस्ट पर green check जो फेल ही नहीं हो सकता, टेस्ट न होने से भी बदतर है, क्योंकि यह झूठा भरोसा गढ़ता है। इसलिए probe दो और assertions साथ रखता है जो इसे असली पकड़ (teeth) देती हैं।
भविष्य को असल में बदलना चाहिए। हम assert करते हैं कि perturbation ने [j, n) में कहीं-न-कहीं crosses को बदला ही:
assert not np.array_equal(s0.cross[j:], s1.cross[j:]) # probe has teeth
अब नतीजा सार्थक है: वही 5% का धक्का जिसने भविष्य को फिर से लिखा, अतीत को bit-for-bit वैसा ही छोड़ गया। perturbation असली है, वह आगे की ओर propagate करता है, और cut पर आकर बिल्कुल रुक जाता है। one-sided leak — अतीत भविष्य पढ़ता है — j के आर-पार पीछे की ओर बह गया होता; यहाँ ऐसा नहीं होता।
boundary ठीक मौजूदा bar पर है — न एक पहले, न एक बाद में। एक ज़्यादा सूक्ष्म failure ऐसा engine हो सकता है जो causal तो हो लेकिन stale हो: यह मौजूदा bar के running close को नज़रअंदाज़ करके एक bar देर से react करे (कोई leak नहीं, लेकिन एक lag जो live trading में नहीं होगा), या ऐसा जो एक bar पहले react करे (एक one-bar leak)। तो हम सिर्फ एक अकेले bar j को perturb करते हैं (1.02 से) और दो चीज़ें एक साथ assert करते हैं: अतीत [0, j) अछूता रहे, और hma[j] तुरंत react करे।
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
यह boundary को बिल्कुल ठीक-ठीक pin करता है। बनती हुई candle का running close bar j पर indicator में zero delay और zero foresight के साथ दाख़िल होता है: bar j अपना खुद का close तुरंत देखता है, और उससे पहले का कोई भी bar उसे बिल्कुल नहीं देखता। यही वह ठीक-ठीक धार है जिस पर closed-bar semantics को टिकना चाहिए, और टेस्ट पुष्टि करता है कि engine वहीं टिका है।
यहाँ है पूरा gate — सारे 25 checks जो इस engine और एक गढ़े हुए (fabricated) backtest के बीच खड़े हैं:
| Group | हर check क्या assert करता है | Count |
|---|---|---|
| Indicators & crosses (×3 timeframes) | RunningCandleBuffer के मुक़ाबले cross events exact; crosses पर separation; HMA/HMA3 values (rtol 1e-9) |
9 |
| Trades (dual + triple) | trade count; field-by-field identical; कुल PnL 1e-12 तक; position में active-time | 8 |
| Shifted-future probe (dual + triple) | past signals bitwise अपरिवर्तित; probe में teeth (भविष्य असल में बदला); j से पहले के trades अपरिवर्तित; single-bar perturbation localized |
8 |
| Total | 25 |
पहले दो groups यह स्थापित करते हैं कि fast engine ही live reference है। तीसरा यह स्थापित करता है कि reference causal है। तुम्हें तीनों चाहिए: एक fast engine जो लीक करे, एक लीक करने वाला engine जो causal हो, और एक causal engine जो lag करे — ये तीन अलग-अलग failures हैं, और gate इनमें से हर एक को खारिज करता है।
Probe Timeframe-Agnostic क्यों है
shifted-future probe की खूबसूरती यह है कि इसे पता ही नहीं, और परवाह भी नहीं, कि कोई leak कहाँ रहती। यह कभी timeframes, resampling, या candle boundaries का ज़िक्र नहीं करता। यह सिर्फ पूछता है: क्या भविष्य को perturb करने से अतीत हिलता है? यही इसे multi-TF leak के लिए बिल्कुल सही औज़ार बनाता है, जिसे shift-the-fill test पकड़ ही नहीं पाता।
naive resample-the-whole-series bug को सीधे देखो। अगर 1h stream पूरी series को पहले से resample करके बनाई गई होती, तो bar j-1000 (उस घंटे के अंदर अच्छी तरह, जो j के बाद बंद होता है) पर पढ़ा गया "1h close" उस candle का final close होता जिसका final close j और उससे आगे के bars पर निर्भर करता। भविष्य को 1.05 से multiply करो और वह final close बदल जाता है — तो j-1000 पर HTF indicator बदल जाता है, j-1000 पर gate बदल जाता है, और अतीत का एक फैसला हिल जाता है। probe तुरंत जल उठता — HTF stream पर, cut से एक हज़ार steps पहले वाले bar पर।
हमारे engine का HTF stream नहीं हिलता, क्योंकि j-1000 पर बनती हुई candle सिर्फ base_close[j-1000] का योगदान देती है — जो अतीत का close है — और candle का final close तब तक बिल्कुल इस्तेमाल ही नहीं होता जब तक boundary पार न हो जाए। probe को mechanism की कोई ख़बर नहीं, फिर भी वह bug को पकड़ लेता है, और यही बात एक प्रमाण से चाहिए होती है: यह behavior को constrain करता है (कोई अतीत का फैसला भविष्य के data पर निर्भर नहीं करता) न कि implementation को audit करता है (क्या हमने resample को सही तरीके से index किया?)। behavior वह है जो ट्रेड करता है; implementation वह है जिसके मेल खाने की तुम उम्मीद करते हो।
Backtest और Live एक ही सच्चाई साझा करते हैं

एक और वजह है कि यह किसी सामान्य backtest audit से ज़्यादा मायने रखता है। जिस reference के मुक़ाबले engine को साबित किया गया है — RunningCandleBuffer — वह backtest को अच्छा दिखाने के लिए लिखी गई कोई test fixture नहीं है। यह live bot का अपना candle logic है, जो production में चलने वाले tick simulator से जस-की-तस उठाया गया है। जिस closed-bar rule को probe validate करता है, वही rule live bot bar-दर-bar execute करता है।
तो parity gate दोहरा काम करता है। यह साबित करता है कि fast engine reference से मेल खाता है, और चूँकि reference ही live core है, यह साबित करता है कि fast engine live से मेल खाता है। prequel ने चेतावनी दी थी कि एक backtest-live parity gap के लिए एक leak सबसे साफ़ स्पष्टीकरण है — live bot वह इकलौती जगह है जहाँ तुम mechanically झाँक ही नहीं सकते, इसलिए झाँकने वाला backtest live जाते ही अलग हो जाता है। यहाँ यह gap construction से ही बंद है: backtest और bot एक candle buffer, एक cross rule, "bar i पर क्या पता है" की एक ही परिभाषा साझा करते हैं। जिस नंबर को search optimize करता है, वही नंबर bot असल में ट्रेड कर सकता है।
यही पूरी बात है look-ahead न होने को मान लेने के बजाय साबित करने की। एक multi-TF search हज़ारों configurations चलाता है; अगर engine लीक करता, तो search वह configuration ढूँढ लेता जो leak का सबसे आक्रामक तरीके से फ़ायदा उठाए और तुम्हें एक गढ़ा हुआ (fabricated) winner थमा दे — वही failure mode जिसे taxonomy ने शुद्ध noise से 15 के Sharpe पर मापा था। probe वही है जो तुम्हें capital लगाने से पहले यह भरोसा करने देता है कि winner असली है।
Probe क्या साबित करता है और क्या नहीं
टेस्ट के बारे में सख़्ती दोनों तरफ़ लागू होती है, इसलिए इसके scope के बारे में सटीक रहना ज़रूरी है। shifted-future probe एक अकेली, specific property साबित करता है: इस data पर, bar j पर या उससे पहले लिया गया कोई भी signal या trade फैसला j के बाद के किसी bar पर निर्भर नहीं करता — समय में information boundary indicator, cross, और trade path से होते हुए कायम रहती है। यही वह exact multi-TF leak है जिसे खत्म करने के लिए हमने शुरुआत की थी, और यही वह property है जिसे कोई code review स्थापित नहीं कर सकती।
यह साबित नहीं करता कि strategy में कोई edge है। एक बिल्कुल causal engine ईमानदारी से पैसा गँवा सकता है, और probe इस बारे में चुप है — जैसा उसे होना भी चाहिए; leakage न होना साबित करना और असली edge ढूँढना दो अलग काम हैं, और इन्हें गड्डमड्ड करना ही वह तरीका है जिससे leaked backtests deploy हो जाते हैं। यह non-temporal biases को cover नहीं करता: instrument में survivorship, सिर्फ engine के अच्छा दिखने के बाद probe चलाने से आने वाला selection bias, या एक fee model जो ज़रूरत से ज़्यादा दयालु हो। और यह अपने-आप में यह साबित नहीं करता कि live fills backtest fills से मेल खाते हैं — slippage और latency असली gaps हैं जिन्हें probe नहीं देख सकता, क्योंकि यह decision path पर काम करता है, execution venue पर नहीं। उस gap को बंद करने वाली अलग बात यह तथ्य है कि engine का candle core live bot के साथ bit-identical है।
probe के अपने design के बारे में एक ईमानदार caveat: यह एक अकेले j (series का 60%) पर cut लगाता है। जो property यह verify करता है वह j में uniform है — bar 51,840 में कुछ भी ख़ास नहीं है — तो एक cut एक structural property का उचित टेस्ट है, लेकिन एक paranoid version पूरी series में j को sweep करता। हम एक अच्छी तरह चुने गए cut, साथ ही single-bar localization check, को पर्याप्त मानते हैं, क्योंकि एक ऐसी leak जो 34,560-bar future perturbation से तो छिप जाए लेकिन किसी और cut पर प्रकट हो जाए, एक बहुत अजीब bug होनी चाहिए होगी। मुद्दा अपने evidence की सीमाएँ जानना है, न कि यह दिखावा करना कि एक अकेला टेस्ट एक universal quantifier है।
निष्कर्ष
- मल्टी-timeframe strategies fill से नहीं, बनती हुई bar से लीक करती हैं। किसी अधूरे HTF period के अंदर लिया गया फैसला सिर्फ उस candle के running close (नवीनतम base close) का इस्तेमाल करना चाहिए, कभी उसके final close का नहीं। resample-the-whole-series backtests हर intra-period फैसले को एक पूरे HTF period भर के भविष्य तक सौंप देते हैं।
- one-bar shift test इस leak तक नहीं पहुँचता। यह execution look-ahead पकड़ता है; multi-TF leak इस बात में रहता है कि indicator कैसे बनाया गया। इसके लिए एक अलग probe चाहिए।
- live rule को बिल्कुल वैसा ही reproduce करो, फिर उसे साबित करो। हमने bot के
RunningCandleBufferवाले closed-bar semantics को एक vectorized engine के रूप में फिर बनाया और उसे 25 checks के पीछे gate किया: reference के मुक़ाबले exact crosses (408 / 2,792 / 3,691), field-by-field identical trades (466 dual, 211 triple), PnL 1e-12 तक। - Parity ज़रूरी है, लेकिन काफी नहीं। किसी reference से मेल खाना यह साबित करता है कि तुम एक जैसे हो, यह नहीं कि तुम causal हो। एक leaked reference जिसे ईमानदारी से reproduce किया गया हो, वह अब भी लीक करता है।
- shifted-future probe ही असली प्रमाण है।
jपर या उसके बाद के हर bar को perturb करो; assert करो किjसे पहले का हर signal और trade bitwise अपरिवर्तित है। अगर भविष्य अतीत को हिला सकता है, तो तुम्हारे पास look-ahead है। - probe को असली पकड़ (teeth) दो। assert करो कि भविष्य असल में बदला (perturbation कोई no-op नहीं है) और यह कि एक single-bar nudge उसी bar पर react करे (कोई leak नहीं, कोई lag नहीं)। एक टेस्ट जो फेल ही नहीं हो सकता, वह कुछ साबित नहीं करता।
- जब backtest और live एक core साझा करते हैं, तो प्रमाण transfer हो जाता है। चूँकि engine live bot के candle logic से bit-identical है, no-look-ahead live सिस्टम की भी एक property है — और backtest-live parity gap construction से ही बंद हो जाता है।
prequel ने दिखाया था कि कैसे एक one-line leak 15 का Sharpe गढ़ देता है। यह वाला उल्टा discipline दिखाता है: "leaks तुम्हें कैसे बेवकूफ़ बनाते हैं" नहीं, बल्कि यह कि असली data पर, mechanically, कैसे साबित किया जाए कि कोई specific engine लीक नहीं करता। भविष्य को perturb करो। अगर अतीत नहीं हिलता, तो तुम वर्तमान में ट्रेड कर रहे हो।
Authors
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.