फ्रेमवर्क टैक्स: जब आपकी बैकटेस्ट लाइब्रेरी एक भोले pandas लूप से भी धीमी होती है

हमने आठ बैकटेस्ट इंजनों को एक ही समान पैरामीटर स्वीप पर बेंचमार्क किया - 150k बार, 80 HMA-क्रॉस संयोजन, ट्रेड-गिनती की पैरिटी 2707 पर लॉक। दो सबसे लोकप्रिय इवेंट-ड्रिवन फ्रेमवर्क हाथ से लिखे pandas लूप से भी धीमे निकले, जबकि एक वेक्टराइज़्ड/कंपाइल्ड इंजन ने वही काम लगभग 13,000× तेज़ी से चलाया। उस प्रति-बार ओवरहेड का अध्ययन जिसे लोकप्रिय लाइब्रेरियाँ कभी अमॉर्टाइज़ करने के लिए बनी ही नहीं थीं।

"भ्रम रहित बैकटेस्ट" श्रृंखला का हिस्सा।

अधिकांश एल्गो-ट्रेडिंग परियोजनाओं की तह में एक आरामदेह मान्यता बैठी होती है: कि एक परिपक्व, खूब-सराही गई बैकटेस्टिंग लाइब्रेरी तेज़ होती है। इसमें सालों का योगदान है, एक असली इवेंट लूप है, एक ब्रोकर मॉडल है, एक कमीशन योजना है। निश्चय ही यह उस अटपटे pandas लूप को हरा देगी जिसे आप खुद लिखते। तो आप इसकी ओर हाथ बढ़ाते हैं, अपनी रणनीति जोड़ते हैं, और एक पैरामीटर स्वीप शुरू करते हैं — कुछ हज़ार कॉन्फ़िगरेशन, रात भर चलने वाला काम। आप सुबह लौटते हैं और यह अब भी चल रहा है।

हमने आठ बैकटेस्ट इंजनों को एक ही समान पैरामीटर स्वीप पर बेंचमार्क किया और कुछ ऐसा पाया जो यह बदल देना चाहिए कि आप खोज के लिए औज़ार कैसे चुनते हैं। दो सबसे लोकप्रिय ओपन-सोर्स इवेंट-ड्रिवन फ्रेमवर्क — backtrader और bt — ने स्वीप को उस भोले pandas लूप से भी धीमी गति से चलाया जिसे हमने फेंकने लायक बेसलाइन के रूप में लिखा था। थोड़ा धीमा नहीं। backtrader को pandas बेसलाइन का लगभग 2.5× समय लगा; bt को, लगभग 4.7×। इस बीच, उसी काम पर एक वेक्टराइज़्ड/कंपाइल्ड इंजन bt से लगभग 13,000× तेज़ चला।

यही फ्रेमवर्क टैक्स है। लोकप्रिय बैकटेस्टर एक ईमानदार रन के लिए बने हैं — एक रणनीति, एक डेटासेट, सावधान फ़िल, एक ऐसा ब्रोकर जो ब्रोकर की तरह बर्ताव करे। एक अंतिम सत्यापन या लाइव-पैरिटी जाँच के लिए आपको ठीक यही चाहिए। पर जिस चीज़ पर एल्गो शोध असल में अपना समय खर्च करता है — वही रणनीति को थोड़े-थोड़े बदले नॉब्स के साथ दस हज़ार बार चलाना — उसके लिए यह बिल्कुल गलत औज़ार है। यह लेख उस टैक्स को मापता है, तंत्र समझाता है, और एक निर्णय-नियम देता है कि "असली" बैकटेस्टिंग फ्रेमवर्क कब गलत चुनाव होता है।

यहाँ का हर आंकड़ा एक ही पुनरुत्पादनीय हार्नेस (benchmarks/bench_oss_engines.py, कमिट 250dbb5) से आता है, एक समान, पैरिटी-लॉक की गई वर्कलोड पर। जहाँ हमने किसी इंजन को खुद नहीं चलाया, वहाँ हम यह कहते हैं और उसे एक आंकड़ा गढ़ने के बजाय एक अलग ईमानदारी वाले खंड में रखते हैं।

एक रन बनाम दस हज़ार

एक ही रणनीति गणना तीन अलग-अलग रास्तों से नीचे बहती हुई: लाल-नारंगी में एक इवेंट-ड्रिवन ट्रैक जिसे हर बार-स्टेशन पर रुकना पड़ता है, एम्बर में एल्गो-नोड्स का एक वेट्स/रीबैलेंस वृक्ष, और एक अकेली वेक्टराइज़्ड पन्ना-सियान किरण जो पूरी शृंखला को एक साथ हल कर देती है — स्वीप की गति के लिए पैराडाइम ही नियति है

