फिल सिमुलेशन: क्लोज-प्राइस फैंटेसी से क्यू-अवेयर हकीकत तक की सीढ़ी
आपके बैकटेस्ट में दो मॉडल होते हैं: आपके अल्फा का एक मॉडल और आपके फिल्स का एक मॉडल। ज़्यादातर लोग अपनी 95% मेहनत पहले पर लगाते हैं और दूसरे को उस फ्रेमवर्क से जस-का-तस विरासत में ले लेते हैं जिसे वे इस्तेमाल करते हैं। यह उलटा है। एक औसत सिग्नल एक ईमानदार फिल मॉडल के साथ मिलकर एक औसत लेकिन वास्तविक PnL अनुमान देता है। एक शानदार सिग्नल fill_price = candle.close के साथ मिलकर एक ऐसा आँकड़ा देता है जो किसी भी चीज़ का अनुमान नहीं है — यह एक ऐसी धारणा का आउटपुट है जिसकी आपने कभी जाँच ही नहीं की।
लिक्विड इंस्ट्रूमेंट्स पर टेकर स्ट्रैटेजी के लिए, फिल मॉडल एक करेक्शन टर्म होता है। पर जहाँ भी रेस्टिंग limit orders शामिल हों — market making, पैसिव एंट्रीज़, post-only रिबेट हार्वेस्टिंग — वहाँ फिल मॉडल ही स्ट्रैटेजी है। आपका फिल होता है या नहीं, कब होता है, किस मात्रा में होता है, और उसके बाद हुई किस प्राइस मूव पर निर्भर होकर — यही PnL का चिह्न तय करता है, न कि सिर्फ़ उसका परिमाण।
Backtest-live parity में हमने बैकटेस्ट-लाइव विचलनों की पूरी वर्गीकरण-सूची बनाई थी और एग्जीक्यूशन विचलन को 5/5 गंभीरता दी थी — सबसे बुरा वर्ग। उस लेख ने फिल सिमुलेशन को तीन मोटे-मोटे एक्युरेसी स्तर दिए और आगे बढ़ गया। यह लेख उसका गहराई-वाला साथी है: पूरी सीढ़ी, पायदान-दर-पायदान, ऑर्डर लाइफसाइकिल को एक स्टेट मशीन के रूप में, ऐसे फिल-प्रोबेबिलिटी बाउंड्स के साथ जिन्हें आप सचमुच कंप्यूट कर सकते हैं, और एक मापा हुआ प्रयोग जो ठीक-ठीक दिखाता है कि सीढ़ी पर किस जगह एक "मुनाफ़े वाली" maker स्ट्रैटेजी दम तोड़ देती है।
सीढ़ी

