Floating Point आपके पैसे खा जाएगा: Trading Systems में Precision
Rounding एक हल हो चुकी समस्या लगती है। आपके पास एक नंबर है, एक step size है, आप निकटतम वैध मान पर round करते हैं। एक utility function, बीस लाइनें, हमेशा के लिए काम तमाम।
लेकिन यह एक function नहीं है। ऐसे सिस्टम में जो कई exchanges पर orders भेजता है, precision अपने आप में एक domain है, और इससे पैदा होने वाले bugs में एक खतरनाक गुण होता है: ये error throw नहीं करते। ये एक ऐसा order बनाते हैं जो थोड़ा गलत है, या एक ऐसा order जो किसी ऐसी वजह से reject हो जाता है जो user को दिखती ही नहीं।
यहाँ बताया गया है कि यह काम असल में कैसा दिखता है, हमारे order path की तीन वास्तविक समस्याओं के ज़रिए।
Key Takeaways
- IEEE-754 step alignment को अविश्वसनीय बना देता है: 0.29 को 100 से गुणा करने पर 28.999999999999996 मिलता है, जो floor होकर पूरे एक step छोटा मान देता है।
- तीन exchanges तीन असंगत minimum-order नियम लागू करते हैं। एक USDT value चेक करता है, एक contract count चेक करता है, एक दोनों चेक करता है।
- इस क्षेत्र में हमारा सबसे बड़ा बग एक ऐसा नियम गढ़ने से आया जो exchange पर था ही नहीं, और फिर valid orders को उसके खिलाफ़ reject करने से।
- कभी ऐसी limit न बनाएँ जो API ने दी ही नहीं। अगर किसी venue पर notional minimum नहीं है, तो दूसरे fields से उसे compute करना fabrication है, defensiveness नहीं।
- Exchange metadata गायब हो तो error raise होना चाहिए, default पर fall back नहीं। गलत default चुपचाप गलत orders बनाता है।
1. Arithmetic की समस्या: rounding आपका नंबर हिला देता है
सबसे सरल केस से शुरू करते हैं, क्योंकि यही वह है जिसे लोग सुरक्षित मान लेते हैं।
एक exchange quantities को step size के multiples में स्वीकार करता है। आपके पास 0.29 है और step 0.01 है। यह value पहले से valid है, इसलिए इसे floor करना no-op होना चाहिए। स्टैंडर्ड implementation scale up करता है, floor करता है, और वापस scale down करता है:
0.29 / 0.01 = 28.999999999999996 ← IEEE-754, 29 नहीं
floor(...) = 28
28 × 0.01 = 0.28
एक value जो पहले से aligned थी, पूरे एक step नीचे खिसक गई, चुपचाप, छोटे order की दिशा में। कोई exception नहीं, कोई warning नहीं, और यह सिर्फ़ value और step के कुछ ख़ास combinations पर होता है, जो इसे पकड़ना और भी मुश्किल बनाता है।
इसका fix एक epsilon है जो scaling के दौरान लगाया जाता है, इतना बड़ा कि representation noise को सोख ले लेकिन इतना छोटा कि 0.289999 जैसी genuinely-below value अब भी सही तरह से floor हो। मैंने इसकी खोज के बारे में इस platform को बनाते हुए जिन चीज़ों को मैंने कम आँका में लिखा था; यहाँ जो बात मायने रखती है वह है कि यह किस category में आता है।
क्योंकि एक बार आप मान लें कि quantities पर arithmetic अविश्वसनीय है, तो आप उन सभी चीज़ों को देखने लगते हैं जो quantity को छूती हैं। असली हैरानियाँ वहीं हैं।
2. तीन exchanges, तीन असंगत minimums
हर venue एक minimum order enforce करता है। आप उम्मीद करेंगे कि नियम अपनी value में अलग हो, अपने shape में नहीं। लेकिन यह shape में अलग है:
| Exchange | नियम |
|---|---|
| Binance | qty × price >= minNotional (एक USDT value check) |
| Bybit | qty >= minOrderQty और notional >= minNotional (दो checks) |
| OKX perpetuals | qty >= minSz (contract count, notional check बिल्कुल नहीं) |
इस table को एक ही order के बारे में पूछे जा रहे तीन अलग सवालों के रूप में पढ़ें। Binance पूछता है कि इसकी value क्या है। OKX पूछता है कि कितने contracts हैं। Bybit दोनों पूछता है, और एक order अकेले किसी एक को पूरा कर सकता है और फिर भी reject हो सकता है।
OKX पर एक और पेचीदगी है: quantities contracts में हैं, coins में नहीं, इसलिए इनमें से कुछ भी meaningful होने से पहले contract size को दोनों के बीच convert करना पड़ता है। एक quantity जिसका मतलब एक venue पर "0.5 BTC" है, दूसरे पर "50 contracts" है।
हमने जो architectural निर्णय लिया: हर exchange अपने orders खुद validate करता है। Strategy engine में एक भी conditional नहीं है कि वह किस venue से बात कर रहा है। वह order एक adapter को सौंपता है और adapter अपने नियम लागू करता है। जब भी हमने इन नियमों को एक abstraction के पीछे एकजुट करने की कोशिश की, वह abstraction चौथा case आते ही झूठ बन गया।
3. सबसे महँगा बग: ऐसा नियम गढ़ना जो है ही नहीं
यह वह बग है जिससे मैं सबसे ज़्यादा चाहूँगा कि कोई और engineer बचे, क्योंकि गलती मेहनत जैसी दिखती है।
OKX perpetuals पर कोई notional minimum नहीं है। हमें यह पता नहीं था। दूसरे venues से अनुमान लगाते हुए, हमने एक compute किया: contract count minimum गुणा contract size गुणा price। यह reasonable लगा। इसने एक विश्वसनीय USDT आँकड़ा दिया। और यह काल्पनिक था।
यह कहाँ टूटा, यह ख़ास है और समझने लायक है। एक DCA strategy अपने safety orders entry price से नीचे रखती है। हमारा synthetic minimum एक price पर compute हुआ और फिर कम price वाले orders से compare किया गया, इसलिए वही quantity जो ladder के ऊपर pass हुई, नीचे जाकर fail हो गई।
नतीजा: ठीक 0.1 contracts के orders, जो exchange minimum है और accept हो जाते, हमारे अपने validator ने reject कर दिए इससे पहले कि वे सिस्टम से बाहर भी जाएँ। Exchange ने उन्हें कभी देखा ही नहीं। User की तरफ़ से strategy बस वह नहीं कर रही थी जो configure की गई थी।
यह सबक trading से कहीं आगे जाता है:
ऐसी constraint न गढ़ें जो API ने दी ही नहीं। अगर कोई venue contracts में minimum बताता है, तो minimum contracts में है। उससे value-based equivalent निकालना सावधानी नहीं है, यह एक नियम गढ़ना है और फिर उसे user के खिलाफ़ लागू करना है।
पीछे मुड़कर देखें तो संकेत यह था कि हमारी computed value price पर निर्भर थी जबकि exchange का असली नियम नहीं था। जब भी आपके constraint के version में कोई ऐसा input हो जो असली में नहीं है, तो आपने वह नहीं बनाया जो आप model करना चाहते थे।
4. गायब metadata error होना चाहिए, default नहीं
आखिरी हिस्सा एक policy है, बग नहीं, और यही वह चीज़ है जो पहले तीनों को चुपचाप दोहराने से रोकती है।
हर exchange हमें per-symbol metadata देता है: step size, tick size, minimum quantity, contract size। इनमें से कोई भी गायब हो सकता है, और लुभावना रास्ता fallback है। Step size गायब? 0.001 मान लो। Minimum quantity गायब? Zero मान लो और exchange को तय करने दो।
हम उल्टा करते हैं: एक required field जो गायब है, throw करता है, तुरंत, कोई भी order बनने से पहले। Downstream में किसी को भी अनुमान पर कुछ बनाने का मौका नहीं मिलता।
तर्क outcomes की विषमता है। एक thrown error एक ज़ोरदार failure है जो तुरंत दिखता है, किसी ऐसे व्यक्ति के सामने जो इसे ठीक कर सकता है। एक default एक चुपचाप गलत जवाब है जो गलत size पर order भेजता है और बाद में, पैसों में, अपना असली चेहरा दिखाता है। एक ऐसी error जिसे आप नज़रअंदाज़ नहीं कर सकते और एक ऐसा नंबर जिसे आप verify नहीं कर सकते, इनमें से error हर बार सस्ती पड़ती है।
इसका यह भी मतलब है कि metadata layer इमानदार है कि वह क्या जानती है। कोड जो required field पढ़ रहा है, उसे असली value मिलती है या exception। उसे कभी ऐसा placeholder नहीं मिलता जो असली data जैसा दिखता हो।
क्या याद रखें
- Step और tick alignment को arithmetic मानें जो आपकी value हिला सकता है, formatting नहीं।
- उन नियमों को एकजुट न करें जो सचमुच अलग हैं। Per-venue validation तब भी सही रहता है जब venues जुड़ते जाते हैं; असंगत नियमों पर बनाया गया shared abstraction नहीं रहता।
- कभी ऐसी limit derive न करें जो API ने बताई ही नहीं। अगर आपके constraint के version में कोई ऐसा input है जो असली में नहीं है, तो वह एक अलग constraint है।
- गायब metadata पर ज़ोर से fail करें। Defaults एक दिखने वाली error को एक अदृश्य, गलत कीमत वाले order में बदल देते हैं।
Precision का काम बेरौनक है और यह कभी किसी feature list में नहीं आता। लेकिन इनमें से हर बग ने एक ऐसा order बनाया जो गलत था या ऐसा order जो कभी बना ही नहीं, और ऐसे सिस्टम में जो किसी की ओर से trade करता है, ये उसी category की failure हैं जैसे उनका पैसा सीधे खो देना।