पैरामीटर खोज को परिभाषित करने वाला तथ्य यह है कि इंजन हज़ारों बार चलता है पर विश्लेषण एक ही बार होता है। बैकटेस्ट सेट करने के लिए आप जो भी नियत लागत चुकाते हैं — इवेंट लूप बनाना, एक ब्रोकर को इंस्टैंशिएट करना, प्रति बार एक ऑब्जेक्ट आवंटित करना — वह हर एक संयोजन पर चुकानी पड़ती है। एक ऐसी लागत जो एक रन में अदृश्य है ("6 सेकंड की किसे परवाह?") पूरे स्वीप में पूरा बिल बन जाती है ("6 सेकंड × 10,000 = 16.6 घंटे")।

बैकटेस्ट इंजन तीन पैराडाइमों में बँटते हैं, और पैराडाइम ही स्वीप प्रदर्शन की नियति है:

  1. इवेंट-ड्रिवन — इंजन बार-दर-बार चलता है, इवेंट उत्सर्जित करता है, आपके next()/onBars() कॉलबैक को बुलाता है, ऑर्डरों को एक ब्रोकर ऑब्जेक्ट के ज़रिए रूट करता है। यह backtrader, backtesting.py, PyAlgoTrade, zipline, और nautilus_trader की वास्तुकला है। यह दर्शाता है कि लाइव ट्रेडिंग असल में कैसे काम करती है, और ठीक इसी कारण इस पर यथार्थवाद के लिए भरोसा किया जाता है — और ठीक इसी कारण यह धीमा है: प्रति-बार Python ओवरहेड प्रति संयोजन 150,000 बार चुकाया जाता है।
  2. वेट्स/रीबैलेंस — bt यहाँ बैठता है। आप इसे एक लक्ष्य-भार मैट्रिक्स सौंपते हैं और यह आपके बताए तारीखों पर रीबैलेंस करता है। कोई प्रति-बार कॉलबैक नहीं, पर फिर भी Python में मूल्यांकित एक प्रति-इवेंट ऑब्जेक्ट ग्राफ़ (एल्गो का एक वृक्ष, एक लेनदेन बही)।
  3. वेक्टराइज़्ड / कंपाइल्ड — पूरी रणनीति ऐरे संक्रियाओं (vectorbt), एक JIT-कंपाइल्ड कर्नेल (numba), या नेटिव कोड (Rust, एक MLX GPU कर्नेल) के रूप में व्यक्त होती है। प्रति-बार Python बिल्कुल नहीं है। लूप, अगर है भी, तो मशीन की गति से चलता है।

इस लेख का बाकी हिस्सा उस वर्गीकरण का अनुभवजन्य परिणाम है। हमने एक वर्कलोड बनाई, हर इंजन को प्रमाणित रूप से वही काम करवाया, और समय मापा।

वर्कलोड: एक रणनीति, अस्सी नॉब्स, सबके लिए समान

एक बेंचमार्क तभी ईमानदार है जब हर इंजन वही काम कर रहा हो। हमारा जानबूझकर सादा है — एक ऐसी वर्कलोड जहाँ इंजनों के बीच एकमात्र भिन्नता खुद इंजन है।

  • डेटा। एक अकेली कृत्रिम ज्यामितीय ब्राउनी गति क्लोज़ शृंखला: 150,000 बार, seed=42, प्रति-बार अस्थिरता sigma=0.0008, x0=30000। नियतात्मक, ताकि कोई भी इसे बिट-दर-बिट पुनरुत्पादित कर सके। रचना से केवल-क्लोज़ — OHLC को हर चरण पर क्लोज़ पर सेट किया जाता है, क्योंकि रणनीति एक क्लोज़-क्रॉस है।
  • रणनीति। एक Hull Moving Average क्रॉस: लंबाई L का एक HMA एक तेज़ एक-तिहाई HMA वेरिएंट (HMA बनाम HMA3) के विरुद्ध। हमेशा बाज़ार में, हर क्रॉस पर लॉन्ग/शॉर्ट पलटना। यह एक असली, गैर-तुच्छ संकेतक है — दो नेस्टेड भारित मूविंग औसत साथ में एक वर्गमूल-विंडो स्मूदर — कोई खिलौना SMA नहीं, इसलिए प्रति-बार काम प्रतिनिधि है।
  • स्वीप। 6..200 तक फैली 80 HMA लंबाइयाँ। यही वे "दस हज़ार रन" हैं जिन्हें सीधे मापने लायक इतना छोटा किया गया: 80 स्वतंत्र संयोजन, हर एक 150k बार पर एक पूरा बैकटेस्ट।
  • लागत। एक राउंड-ट्रिप शुल्क 0.09%, उन इंजनों के लिए प्रति-साइड बाँटा गया जो साइड कमीशन मॉडल करते हैं। उसी-बार फ़िल close[i] पर — बार i का संकेत उसी बार के क्लोज़ पर निष्पादित होता है, वही परिपाटी जो हमारा प्रोडक्शन इंजन उपयोग करता है।