हर पायदान को ज़्यादा डेटा और ज़्यादा कोड चाहिए, और हर पायदान एक विशिष्ट सिस्टेमैटिक बायस को हटाता है। पायदानों को इस आधार पर क्रमबद्ध किया गया है कि वे किस चीज़ को गलत करते हैं, न कि सिर्फ़ लागत के आधार पर।
पायदान 0: क्लोज-प्राइस फिल
fill_price = bar.close
ऑर्डर तुरंत, पूरा-का-पूरा, उसी बार के क्लोज पर फिल हो जाता है जिसने सिग्नल जनरेट किया। सिग्नल की गणना उसी क्लोज से हुई थी, इसलिए आप ऐसे डेटा पर ट्रेड कर रहे हैं जो प्राइस प्रिंट होते समय अस्तित्व में ही नहीं था: एग्जीक्यूशन का लबादा ओढ़े हुए look-ahead bias। टर्नओवर वाली कोई भी स्ट्रैटेजी यहाँ अच्छी दिखती है।
पायदान 1: नेक्स्ट-बार ओपन
fill_price = next_bar.open
बार्स पर टेकर लॉजिक के लिए न्यूनतम ईमानदार मॉडल। सिग्नल की गणना बार पर होती है, फिल बार की पहली ऑब्ज़र्वेबल प्राइस पर होता है। यह look-ahead को खत्म कर देता है पर फिर भी शून्य spread, शून्य इम्पैक्ट, ओपन प्रिंट पर अनंत लिक्विडिटी, और 100% फिल निश्चितता मान लेता है। limit orders के लिए यह टच-फिल लॉजिक में गिर जाता है (यह क्यों ज़हर है, उस पर नीचे और बात)।
पायदान 2: spread + फिक्स्ड slippage
half_spread = mid * spread_bps / 2e4
slip = mid * slippage_bps / 1e4
fill_price = mid + side * (half_spread + slip) # side: +1 buy, -1 sell
अब हर टेकर ट्रेड आधा spread प्लस एक कैलिब्रेटेड स्थिरांक चुकाता है। यह पहला पायदान है जहाँ एक हाई-टर्नओवर स्ट्रैटेजी बैकटेस्ट में दम तोड़ सकती है — और यही तो पूरी बात है। बची हुई त्रुटि: slippage स्थिर नहीं है। यह डिस्प्लेड depth के सापेक्ष ऑर्डर साइज़ के अनुपात में बढ़ता है और ठीक उसी वक्त फट पड़ता है जब आपकी स्ट्रैटेजी सबसे ज़्यादा ट्रेड करना चाहती है। एक फिक्स्ड 5 bps विभिन्न रेजीम्स का औसत है; आपकी स्ट्रैटेजी औसत रेजीम पर ट्रेड नहीं करती।
पायदान 3: market orders के लिए L2 depth-walk
order book snapshots के साथ आप slippage का अनुमान लगाना बंद कर देते हैं और उसकी गणना करने लगते हैं। साइज़ का एक market buy ask साइड को स्तर-दर-स्तर चलकर पार करता है; फिल प्राइस खपत किए गए स्तरों का वॉल्यूम-वेटेड औसत होती है:
def depth_walk(levels: list[tuple[float, float]], qty: float) -> tuple[float, float]:
"""levels: [(price, size), ...] sorted best-first. Returns (vwap, filled_qty)."""
remaining, cost = qty, 0.0
for price, size in levels:
take = min(size, remaining)
cost += take * price
remaining -= take
if remaining <= 0:
break
filled = qty - remaining
return (cost / filled if filled > 0 else float("nan"), filled)
दो करेक्शन इसे काफ़ी हद तक ज़्यादा ईमानदार बना देते हैं। पहला, latency: book को उस स्थिति में चलकर पार करें जिस स्थिति में वह आपके निर्णय टाइमस्टैम्प के बाद था, जहाँ आपकी मापी गई सिग्नल-से-एक्सचेंज latency है — जो book आपने देखी थी, वह वह book नहीं है जिस पर आप वार करते हैं। दूसरा, पार्शियल मार्केटेबल फिल्स: यदि आपकी लिमिट-थ्रू प्राइस के भीतर डिस्प्लेड depth से कम है, तो मॉडल को एक पार्शियल फिल लौटाना चाहिए और एक रेस्टिंग शेष छोड़ देना चाहिए, जो समस्या को नीचे दी गई स्टेट मशीन के हवाले कर देता है।
पायदान 3 पर बची हुई त्रुटि इम्पैक्ट और रीफिल है: आप book को एक स्थिर वस्तु के रूप में खपत करते हैं, पर असली books आंशिक रूप से फिर से भर जाती हैं (और असली counterparties प्रतिक्रिया करते हैं)। टॉप-ऑफ-book depth के कुछ प्रतिशत से कम क्लिप साइज़ के लिए यह त्रुटि छोटी है; बड़े क्लिप्स के लिए आपको ऊपर एक इम्पैक्ट मॉडल (Almgren-Chriss 2001) चढ़ाने की ज़रूरत होती है।
पायदान 4: limit orders के लिए प्रोबेबिलिस्टिक क्यू-पोज़िशन फिल्स
पायदान 0-3 इस सवाल का जवाब देते हैं कि "मेरा आक्रामक ऑर्डर किस प्राइस पर फिल होता है।" पायदान 4 उससे कठिन सवाल का जवाब देता है: मेरा पैसिव ऑर्डर फिल होता भी है या नहीं — और यह इकलौता पायदान है जो एक maker स्ट्रैटेजी की कीमत आँक सकता है। प्राइस पर एक रेस्टिंग limit order तब फिल होता है जब पर संचित ट्रेडेड वॉल्यूम उस queue वॉल्यूम से अधिक हो जाता है जो उसके आगे था। इसके लिए एक FIFO queue में अपनी पोज़िशन को ट्रैक करना पड़ता है जिसे आप सीधे ऑब्ज़र्व नहीं कर सकते।
क्यू-पोज़िशन एस्टिमेशन का पूरा तंत्र — शुरुआती पोज़िशन, ट्रेड्स बनाम कैंसल्स पर अपडेट नियम, अनऑब्ज़र्व्ड कैंसलेशन को आवंटित करने के लिए प्रोबेबिलिटी परिवार — वह प्रिमिटिव है जिसे हमने Queue inside the wall में बनाया था। मैं इसे यहाँ दोबारा नहीं गढ़ूँगा; सिम्युलेटर इसे उपभोग करता है। पायदान 4 जो जोड़ता है वह उस अनुमान के ऊपर बैठा फिल निर्णय नियम है, जिसे नीचे फिल-प्रोबेबिलिटी सेक्शन में कवर किया गया है।
यह पायदान क्यों मायने रखता है, इसका परिमाणीकरण साहित्य में हुआ है: Moallemi और Yuan (2016), "A Model for Queue Position Valuation in a Limit Order Book," दिखाते हैं कि लार्ज-टिक इंस्ट्रूमेंट्स के लिए फ्रंट-ऑफ-queue पोज़िशन बनाम बैक-ऑफ-queue का आर्थिक मूल्य आधे spread के बराबर होता है — यानी एक market-making स्ट्रैटेजी की पूरी सैद्धांतिक edge के समान परिमाण-कोटि का। एक फिल मॉडल जो queue position को अनदेखा करता है, वह किसी maker की PnL का गलत अनुमान नहीं लगाता; वह एक अलग ही स्ट्रैटेजी की PnL का अनुमान लगाता है।
एक पायदान 5 भी है — पूर्ण एजेंट-आधारित सिमुलेशन जहाँ मार्केट आपके ऑर्डर्स पर प्रतिक्रिया करता है (Huang, Lehalle और Rosenbaum 2015 के अर्थ में queue-reactive मॉडल; ABIDES जैसे multi-agent फ्रेमवर्क, Byrd et al. 2020)। ऐतिहासिक रीप्ले, चाहे queue-aware ही क्यों न हो, यह मान लेता है कि आपका ऑर्डर बाक़ी सबके व्यवहार के बारे में कुछ नहीं बदलता। यह धारणा रिटेल साइज़ पर ठीक है और जैसे-जैसे आपके quotes लेवल का एक दृश्यमान अंश बनने लगते हैं, वैसे-वैसे यह उत्तरोत्तर गलत होती जाती है। पायदान 5 यहाँ हमारे दायरे से बाहर है; बस इतना जानिए कि सीढ़ी 4 पर खत्म नहीं होती।
पार्शियल फिल्स को एक स्टेट मशीन के रूप में
पायदान 0-2 यह दिखावा कर सकते हैं कि एक ऑर्डर एक फंक्शन कॉल है: सबमिट करो, प्राइस पाओ, हो गया। पायदान 3 से ऊपर, एक ऑर्डर एक लाइफसाइकिल वाली प्रक्रिया है, और सिम्युलेटर को इसे एक स्टेट मशीन के रूप में मॉडल करना पड़ता है वरना यह चुपचाप उन्हीं मामलों को गलत ढंग से संभालेगा जो सबसे ज़्यादा मायने रखते हैं।