प्रति-संयोजन टाइमर ठीक दो चीज़ों को लपेटता है: numpy का HMA पूर्व-गणना और इंजन रन। जो सेटअप वाकई एक-बार का है (डेटा लोड करना, बार ऑब्जेक्ट बनाना) वह टाइमर के बाहर बैठता है। एक वॉर्मअप रन होता है, फिर best-of-N पुनरावृत्तियाँ, और — चूँकि इवेंट-ड्रिवन इंजन इतने धीमे हैं कि 80 पूरे संयोजनों में कई मिनट से लेकर एक घंटे से अधिक लग जाएँ — हम ग्रिड के एक समान नमूने का समय मापते हैं और रैखिक रूप से बहिर्वेशन करते हैं। संयोजन स्वतंत्र हैं, इसलिए रैखिक बहिर्वेशन प्रत्याशा में सटीक है; वही परिपाटी pandas बेसलाइन पर भी लागू होती है, इसलिए इससे किसी इंजन को फ़ायदा नहीं मिलता।

पैरिटी: यह साबित करना कि सब वही काम कर रहे हैं

यहाँ वह जाल है जिसमें एक भोली इंजन-तुलना फँसती है: एक "तेज़" इंजन शायद बस कम काम कर रहा हो। अगर backtrader 2,700 ट्रेड दर्ज करता है और आपका वेक्टराइज़्ड इंजन 40 दर्ज करता है, तो वेक्टराइज़्ड इंजन तेज़ नहीं है — वह गलत है, और तुलना निरर्थक है।

तो हम तुलना को एक ट्रेड-गिनती पैरिटी जाँच से लॉक करते हैं। L=104 पर, numpy संदर्भ ठीक 2,707 बंद ट्रेड पैदा करता है। हर इंजन को इसे ±1 की सहनशीलता के भीतर पुनरुत्पादित करना ही होगा, वरना रन एक work-parity FAILED एसर्शन के साथ रुक जाता है। यह सहनशीलता केवल इसलिए है क्योंकि इंजन बहीखाता परिपाटियों पर असहमत होते हैं — कि अंतिम खुली स्थिति ज़बरन बंद करके गिनी जाए या नहीं, कि आरंभिक प्रवेश एक "ट्रेड" है या नहीं — न कि खुद ट्रेडों पर:

इंजनL=104 पर रिपोर्ट किए ट्रेडपरिपाटी
numpy संदर्भ2707बंद राउंड-ट्रिप
backtesting.py2708+1: अंत में अंतिम स्थिति ज़बरन बंद
backtrader2707अंतिम खुली स्थिति नहीं गिनी गई
bt2708+1: आरंभिक प्रवेश एक लेनदेन के रूप में गिना गया
PyAlgoTrade2708+1: आरंभिक प्रवेश एक फ़िल के रूप में गिना गया

हर इंजन 2707 ± 1 पर उतरता है। गति के अंतर जो भी निकलें, वे किसी एक इंजन के चुपचाप काम छोड़ देने का परिणाम नहीं हैं। यही वह अनुशासन है जो हमें एक इवेंट-ड्रिवन फ्रेमवर्क और एक GPU कर्नेल को एक ही तालिका में रखने और उसे गंभीरता से कहने देता है।

परिणाम

यहाँ पूरी तालिका है, सबसे तेज़ से सबसे धीमे तक क्रमबद्ध। combos/s थ्रूपुट है; अंतिम स्तंभ बताता है कि पूरा 80-संयोजन स्वीप कितना समय लेता है। बेसलाइन पंक्ति M0 है — भोला pandas इंजन, बारों पर एक for लूप जिसमें स्केलर बहीखाता है, वह चीज़ जिसे आप एक दोपहर में लिखकर फेंक देते। उससे धीमी हर चीज़ बोल्ड में है।

इंजनcombos/sपैराडाइमपूरा 80-संयोजन स्वीप
MLX GPU कर्नेल779वेक्टराइज़्ड (Apple GPU)0.10 s
नेटिव Rust~350कंपाइल्ड0.23 s
mp + numba246कंपाइल्ड JIT + मल्टीप्रोसेस0.33 s
vectorbt56.9वेक्टराइज़्ड (numpy/numba)1.4 s
numba (सिंगल कोर)39.7कंपाइल्ड JIT2.0 s
backtesting.py1.42इवेंट-ड्रिवन56 s
PyAlgoTrade0.51इवेंट-ड्रिवन2.6 min
M0 — भोला pandas + लूप0.28स्केलर बेसलाइन4.8 min
backtrader0.11इवेंट-ड्रिवन12.7 min
bt0.06वेट्स / रीबैलेंस22.5 min

तालिका को ऊपर से नीचे पढ़िए और पैराडाइम खुद-ब-खुद छँट जाता है: शीर्ष पाँच सब वेक्टराइज़्ड या कंपाइल्ड हैं, निचले पाँच सब इवेंट-ड्रिवन या ऑब्जेक्ट-ग्राफ़ आधारित हैं — और भोला pandas लूप दो परिपक्व, लोकप्रिय फ्रेमवर्कों के ऊपर बैठा है। ऊपर से नीचे का फैलाव परिमाण के चार कोटियों का है। ठीक उसी 2,707-ट्रेड वर्कलोड पर, MLX कर्नेल स्वीप को एक-दसवें सेकंड में खत्म कर देता है; bt को साढ़े बाईस मिनट चाहिए। यह लगभग 13,000× का गुणक है।

तालिका के बीचोंबीच का घोटाला

तालिका के बीच का विरोधाभास: एक सादा धूसर-नीला भोला for-लूप धावक दो अलंकृत, फ़ीचर-भारी इवेंट-ड्रिवन फ्रेमवर्क मशीनों से आगे फ़िनिश लाइन पार करता हुआ, जो लाल-नारंगी में दमकती हुई अब भी पीछे मशक्कत कर रही हैं, ब्रोकर मॉडलों और एनालाइज़र स्टैकों के बोझ से दबी हुई — विनम्र बेसलाइन एक समान 2,707-ट्रेड वर्कलोड पर परिपक्व फ्रेमवर्कों को हराती हुई

आँख खींचने वाले आंकड़े छोरों पर हैं, पर शिक्षाप्रद परिणाम बीच में है: backtrader (0.11 combos/s) और bt (0.06 combos/s) दोनों भोले pandas बेसलाइन (0.28 combos/s) से धीमे हैं।

इसे भीतर तक बैठने देना चाहिए। M0 कोई चतुर इंजन नहीं है। यह एक Python for लूप है जो एक DataFrame में इंडेक्स करता है, एक स्थिति और नकदी को सादे स्केलरों में ट्रैक करता है, और ट्रेडों को एक सूची में जोड़ता है — जानबूझकर बिना-अनुकूलित किया गया "नियंत्रण" जिसे हमने इसलिए शामिल किया ताकि हराने के लिए कुछ स्पष्ट रूप से बुरा हो। pandas की प्रति-पंक्ति पहुँच बदनाम रूप से धीमी है, और हमने उसी का सहारा लिया। और फिर भी पारिस्थितिकी की दो सबसे अनुशंसित बैकटेस्ट लाइब्रेरियाँ इससे हारती हैं: backtrader 2.5× से, bt 4.7× से।

वह बारीकी जो इसे ईमानदार रखती है: हर इवेंट-ड्रिवन इंजन pandas से धीमा नहीं है। backtesting.py (1.42 combos/s) बेसलाइन को 5× से हराता है, क्योंकि यह एक दुबला, numpy-समर्थित इवेंट लूप है जो प्रति-बार ऑब्जेक्ट निर्माण को न्यूनतम रखता है। PyAlgoTrade (0.51) भी बेसलाइन से थोड़ा आगे निकल जाता है। तो "इवेंट-ड्रिवन" अपने आप मृत्युदंड नहीं है — पर प्रति-बार मशीनरी जितनी भारी, उतना ही बुरा, और backtrader और bt यहाँ सबसे भारी मशीनरी ढो रहे हैं। पैराडाइम छत तय करता है; कार्यान्वयन तय करता है कि उस छत के नीचे आप कहाँ उतरते हैं।

बात यह नहीं है कि ये बुरी लाइब्रेरियाँ हैं। backtrader का ब्रोकर मॉडल और bt का एल्गो-वृक्ष डिज़ाइन आपको शुद्धता और अभिव्यंजना खरीदकर देने के लिए मौजूद हैं — यथार्थवादी ऑर्डर हैंडलिंग, पोर्टफ़ोलियो रीबैलेंसिंग, एनालाइज़र। उन फ़ीचरों की एक रनटाइम लागत है, और वह लागत तब अदृश्य होती है जब आप एक बार चलाते हैं। पूरे स्वीप में वही पूरी कहानी है।

इवेंट-ड्रिवन इंजन टैक्स क्यों चुकाते हैं

फ्रेमवर्क टैक्स कहाँ वसूला जाता है, प्रति बार: एक अकेली बार-पुनरावृत्ति अपने महँगे घटकों में फूटती हुई — एक मूर्त बार ऑब्जेक्ट, एक कॉलबैक फ़्रेम, एक ऑर्डर रूट करता ब्रोकर, जोड़ी जाती एक बही — हर एक एक लाल-नारंगी में दमकती टोल-बूथ जो एक सिक्का छीलती है, एक विशाल दूर तक सिमटते गलियारे में एक करोड़ बीस लाख समान बार-पुनरावृत्तियों तक दोहराई गई