enum OrderState {
PendingNew, // sent, not yet acked (latency window)
Resting { remaining: f64, q_ahead: f64 }, // in book, queue position estimated
PartialFill { remaining: f64, q_ahead: f64 }, // some qty done, rest still queued
PendingAmend, // amend in flight
PendingCancel, // cancel in flight
Filled,
Canceled { filled_qty: f64 }, // may be partially filled at cancel time
Rejected,
}
ट्रांज़िशन अपने साथ अर्थशास्त्र ढोते हैं:
PendingNew→Resting: ऑर्डर queue में उस सब चीज़ के पीछे शामिल होता है जो ack टाइम पर मौजूद थी, न कि निर्णय टाइम पर। आपकी queue position को के अनुसार लेवल वॉल्यूम से सीड किया जाता है। जो सिम्युलेटर निर्णय टाइम पर सीड करते हैं वे queue प्राथमिकता का सिस्टेमैटिक रूप से अधिक अनुमान लगाते हैं — ठीक उतने वॉल्यूम से जितना आपकी latency विंडो के दौरान आया, जो बर्स्ट्स के दौरान अधिकांश वॉल्यूम होता है।Resting→PartialFill: आपके लेवल पर आपके आगे की queue से बड़ा एक ट्रेड आपको आंशिक रूप से फिल कर देता है। शेष अपनी (अब फ्रंट-ऑफ-queue) पोज़िशन बनाए रखता है। पार्शियल फिल्स शोर नहीं हैं — वे सूचना हैं: 1.0 में से 0.3 फिल होना और प्राइस को उछलकर दूर जाते देखना एक पूर्ण फिल से अलग PnL घटना है, और एक maker की इन्वेंट्री प्रक्रिया इन्हीं टुकड़ों से बनती है।Resting→PendingAmend→Resting: यही जाल है। लगभग हर क्रिप्टो वेन्यू पर, एक amend असल में एक कैंसल/रिप्लेस होता है — Binance काcancelReplaceडबल-एग्ज़ीक्यूशन के विरुद्ध एटॉमिक है पर queue के अंत में एक नया ऑर्डर ID लौटाता है। नेटिव मॉडिफ़ाई सिमैंटिक्स वाले वेन्यू भी (CME Globex) टाइम प्राथमिकता को केवल मात्रा घटाने के लिए बनाए रखते हैं; प्राइस बदलाव या मात्रा वृद्धि उसे गँवा देती है। तो सिम्युलेटर में: कोई भी प्राइस amendq_aheadको पूरे मौजूदा लेवल वॉल्यूम पर रीसेट कर देता है। एक quoting इंजन जो हर 500ms में री-पेग करता है, वह "quote बनाए नहीं रख रहा" — वह लगातार queue के अंत में दोबारा घुस रहा है, और उसका यथार्थवादी फिल प्रोफाइल लगभग शुद्ध adverse selection है। Moallemi-Yuan नतीजे के साथ मिलाएँ: री-quoting की एक कीमत होती है, और वह कीमत आपकी queue position है।PendingCancel→PartialFill→Canceled: कैंसल्स को भी latency लगती है। एक quote खींचने का निर्णय लेने और कैंसल के matching engine तक पहुँचने के बीच की विंडो में, आप अब भी फिल हो सकते हैं — और वे फिल्स सबसे बुरे फिल्स हैं जो आपको कभी मिलेंगे, क्योंकि आप quote इसीलिए खींच रहे थे कि मार्केट आपको कुचलने वाला था। cancel latency के बिना बना एक सिम्युलेटर आपके इतिहास से ठीक-ठीक सबसे विषैले फिल्स को मिटा देता है।
स्टेट मशीन वही चीज़ है जो सिम्युलेटर के हिसाब-किताब को ईमानदार भी बनाती है: फीस प्रति फिल घटना संचित होती है, इन्वेंट्री प्रति फिल घटना अपडेट होती है, और स्टेट-में-बिताया-समय के आँकड़े (ऑर्डर्स फिल होने बनाम कैंसल होने से पहले कितनी देर रेस्ट करते हैं) सीधे लाइव लॉग्स से तुलनीय बन जाते हैं — और अंत में मौजूद कैलिब्रेशन लूप इसी पर पलता है।
लिमिट-फिल प्रोबेबिलिटी: तीन मॉडल और एक ब्रैकेट
प्राइस पर रेस्टिंग एक ऑर्डर (मान लीजिए एक bid) दिया गया हो, तो सिम्युलेटर एक फिल की घोषणा कब करता है? बढ़ती ईमानदारी के क्रम में तीन निर्णय नियम:
1. टच-फिल (नैव)। फिल यदि प्राइस आपके लेवल को छू ले: । यह एक फर्स्ट-पैसेज-टाइम नियम है, और इसकी विफलता एक चौथाई सदी पहले मापी जा चुकी थी: Lo, MacKinlay और Zhang (2002), "Econometric Models of Limit-Order Executions" (Journal of Financial Economics 65), असली limit order डेटा पर survival मॉडल फिट करते हैं और निष्कर्ष निकालते हैं कि फर्स्ट-पैसेज टाइम्स से गढ़े गए काल्पनिक एग्ज़ीक्यूशन "वास्तविक limit-order एग्ज़ीक्यूशन के बहुत ही घटिया प्रॉक्सी हैं।" विफलता का तरीका संरचनात्मक है: जब प्राइस आपके लेवल को छूकर उछलता है, तो उस छुअन ने queue के अगले हिस्से को खपा दिया — वे ट्रेडर जिन्होंने आपसे पहले quote किया था। टच-फिल आपको उनके फिल्स से नवाज़ देता है। इससे भी बुरा, यह आपको ठीक-ठीक अच्छे फिल्स से नवाज़ता है (टच-एंड-बाउंस एक maker के लिए मुनाफ़े वाला परिदृश्य है), जबकि आपका असली फिल सेट टच-एंड-रन-थ्रू की ओर झुका होता है — adversely selected वाले।
2. ट्रेड-थ्रू (कंज़र्वेटिव बाउंड)। फिल केवल तब जब प्राइस सख्ती से आपके लेवल के आर-पार ट्रेड करे: , या tick डेटा पर, पर संचित ट्रेडेड वॉल्यूम पूरे लेवल से अधिक हो जाए। यदि प्राइस आर-पार ट्रेड कर गया, तो पर पूरी queue खप गई, इसलिए आप पोज़िशन चाहे जो भी रही हो, फिल हो गए। यह आपको कभी ऐसा फिल नहीं देता जो आपको न मिला होता। इसका बायस टच-फिल के बायस की दर्पण-छवि है: यह आपको हर उस फिल से वंचित कर देता है जहाँ queue पूरी पेनिट्रेशन के बिना आपकी पोज़िशन तक घट गई, और जो फिल्स यह देता भी है वे असमानुपातिक रूप से रन-थ्रू (adversely selected) वाले होते हैं। ट्रेड-थ्रू के तहत एक maker बैकटेस्ट एक स्ट्रेस टेस्ट है, अनुमान नहीं।
3. क्यू-डिप्लीशन एस्टिमेट। ट्रेड टेप से अपनी प्राइस पर संचित ट्रेडेड वॉल्यूम और L2 डेल्टास से अनुमानित कैंसल्ड वॉल्यूम को ट्रैक करें। आपकी अनुमानित आगे की queue:
जहाँ (ack-टाइम) ऑर्डर एंट्री पर लेवल वॉल्यूम है और उन कैंसलेशन का अंश है जिन्हें आपके आगे से आया हुआ माना गया — वह घुंडी जिसका सैद्धांतिक रूप ( परिवार) queue-position लेख में गढ़ा गया है। फिल तब शुरू होता है जब ; आपकी फिल्ड मात्रा उससे अधिक का ट्रेडेड वॉल्यूम है, जो स्वाभाविक रूप से पार्शियल फिल्स देता है:
सेट करना (सभी कैंसल्स आपके आगे) इस मॉडल का आशावादी किनारा देता है; निराशावादी किनारा। जब आपके पास केवल विरल L2 snapshots हों और लेवल पर कोई ट्रेड टेप न हो — 100ms-थ्रॉटल्ड क्रिप्टो फीड्स के साथ आम बात — तो आप एक मॉडल-आधारित प्रायर पर लौट सकते हैं: Cont, Stoikov और Talreja (2010), "A Stochastic Model for Order Book Dynamics" (Operations Research 58), हर प्राइस लेवल को एक birth-death queue के रूप में मॉडल करते हैं और Laplace ट्रांसफॉर्म के ज़रिए यह प्रोबेबिलिटी कंप्यूट करते हैं कि bid पर एक ऑर्डर mid के हिलने से पहले एग्ज़ीक्यूट हो जाता है, मौजूदा queue साइज़ों पर सशर्त। यह एक विश्लेषणात्मक फिल-प्रोबेबिलिटी ओरैकल है: टेप रीप्ले के मुकाबले भोंडा, टच-फिल से कहीं बेहतर, और इतना सस्ता कि एक हॉट बैकटेस्ट लूप के अंदर मूल्यांकित किया जा सके।
ब्रैकेट अनुशासन
तीनों नियम प्रतिद्वंद्वी नहीं हैं — वे एक क्रम हैं:
जो एक PnL ब्रैकेट प्रेरित करता है। हर maker बैकटेस्ट को तीन बार चलाएँ और अंतराल रिपोर्ट करें:
(ऊपरी असमानता अनुमानित है — टच-फिल स्ट्रैटेजी को सिर्फ़ फुलाता नहीं, गलत रैंकिंग भी दे सकता है, क्योंकि यह आपको काल्पनिक अच्छे फिल्स थमा देता है)। इससे जो निर्णय नियम निकलता है: एक maker स्ट्रैटेजी तभी डिप्लॉय करने लायक है जब वह ब्रैकेट के कंज़र्वेटिव किनारे पर टिकी रहे, और ब्रैकेट इतना संकरा हो कि पॉइंट एस्टिमेट का कोई मतलब बने। +3k ट्रेड-थ्रू दिखाने वाली स्ट्रैटेजी का ब्रैकेट 2.1k / +$0.4k दिखाने वाली स्ट्रैटेजी आपको कुछ वास्तविक बता रही है।
प्रयोग: एक maker स्ट्रैटेजी को सीढ़ी पर उतारना