तंत्र रहस्यमय नहीं है। एक इवेंट-ड्रिवन बैकटेस्ट, प्रति बार, कुछ ऐसा करता है:

  1. घड़ी आगे बढ़ाता है, डेटा फ़ीड से अगली बार काटता है, और उसे एक ऑब्जेक्ट (एक Bar, एक Line, एक dict) के रूप में मूर्त करता है।
  2. उपयोगकर्ता कोड (next(), onBars()) में एक कॉलबैक दागता है, जो अपने खुद के फ़्रेम वाला एक Python फ़ंक्शन कॉल है।
  3. कॉलबैक के भीतर, ब्रोकर/स्थिति की अवस्था को क्वेरी करता है, फिर से मेथड कॉलों और एट्रिब्यूट लुकअप के ज़रिए।
  4. अगर कोई ऑर्डर बनता है, तो उसे ब्रोकर के ज़रिए रूट करता है: उसे मान्य करता है, मार्जिन/नकदी जाँचता है, एक फ़िल शेड्यूल करता है, एक पोर्टफ़ोलियो ऑब्जेक्ट बदलता है, एक लेनदेन बही में जोड़ता है।
  5. एनालाइज़र, ऑब्ज़र्वर, और फ्रेमवर्क द्वारा रखा गया कोई भी बहीखाता अपडेट करता है।

अब 150,000 बारों से गुणा कीजिए, फिर 80 संयोजनों से: प्रति स्वीप एक करोड़ बीस लाख बार-पुनरावृत्तियाँ, हर एक Python-स्तर के ऑब्जेक्ट आवंटनों और गतिशील डिस्पैचों की एक मुट्ठी। Python का प्रति-संक्रिया ओवरहेड — एक एट्रिब्यूट लुकअप या एक छोटे आवंटन के लिए दसियों से सैकड़ों नैनोसेकंड — एक बार तो तुच्छ है और एक करोड़ बीस लाख बार विनाशकारी। bt का मामला उसी बीमारी का एक रूप है: भले यह हर बार के बजाय केवल ट्रेड-तारीखों पर रीबैलेंस करता है, हर रीबैलेंस एल्गो ऑब्जेक्टों का एक वृक्ष मूल्यांकित करता है और एक pandas-समर्थित पोर्टफ़ोलियो बही को छूता है, और उनमें से 2,707 प्रति संयोजन हैं, गुणा 80।

भोला pandas लूप backtrader और bt को एक भोंडे कारण से हराता है: यह प्रति बार कम काम करता है। यह ब्रोकर, इवेंट ऑब्जेक्ट, एनालाइज़र स्टैक, ऑर्डर-रूटिंग स्टेट मशीन को छोड़ देता है। यह pandas का कुरूप प्रति-पंक्ति टैक्स चुकाता है, पर वह एक कुरूप टैक्स भी फ्रेमवर्क के सुव्यवस्थित, फ़ीचर-पूर्ण, प्रति-इवेंट-एक-ऑब्जेक्ट वाले टैक्स से सस्ता है। जब आप एक बैकटेस्ट को "स्थिति × अगला रिटर्न − शुल्क" तक छील देते हैं, तो एक फ्रेमवर्क प्रति बार जो करता है उसका अधिकांश ओवरहेड है जिसे आप खोज के दौरान इस्तेमाल ही नहीं कर रहे।

और यही जाल है: वह ओवरहेड वही कारण है जिसके लिए आपने फ्रेमवर्क चुना। आपको यथार्थवादी ब्रोकर चाहिए था। आपको एनालाइज़र चाहिए थे। अंतिम सत्यापन के दौरान, आपको वह सब चाहिए। पर एक 10,000-संयोजन खोज के दौरान, जहाँ आप दूसरे छोर से बस एक स्केलर उद्देश्य पढ़ते हैं, आप ट्रैक के चक्कर लगाने के लिए एक लिमोज़ीन का किराया चुका रहे हैं।

तालिका का दूसरा छोर: वेक्टराइज़्ड और कंपाइल्ड

तालिका का शीर्ष वही है जो तब होता है जब आप प्रति-बार Python को पूरी तरह मिटा देते हैं।

  • vectorbt (56.9 combos/s) पूरी रणनीति को numpy/numba ऐरे संक्रियाओं के रूप में व्यक्त करता है। Python में कोई बार लूप नहीं है — संकेत, स्थिति, PnL सब ऐरे-स्तर पर हैं। यह स्वीप को 1.4 सेकंड में चलाता है बनाम bt के 22.5 मिनट: समान काम पर लगभग 950× तेज़। (हम vectorbt के डिज़ाइन को vectorbt अवलोकन और व्यापक pandas-बनाम-polars तुलना में अधिक गहराई से देखते हैं।)
  • numba (39.7) वही pandas लूप है, आकार में अपरिवर्तित, JIT से नेटिव कोड में कंपाइल किया गया। M0 जैसा ही एल्गोरिथम, पर @njit 0.28 combos/s को 39.7 में बदल देता है — एक डेकोरेटर से ~140× गति-वृद्धि, क्योंकि जो इंटरप्रेटर ओवरहेड स्केलर लूप पर हावी था वह बस उड़ जाता है।
  • mp + numba (246) कंपाइल्ड कर्नेल को CPU कोरों पर चलाता है। संयोजन शर्मनाक हद तक समांतर हैं — हर एक स्वतंत्र है — इसलिए मल्टीप्रोसेसिंग JIT के ऊपर लगभग रैखिक रूप से स्केल करती है।
  • नेटिव Rust (~350) Python गोंद का आखिरी अंश हटा देता है: पूरा स्वीप नेटिव है।
  • MLX GPU (779) स्वीप को एक Apple-सिलिकॉन GPU कर्नेल पर मैप करता है। 80 संयोजन अंकगणित की 80 समांतर लेन बन जाते हैं; एंटर कुंजी छोड़ने से पहले ही स्वीप खत्म हो जाता है।

दो चीज़ें सटीकता से नाम लेने योग्य हैं। पहला, numba साबित करता है कि पैराडाइम भाषा से अधिक मायने रखता है। M0 और numba एक ही एल्गोरिथम चलाते हैं — अंतर बस इतना है कि एक प्रति-बार व्याख्यायित Python है और दूसरा कंपाइल्ड। यह एक नियंत्रित A/B में पूरा फ्रेमवर्क टैक्स है: भीतरी लूप से इंटरप्रेटर मिटाने के लिए ~140×। दूसरा, numba (39.7) से mp+numba (246) से MLX (779) तक की छलाँग अब इंजन के बारे में है ही नहीं — यह ऑर्केस्ट्रेशन और हार्डवेयर के बारे में है। एक बार प्रति-बार टैक्स चला गया, तो गति इस बात का सवाल बन जाती है कि आप कितने संयोजन समांतर में और किस सिलिकॉन पर चलाते हैं। हम उस पूरे क्रम को बैकटेस्ट इंजन गति सीढ़ी में चलते हैं, और वह कारण कि आखिरी मील प्रोसेस/सीरियलाइज़ेशन लागत से हावी क्यों है, IPC-टैक्स लेख में।

हमने क्या नहीं मापा (और हम आपको क्यों बता रहे हैं)

एक बेंचमार्क की विश्वसनीयता उसमें बसती है जिसे वह गढ़ने से इनकार करता है। हमने आठ इंजनों को पैरिटी के तहत छोर-से-छोर चलाया। कई जाने-माने फ्रेमवर्कों पर हमने कोई आंकड़ा नहीं रखा, और हम उन्हें ईमानदारी से सूचीबद्ध करना पसंद करेंगे बजाय एक ऐसा आंकड़ा बहिर्वेशित करने के जिसे हमने मापा नहीं:

  • zipline / zipline-reloaded — इवेंट-ड्रिवन, Quantopian वंश। भारी सेटअप (एक पूरा ट्रेडिंग कैलेंडर और डेटा बंडल), जो सेब-से-सेब प्रति-संयोजन समय मापन को पेचीदा बनाता है। वास्तुकला में यह backtrader के साथ इवेंट-ड्रिवन खेमे में बैठता है; हम इसे तालिका के उस छोर के पास होने की उम्मीद करेंगे, पर हमने यह साबित नहीं किया है।
  • nautilus_trader — Rust/Cython कोर वाला इवेंट-ड्रिवन, स्पष्ट रूप से लाइव-पैरिटी के लिए डिज़ाइन किया गया। इसका कोर कंपाइल्ड है, इसलिए यह वह इवेंट-ड्रिवन इंजन है जिसके पूरा Python टैक्स न चुकाने की सबसे अधिक संभावना है — एक सचमुच दिलचस्प मापन जिसे हमने अभी तक नहीं चलाया।
  • QuantConnect Lean — C#-आधारित, एक बिल्कुल अलग रनटाइम; एक Python हार्नेस में सीधे तुलनीय नहीं।
  • Jesse — इवेंट-ड्रिवन, क्रिप्टो-केंद्रित; हमने इसके डिज़ाइन की समीक्षा एक अलग नोट में की पर यहाँ इसे बेंचमार्क नहीं किया।
  • QSTrader — इवेंट-ड्रिवन, पोर्टफ़ोलियो-उन्मुख; वही पैराडाइम संबंधी चेतावनियाँ।
  • fastquant — हमने कोशिश की; हमारे परिवेश में इंस्टॉल/API टूटा हुआ था, इसलिए कोई आंकड़ा नहीं है। हम कोई अनुमान नहीं लगाने वाले।

जिन आंकड़ों की हम वाकई रिपोर्ट करते हैं उनके बारे में दो ईमानदार चेतावनियाँ। vectorbt, numba, mp+numba, नेटिव-Rust और MLX के आंकड़े समान वर्कलोड पर हमारी अपनी इंजन सीढ़ी से आते हैं, उस OSS हार्नेस से नहीं जिसने चार इवेंट-ड्रिवन पंक्तियाँ पैदा कीं — वह वही वर्कलोड है पर एक अलग मापन-यंत्र, और नेटिव-Rust आंकड़ा एक अनुमानित ~350 है, कोई कसा हुआ मापन नहीं। और परम combos/s हार्डवेयर-विशिष्ट है; जो चीज़ यात्रा करती है वह है क्रम और अनुपात, जो इतने बड़े हैं (ऊपर-से-नीचे 13,000×, pandas-बनाम-फ्रेमवर्क उलटाव के लिए 2.5–4.7×) कि कोई भी उचित हार्डवेयर अंतर उन्हें पलट नहीं सकता।