एक जानबूझकर सादा maker लें — बेस्ट bid/ask पर सिमेट्रिक quotes, फिक्स्ड 0.05 BTC क्लिप्स, ±0.5 BTC पर इन्वेंट्री-कैप्ड और कैप पर टेकर फ्लैटनिंग के साथ। एक महीने का BTCUSDT perpetual डेटा: निचले पायदानों के लिए 1m बार्स, ऊँचे पायदानों के लिए 100ms L2 diffs प्लस ट्रेड टेप; maker फीस 1.0 bps, taker 4.0 bps। हर पायदान पर वही सिग्नल कोड (shared core, ताकि एकमात्र चर फिल मॉडल हो)। नीचे के आँकड़े हमारे एक प्रतिनिधि रन के हैं — आपके परिमाण वेन्यू, महीने और साइज़ के हिसाब से अलग होंगे; आकार नहीं होगा:
| पायदान | फिल मॉडल | Quote फिल रेट | फिल्स | महीने की PnL | निष्कर्ष |
|---|---|---|---|---|---|
| 0 | close-fill | 98% | 41,200 | +$14,800 | फैंटेसी |
| 1 | next-bar-open / touch on 1m bars | 89% | 37,400 | +$9,600 | latency के साथ फैंटेसी |
| 2 | touch + spread & fixed slippage on taker flattens | 89% | 37,400 | +$7,100 | लागतें मॉडल हुईं, फिल्स अब भी काल्पनिक |
| 3 | + L2 depth-walk on taker flattens | 89% | 37,400 | +$6,400 | एग्ज़िट ईमानदार, एंट्री अब भी काल्पनिक |
| 4a | trade-through (conservative) | 21% | 8,900 | -$3,900 | स्ट्रेस बाउंड |
| 4b | queue-depletion, calibrated | 37% | 15,600 | -$700 | सर्वोत्तम अनुमान |
| 4c | queue-depletion, (optimistic) | 44% | 18,700 | +$1,900 | ऊपरी किनारा |
| — | live shadow run, same month | 35% | 14,100 | -$1,150 | हकीकत |
तालिका को ऊपर से नीचे पढ़िए और देखिए कि स्ट्रैटेजी कहाँ दम तोड़ती है। यह पायदान 2 नहीं है — फीस और slippage 26% छाँट देते हैं और स्ट्रैटेजी फिर भी मज़बूती से मुनाफ़े वाली दिखती है। यह पायदान 3 और पायदान 4 के बीच दम तोड़ती है, और एक ऐसे कारण से दम तोड़ती है जिसे कोई लागत मॉडल पकड़ नहीं सकता: फिल सेलेक्शन। टच-फिल ने 37,400 फिल्स दिए जिनमें से अधिकांश टच-एंड-बाउंस थे — शुद्ध spread कैप्चर। क्यू-अवेयर मॉडल ने उन फिल्स में से 58% मिटा दिए, और जो फिल्स उसने मिटाए वे असमानुपातिक रूप से मुनाफ़े वाले थे: जब लेवल हल्के से टैप होता है, तो एक री-quoting रिटेल-latency maker के आगे की queue सब कुछ सोख लेती है। जो फिल्स पायदान 4 तक बचते हैं वे लेवल-क्लियरिंग स्वीप्स की ओर झुके होते हैं — वे फिल्स जहाँ प्राइस पहले से ही आपके आर-पार बढ़ रहा है। फिल रेट 2.4 गुना गिरा; PnL का चिह्न पलट गया। यह असममिति — अच्छे फिल्स गँवाना, बुरे रखना — यंत्रवत रूप से स्पष्ट किया गया adverse selection है, और यह पायदान 4 से नीचे हर पायदान पर अदृश्य है।
यह भी ध्यान दें कि ब्रैकेट ने क्या किया: [-1,900] शून्य को घेरता है, कैलिब्रेटेड एस्टिमेट -1,150 पर। सिम्युलेटर ने लाइव PnL को डॉलर-दर-डॉलर सटीक नहीं पकड़ा — उसने चिह्न, परिमाण, और फिल रेट को 2 पॉइंट के भीतर पकड़ा। एक फिल मॉडल इसी काम के लिए होता है। पायदान-1 बैकटेस्ट ने एक ऐसी स्ट्रैटेजी पर लाइव PnL को $10,750 से चूक दिया जिसकी पूरी मासिक ग्रॉस edge कुछ हज़ार डॉलर थी: फिल-मॉडल की त्रुटि अल्फा के आकार से लगभग 3 गुना थी। इसीलिए यह थीसिस: आपका फिल मॉडल आपके अल्फा से बड़ी धारणा है।
निचले पायदानों के लिए एक चेतावनी: यदि आपको बार डेटा पर ही रहना पड़े (पायदान 0-2), तो कम-से-कम इंट्राबार अस्पष्टता को adaptive drill-down से सुलझाएँ — जहाँ SL, TP, या quote लेवल बार रेंज के भीतर आते हों वहाँ 1m से 1s/100ms/trades तक ड्रिल करें। ड्रिल-डाउन सीक्वेंसिंग त्रुटियों (कौन-सा लेवल पहले हिट हुआ) को ठीक करता है पर queue त्रुटियों को ठीक नहीं कर सकता; यह इस लेख का डेटा-रेज़ोल्यूशन साथी है, पायदान 4 का विकल्प नहीं।
कैलिब्रेशन लूप: लाइव फिल्स के विरुद्ध बंद करना
एक पायदान-4 सिम्युलेटर में मुक्त पैरामीटर होते हैं — , latency , cancel-latency, लेवल-रीफिल धारणा। बिना कैलिब्रेशन के, यह बस एक अलग आकार का अनुमान भर है। वह लूप जो इसे एक उपकरण में बदलता है:
1. लाइव में सब कुछ लॉग करें। एक्सचेंज टाइमस्टैम्प के साथ हर ऑर्डर घटना: सबमिट, ack, हर पार्शियल फिल, amend acks, cancel acks। साथ ही सबमिट टाइम पर L2 स्थिति। यह वही लॉगिंग अनुशासन है जिसकी backtest-live parity अपने DivergenceMonitor के लिए माँग करती है — फिल-मॉडल कैलिब्रेशन उस मॉनिटर की सबसे गहरी परत है।
2. वही ऑर्डर्स सिम्युलेटर से दोबारा चलाएँ। रिकॉर्ड किया गया मार्केट डेटा और रिकॉर्ड किए गए ऑर्डर निर्देश (फिल्स नहीं) सिम्युलेटर में डालें। अब आपके पास जोड़ीदार परिणाम हैं: हर लाइव ऑर्डर के लिए, एक सिम्युलेटेड नियति।
3. वितरणों की तुलना करें, औसतों की नहीं, बकेट्स में। एक अकेला वैश्विक फिल-रेट मैच क्षतिपूरक त्रुटियों को छिपा सकता है (शांत रेजीम्स में बहुत आशावादी, बर्स्ट्स में बहुत निराशावादी — नेट होकर "कैलिब्रेटेड")। चालकों के अनुसार बकेट बनाएँ:
import numpy as np
from scipy.stats import ks_2samp
def calibration_report(pairs, bucket_key):
"""pairs: [{'bucket':…, 'live_filled':bool, 'sim_filled':bool,
'live_ttf':float|None, 'sim_ttf':float|None}, …]"""
out = {}
for b in sorted({p['bucket'] for p in pairs}):
grp = [p for p in pairs if p['bucket'] == b]
live_fr = np.mean([p['live_filled'] for p in grp])
sim_fr = np.mean([p['sim_filled'] for p in grp])
live_ttf = [p['live_ttf'] for p in grp if p['live_ttf'] is not None]
sim_ttf = [p['sim_ttf'] for p in grp if p['sim_ttf'] is not None]
ks = ks_2samp(live_ttf, sim_ttf) if len(live_ttf) > 20 and len(sim_ttf) > 20 else None
out[b] = {
'n': len(grp),
'fill_rate_live': live_fr,
'fill_rate_sim': sim_fr,
'fill_rate_gap': sim_fr - live_fr, # signed: + means sim optimistic
'ttf_ks_pvalue': ks.pvalue if ks else None,
}
return out
प्रति बकेट दो आँकड़े: साइन्ड फिल-रेट गैप (सिम्युलेटर माइनस लाइव) और फिल्ड ऑर्डर्स के बीच टाइम-टू-फिल वितरणों पर एक KS टेस्ट। टाइम-टू-फिल तुलना सबसे तीखी है — एक सिम्युलेटर फिल रेट से मेल खा सकता है जबकि सिस्टेमैटिक रूप से गलत समयों पर फिल कर रहा हो, जो हर डाउनस्ट्रीम इन्वेंट्री और adverse-selection आँकड़े को भ्रष्ट कर देता है। यह ठीक-ठीक Lo-MacKinlay-Zhang की survival-analysis फ्रेमिंग का सबक़ है: एग्ज़ीक्यूशन एक टाइम-टू-इवेंट समस्या है, इसलिए इसे वैसे ही वैलिडेट करें।
4. घुंडियाँ फिट करें, पहचान-योग्यता के क्रम में। पहले latency (ack टाइमस्टैम्प से सीधे मापी गई — फिट नहीं की गई)। फिर को queue-depth बकेट्स में फिल-रेट गैप न्यूनतम करके। फिर vol-रेजीम बकेट्स जाँचें: बर्स्ट बकेट्स में केंद्रित एक स्थायी आशावादी गैप का आमतौर पर मतलब है कि आपका सिम्युलेटर cancel-latency विषैले फिल्स या लेवल रीफिल का कम-मॉडलिंग कर रहा है, का नहीं।
5. ब्रैकेट दोबारा चलाएँ। कैलिब्रेशन के बाद, क्यू-डिप्लीशन एस्टिमेट को ब्रैकेट के अंदर लाइव के पास बैठना चाहिए, और — असली स्वीकृति परीक्षण — सिम्युलेटर के तहत स्ट्रैटेजी वेरिएंट्स की रैंकिंग को शैडो मोड में उनकी रैंकिंग से मेल खाना चाहिए। फिर पैरामीटर फ्रीज़ कर दें और एक अनुसूची पर पुनः कैलिब्रेट करें; फिल डायनैमिक्स वेन्यू के फीस टियर्स, tick-size बदलावों, और HFT आबादी के साथ बहते हैं, और मार्च में कैलिब्रेट किया गया जुलाई तक एक परिकल्पना है।
हमारे रनों से अभिसरण अपेक्षाएँ: एक अनकैलिब्रेटेड पायदान-4 सिम्युलेटर आमतौर पर लाइव फिल रेट के ±10-15 पॉइंट के भीतर आ बैठता है; एक कैलिब्रेशन पास के बाद, ±3-5 पॉइंट, जहाँ टाइम-टू-फिल KS p-वैल्यू अब समान रूप से अस्वीकृत नहीं करतीं। ऐतिहासिक रीप्ले के साथ आप इससे बेहतर नहीं कर पाएँगे — बचा हुआ शेष आपके प्रति मार्केट की प्रतिक्रिया है, जो पायदान 5 की समस्या है।
क्या साथ ले जाएँ
- अपने पायदान का नाम लें। हर बैकटेस्ट इस सीढ़ी पर बैठता है, चाहे पायदान आपने चुना हो या आपके फ्रेमवर्क ने आपके लिए चुना हो। यदि आप अपने फिल मॉडल के पायदान और उसके ज्ञात बायस का नाम नहीं ले सकते, तो आपके PnL आँकड़े में ऐसे error bars हैं जो आपने देखे नहीं।
- टेकर पायदान 3 पर रुक सकते हैं। depth-walk प्लस मापी गई latency रिटेल साइज़ पर आक्रामक एग्ज़ीक्यूशन की ईमानदार कीमत आँकती है। बचाई गई मेहनत डेटा गुणवत्ता पर खर्च करें।
- मेकर पायदान 4 से शुरू होते हैं। उससे नीचे, limit-order फिल लॉजिक अनुमानित नहीं है — यह विपरीत adverse-selection झुकाव वाले फिल्स का एक काल्पनिक सेट चुनता है। maker स्ट्रैटेजी के टच-फिल बैकटेस्ट उन स्ट्रैटेजी का सबसे भरोसेमंद इकलौता जनरेटर हैं जो प्रोडक्शन से टकराते ही दम तोड़ देती हैं।
- लाइफसाइकिल को मॉडल करें, फिल को नहीं। पार्शियल फिल्स, amend-रीसेट्स-queue, cancel latency — स्टेट मशीन वही जगह है जहाँ विषैले फिल्स रहते हैं, और विषैले फिल्स ही वह जगह हैं जहाँ maker PnL दम तोड़ने जाती है।
- ब्रैकेट रिपोर्ट करें। कंज़र्वेटिव और आशावादी बाउंड्स दो अतिरिक्त बैकटेस्ट रन की लागत लेते हैं और "मेरा बैकटेस्ट कहता है +3.9k, +$1.9k] में है" में बदल देते हैं — जो एक अलग, और बेहतर, निर्णय है।
- बकेट्स में लाइव फिल्स के विरुद्ध कैलिब्रेट करें। केवल कुल-मिलाकर वैलिडेट किया गया फिल मॉडल एक छिपी हुई क्षतिपूरक त्रुटियों वाला फिल मॉडल है। बकेटेड फिल-रेट गैप्स प्लस टाइम-टू-फिल KS टेस्ट, तिमाही-दर-तिमाही पुनः कैलिब्रेटेड।
सीढ़ी के पायदान अकादमिक श्रेणियाँ नहीं हैं — हर एक एक विशिष्ट झूठ है जो आपका बैकटेस्ट आपसे कहना बंद कर देता है। तब तक चढ़ते रहें जब तक झूठ आपकी edge से छोटे न हो जाएँ।
उपयोगी लिंक
- Moallemi, C., Yuan, K. — A Model for Queue Position Valuation in a Limit Order Book (2016)
- Cont, R., Stoikov, S., Talreja, R. — A Stochastic Model for Order Book Dynamics, Operations Research 58(3), 549-563 (2010)
- Lo, A., MacKinlay, C., Zhang, J. — Econometric Models of Limit-Order Executions, Journal of Financial Economics 65(1), 31-71 (2002)
- Huang, W., Lehalle, C.-A., Rosenbaum, M. — Simulating and Analyzing Order Book Data: The Queue-Reactive Model, JASA 110(509), 107-122 (2015)
- Almgren, R., Chriss, N. — Optimal Execution of Portfolio Transactions (2001)
- Byrd, D., Hybinette, M., Balch, T. — ABIDES: Towards High-Fidelity Multi-Agent Market Simulation (2020)
- Binance API — Cancel-Replace order semantics
- CME Globex — Order modification and time priority rules
Citation
@article{soloviov2026fillsimulation,
author = {Soloviov, Eugen},
title = {Fill simulation: the ladder from close-price fantasy to queue-aware reality},
year = {2026},
url = {https://marketmaker.cc/blog/fill-simulation-partial-fills-backtest},
description = {Five rungs of fill simulation fidelity — from close-price fills to probabilistic queue-position models. Partial fills as a state machine, limit-fill probability bounds as a PnL bracket, and a calibration loop against live fills.}
}
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.