इवेंट-ड्रिवन इंजनों के बचाव में

अपनी उचित भूमिका में इवेंट-ड्रिवन इंजन: एक जीवित बची रणनीति का एक अकेला सूक्ष्म, उच्च-निष्ठा सत्यापन रन, एक सटीक पन्ना-और-सियान यंत्र के रूप में चित्रित जो एक यथार्थवादी फ़िल मॉडल करता है, मार्जिन और ऑर्डर जीवनचक्र का सम्मान करता है — वही प्रति-बार मशीनरी जो खोज के दौरान शुद्ध बर्बादी थी, यहाँ मूल्यवान शिल्पकारी के रूप में दिखाई गई, तैनाती से पहले एक सावधान ड्रेस रिहर्सल

इसे "इवेंट-ड्रिवन बैकटेस्टर बुरे हैं" के रूप में पढ़ना आसान होगा। यह गलत सीख है, और अन्यायपूर्ण भी।

इवेंट-ड्रिवन इंजन एक अलग काम के लिए बने हैं, और उसमें अच्छे हैं। प्रति-बार ब्रोकर, ऑर्डर जीवनचक्र, फ़िल तर्क, एनालाइज़र — वह मशीनरी इसलिए मौजूद है कि एक बैकटेस्ट को लाइव ट्रेडिंग जितना संभव हो उतना करीब से मिलता-जुलता बनाया जा सके। जब आपका लक्ष्य उस रणनीति का एक अकेला, भरोसेमंद ड्रेस रिहर्सल है जिसे आप तैनात करने ही वाले हैं, तो आप चाहते हैं कि इंजन हर फ़िल पर पसीना बहाए, आंशिक निष्पादन मॉडल करे, मार्जिन का सम्मान करे, और आपको उन दामों पर ट्रेड करने से इनकार करे जो आपको मिल ही नहीं सकते थे। वही निष्ठा ही उत्पाद है। इसकी रनटाइम लागत यथार्थवाद की कीमत है, और एक रन के लिए वह कीमत नगण्य है।

चूक इंजन नहीं है; चूक इसे गलत चरण में इस्तेमाल करना है। शोध के दो अलग चरण हैं जिनकी माँगें विपरीत हैं:

  • खोज थ्रूपुट चाहती है। आप एक परिदृश्य की छानबीन कर रहे हैं, जिसका अधिकांश कचरा है, और आपको उन कुछ को खोजने के लिए हज़ारों बिंदु मूल्यांकित करने होते हैं जो दूसरी नज़र के लायक हैं। प्रति-बिंदु निष्ठा शायद ही मायने रखती है — आप श्रेणीबद्ध कर रहे हैं, तैनात नहीं। यहाँ फ्रेमवर्क टैक्स शुद्ध बर्बादी है।
  • सत्यापन निष्ठा चाहता है। आपके पास मुट्ठी भर उम्मीदवार हैं और आपको यथासंभव सटीकता से जानना है कि क्या वे यथार्थवादी निष्पादन, शुल्क, स्लिपेज, और उन लुक-अहेड जालों से बचे रहते हैं जो कागज़ी रिटर्न को फुला देते हैं। यहाँ इवेंट-ड्रिवन इंजन अपनी लागत वसूल कर देता है।

जिस गलती को फ्रेमवर्क टैक्स दंडित करता है वह है अपनी खोज को अपने सत्यापन इंजन पर चलाना — एक ऐसे परिदृश्य की छानबीन के लिए लिमोज़ीन दरें चुकाना जिसका 99% आप फेंकने वाले हैं।

निर्णय-नियम

व्यावहारिक निष्कर्ष एक अकेले निर्णय में सिमट जाता है:

खोज एक वेक्टराइज़्ड/कंपाइल्ड इंजन पर करें। जीवित बचे उम्मीदवारों का सत्यापन एक इवेंट-ड्रिवन इंजन पर करें।

ठोस रूप से:

  1. स्वीप के लिए एक तेज़ कर्नेल बनाएँ या उधार लें। अगर आप इसे तैयार-शुदा चाहते हैं तो vectorbt; अगर आपकी रणनीति साफ़-सुथरे ढंग से वेक्टराइज़ नहीं होती तो एक numba-कंपाइल्ड लूप (@njit डेकोरेटर ने अकेले ही यहाँ ~140× खरीदा)। पूरा पैरामीटर स्थान उसमें से गुज़ारें।
  2. backtrader, bt, zipline, या किसी भी भारी इवेंट-ड्रिवन फ्रेमवर्क पर एक बड़ा स्वीप कभी न चलाएँ। अगर उनमें से किसी पर एक स्वीप आपकी अड़चन है, तो हल एक बड़ी मशीन नहीं है — यह गलत इंजन है। भोला pandas लूप भी उनमें से दो को हरा देगा।
  3. एक छोटी सूची को निष्ठा जाँच के लिए इवेंट-ड्रिवन इंजन पर पदोन्नत करें। मुट्ठी भर जीवित बचों को लें और उन्हें यथार्थवादी इंजन पर फिर से चलाएँ, जहाँ ब्रोकर मॉडल और फ़िल तर्क उन समस्याओं को उजागर कर सकते हैं जिन्हें तेज़ कर्नेल ने अमूर्त कर दिया था।
  4. दोनों के बीच पैरिटी लागू करें। तेज़ इंजन और निष्ठा इंजन को एक नियत कॉन्फ़िगरेशन पर ट्रेड-गिनतियों और PnL पर सहमत होना ही चाहिए (L=104 पर हमारी ±1-ट्रेड जाँच), वरना खोज और सत्यापन अलग-अलग रणनीतियाँ माप रहे हैं और पूरी पाइपलाइन एक झूठ है।

यही वही दो-गति वास्तुकला है जो तब-तब प्रकट होती है जब खोज और सत्यापन की लागत-रूपरेखाएँ विपरीत हों, और यही कारण है कि हमारा अपना स्टैक पैरामीटर खोज के लिए एक तेज़ वेक्टराइज़्ड/कंपाइल्ड रास्ता रखता है और भारी मशीनरी को अंतिम उद्देश्य फ़ंक्शन मूल्यांकन और पठार जाँचों के लिए आरक्षित रखता है।

मुख्य बातें

  1. लोकप्रिय ≠ स्वीप के लिए तेज़। एक समान, पैरिटी-लॉक की गई वर्कलोड (150k बार, 80 HMA-क्रॉस संयोजन, 2,707 ट्रेड) पर, backtrader (0.11 combos/s) और bt (0.06) दोनों एक भोले pandas लूप (0.28) से धीमे चले। एक परिपक्व, खूब-सराहा गया फ्रेमवर्क अपने आप तेज़ चुनाव नहीं है।
  2. फ्रेमवर्क टैक्स प्रति-बार है, और एक स्वीप उसे गुणा कर देता है। प्रति स्वीप एक करोड़ बीस लाख बार-पुनरावृत्तियाँ, हर एक एक इवेंट ऑब्जेक्ट, एक कॉलबैक, और एक ब्रोकर राउंड-ट्रिप ढोती हुई। एक ऐसी लागत जो एक रन में अदृश्य है, दस हज़ार में पूरा बिल है।
  3. पैराडाइम छत तय करता है। वेक्टराइज़्ड/कंपाइल्ड इंजन (vectorbt 56.9, numba 39.7, mp+numba 246, नेटिव Rust ~350, MLX 779) इवेंट-ड्रिवन इंजनों को परिमाण की दो से चार कोटियों से हराते हैं — ऊपर-से-नीचे ~13,000× तक। वही एल्गोरिथम, बस JIT-कंपाइल किया गया, 140× तेज़ हो गया।
  4. सभी इवेंट-ड्रिवन इंजन बराबर नहीं हैं। backtesting.py (1.42) और PyAlgoTrade (0.51) फिर भी भोले बेसलाइन को हराते हैं; टैक्स इस बात से बढ़ता है कि प्रति-बार मशीनरी कितनी भारी है। कार्यान्वयन तय करता है कि छत के नीचे आप कहाँ उतरते हैं।
  5. दो इंजन, दो चरण। खोज एक वेक्टराइज़्ड/कंपाइल्ड कर्नेल पर करें; जीवित बचों का सत्यापन यथार्थवादी इवेंट-ड्रिवन इंजन पर करें। उन दोनों के बीच ट्रेड-गिनती/PnL पैरिटी लागू करें ताकि दोनों वही रणनीति माप रहे हों।
  6. आपने जो मापा उसके बारे में ईमानदार रहें। हमने आठ इंजनों को पैरिटी के तहत बेंचमार्क किया और जिन्हें नहीं मापा (zipline, nautilus_trader, Lean, Jesse, QSTrader, और वह fastquant इंस्टॉल जो चलता ही नहीं था) उन्हें आंकड़े गढ़ने के बजाय सूचीबद्ध किया।

असहज सारांश: अगर एक पैरामीटर स्वीप आपकी अड़चन है, तो समस्या शायद आपकी मशीन नहीं और आपकी रणनीति नहीं है। समस्या यह है कि आप एक ऐसे इंजन पर खोज चला रहे हैं जो एक अकेले ईमानदार रन के लिए बना था — और आप उस pandas लूप के साथ ज़्यादा तेज़ होते जिसे रखते हुए आपको बहुत शर्म आ रही थी।

पूरा हार्नेस, पैरिटी एसर्शन, और प्रति-इंजन कच्चे JSON परिणाम कमिट 250dbb5 पर benchmarks/bench_oss_engines.py और benchmarks/results_oss/ में रहते हैं। सीढ़ी के कंपाइल्ड/GPU छोर के लिए, बैकटेस्ट इंजन गति सीढ़ी और IPC-टैक्स विश्लेषण देखें।