11 ستمبر، 2026 · قابلِ تکراریت

ایک ہی کوڈ، ایک ہی ڈیٹا، دو مختلف Sharpe: آپ کے بیک ٹیسٹ کے لیے نان ڈیٹرمنزم آڈٹ

ایک ہی کوڈ، ایک ہی ڈیٹا، دو مختلف Sharpe: آپ کے بیک ٹیسٹ کے لیے نان ڈیٹرمنزم آڈٹ

ایک ہی commit، وہی parquet فائلیں، وہی مشین، ایک گھنٹے کے وقفے سے دو رنز۔ Sharpe 1.34 اور Sharpe 1.19۔ کل منافع 41.2% اور 37.8%۔ ٹریڈز کی تعداد 1,418 اور 1,421۔ اور دونوں ایکویٹی منحنیات 11,205 لگاتار بار تک ہر چھپے ہوئے اعشاریے پر متفق رہیں، پھر الگ ہو گئیں۔

0.15یکساں ان پٹس کے باوجود Sharpe کا فرق
3 / 1,418مختلف ہونے والی ٹریڈز
11,205الگ ہونے سے پہلے یکساں بارز

پہلے تین اعداد پریشان کن ہیں۔ آخری عدد دلچسپ ہے، کیونکہ یہ بتاتا ہے کہ مسئلہ پورے رن میں پھیلی ہوئی ناقص floating point گنتی نہیں۔ کسی ایک مخصوص بار پر کوئی منفرد واقعہ ہوا، اور پھر فرق بڑھتا چلا گیا۔ یہ تحریر اسی بار کو ڈھونڈنے، اور یہ سمجھنے کے بارے میں ہے کہ 0.15 کا یہ فرق آپ کے اب تک کے ہر parameter sweep پر کیا اثر ڈالتا ہے۔

حکمتِ عملی: 30 سب سے زیادہ حجم والے USDⓈ-M پرپس پر cross-sectional momentum، ہر 4 گھنٹے بعد ری بیلنس، 12 گھنٹے کے منافع کے لحاظ سے اوپر کے پانچ خریدنا، نیچے کے پانچ شارٹ کرنا، 18 ماہ کی تاریخ، اور ہر لیگ پر فیس اور funding وصول کرنا۔

وہ بار ڈھونڈنا جہاں دونوں رنز الگ ہوئے

اگر آپ ہر بار کی ایکویٹی پوری precision کے ساتھ log کریں تو اس میں تقریباً چار منٹ لگتے ہیں۔ دونوں رنز کو (bar_index, equity, open_positions_hash) کی CSV میں نکالیں، دونوں لوڈ کریں، اور پہلی ایسی انڈیکس تلاش کریں جہاں ان میں اختلاف ہو۔ ہم نے یہ اسکرپٹ پہلے ہی ایک مختلف بگ کے لیے لکھ رکھا تھا؛ اسی وجہ سے میرا ایک پوری صبح اس پر نہیں لگی۔

بار 11,206، 2025-03-14T08:00 UTC۔ پچھلے بار تک ایکویٹی 13 اہم ہندسوں تک یکساں تھی۔ 11,206 پر پوزیشن کے hashes مختلف ہیں: رن A میں SOL کی لانگ پوزیشن، رن B میں AVAX کی۔ رخ ایک، notional ایک، مگر کوائن مختلف۔ اس کے بعد کی ہر چیز اسی فرق کا نتیجہ ہے۔

تو میں نے دونوں رنز میں اس بار کی رینکنگ کے ان پٹس چھاپے۔ وہ ایک جیسے تھے۔ بائٹ بہ بائٹ یکساں، وہی 30 سمبلز، وہی 30 اسکور۔ دو اسکور 0.0 تھے۔ عین صفر، دونوں کے لیے، کیونکہ دونوں ناموں کی lookback window میں ایک 4 گھنٹے کی بار پر کوئی ٹریڈ نہیں ہوئی تھی، اس لیے close-over-close ایک نکلا اور log صفر۔ یہ rounding سے صفر نہیں ہوا تھا۔ صفر ہی تھا۔

دو سمبل rank پانچ پر برابر تھے۔ انتخاب میں اوپر کے پانچ لیے گئے۔ دونوں میں سے کون شامل ہوا، اس کا انحصار اس بات پر تھا کہ sort نے انہیں کس ترتیب میں رکھا، اور sort وہ نہیں کر رہا تھا جو میں سمجھ رہا تھا۔

ٹائی، غیر مستحکم sort، اور وہ بات جسے میں دو سال سے نظرانداز کر رہا تھا

رینکنگ ایک grouped aggregation کے ذریعے چلتی تھی، اور اسے ڈیٹا دینے والا frame، async fetch کے بعد ہر بار سمبلز کے ایک set پر dict comprehension چلا کر بنتا تھا۔ hash seed کے ساتھ set کی iteration order بدلتی رہتی ہے، اور اگر آپ PYTHONHASHSEED کو مقرر نہ کریں تو Python ہر process کے لیے string hash seed کو بے ترتیب بناتا ہے۔ یوں sort سے پہلے قطاروں کی ترتیب رنز میں مختلف تھی، اور برابر key پر غیر مستحکم sort نے ٹائی مختلف انداز میں توڑی۔

ایسی ٹائیاں کوئی انوکھی بات نہیں۔ یہ تو ساختی طور پر پیدا ہوتی ہیں۔ جہاں بھی کوئی feature حد تک پہنچتا یا clip ہوتا ہے، وہاں بالکل برابر قدریں بن جاتی ہیں: صفر حجم والی بارز بالکل صفر منافع دیتی ہیں، clipped z-score ±3.0 پر رک جاتا ہے، کم مختلف قدروں والا rank-transform درجنوں ٹائیاں بناتا ہے، اور boolean filter ہر گزرنے والے نتیجے کو 1.0 اسکور دیتا ہے۔ 4 گھنٹے کی بارز پر 18 ماہ کے اس رن میں انتخابی حد پر 47 بار ٹائی ہوئی۔ ان میں سے تین نے منتخب basket بدلا۔ باقی میں ٹائی ان دو ناموں کے درمیان تھی جو دونوں پہلے سے شامل تھے یا دونوں باہر تھے۔

تقریباً ایک دن تک مجھے یقین تھا کہ data loader میں نان ڈیٹرمنزم ہے، کیونکہ یہی سنسنی خیز جواب لگتا تھا۔ ایسا نہیں تھا۔ ایسا کبھی نہیں ہوتا۔ قصہ ایک set، ایک ٹائی، اور sort کے استحکام سے متعلق ایک ایسے مفروضے کا تھا جسے کسی نے لکھا ہی نہیں تھا۔

تین ٹریڈز سے Sharpe میں 0.15 کا فرق کیوں پڑتا ہے

یہ وہ بات ہے جس پر لوگ اعتراض کرتے ہیں، اور اس کا جواب یہ ہے کہ equity-fraction sizing والا بیک ٹیسٹ ایک ایسا نظام ہے جس کا انحصار راستے پر ہوتا ہے۔ ہر لیگ پر موجودہ ایکویٹی کا 8% لگانے کا مطلب ہے کہ بار n پر ایکویٹی کا فرق، بار n کے بعد ہر notional میں فرق بن جاتا ہے۔

پہلے فرق کا اپنا اثر بہت کم تھا۔ رن B کی AVAX لیگ نو گھنٹوں میں 2.1% گھٹی؛ رن A کی SOL لیگ 0.4% بڑھی۔ اس ٹریڈ کے بعد ایکویٹی کا فرق: 0.21%۔ معمولی۔ لیکن اس کے بعد دونوں رنز ایک حکمتِ عملی نہیں رہے۔ ان کی پوزیشنوں کے سائز ذرا مختلف ہو گئے، چنانچہ ان کی funding accruals بھی ذرا مختلف ہوئیں، اور بعد کی دو حدی ٹائیاں پھر مختلف انداز میں ٹوٹیں کیونکہ ان کے اسکور اب معمولی طور پر مختلف held positions سے آ رہے تھے۔ ان میں سے ایک 2025-03-27 کو پڑی، اس چھ روزہ رجحان سے ایک دن پہلے جس نے رن کے کل PnL کا تقریباً ایک تہائی بنایا۔ رن A پورے اتار چڑھاؤ کے دوران مارکیٹ میں تھا؛ رن B نے ایک ری بیلنس دیر سے انٹری لی۔

منافع کا فرق: 3.4 پوائنٹس۔ Sharpe کا فرق، منافع کے فرق سے ظاہر ہونے والے فرق سے بڑا ہے، کیونکہ رن B کی بدلی ہوئی ٹریڈز زیادہ اتار چڑھاؤ والے عرصے میں آئیں؛ یوں denominator بڑھا اور numerator گھٹا۔ چھوٹی وجہ، دو تقویت دینے والے عوامل۔

اگر آپ کا سائز fixed-notional ہے اور آپ کی انٹریز کا انحصار موجودہ پوزیشنوں پر نہیں، تو آپ اس اثر سے کہیں زیادہ محفوظ ہیں۔ زیادہ تر دلچسپ حکمتِ عملیاں ان دونوں میں سے کوئی نہیں ہوتیں۔

وہ پانچ مقامات جہاں یہ مسئلہ واقعی پیدا ہوتا ہے

ماخذعلامتحل
غیر مقرر PYTHONHASHSEED، جہاں set/dict کی iteration order sort پر اثر ڈالتی ہےرنز میں ٹائی ٹوٹنے کا طریقہ بدلتا ہے؛ ایک خاص بار پر پہلا فرق سامنے آتا ہےseed مقرر کریں؛ ثانوی کلید (symbol) کے مطابق sort کریں تاکہ ٹائیاں ایک ہی طرح ٹوٹیں
برابر key پر غیر مستحکم sort (NumPy/pandas میں quicksort default)اوپر جیسا ہی مسئلہ، seed مقرر کرنے کے بعد بھی برقرار رہتا ہےkind="stable"، یا key کو مکمل بنائیں
bootstrap، train/test shuffles، یا synthetic fill jitter میں غیر مقرر RNGپورے رن میں بتدریج فرق، اختلاف کا کوئی واضح ابتدائی مقام نہیںہر component کے لیے ایک واضح seed رکھیں اور اسے run manifest میں log کریں
متوازی float reduction (جمع کی ترتیب کا انحصار thread count پر)آخری چند بٹس میں فرق؛ عموماً بے ضرر، جب تک threshold comparison عبور نہ ہوتحقیقی رنز کے لیے thread counts مقرر کریں؛ فیصلہ کن حد پر floats کا == کے ساتھ کبھی موازنہ نہ کریں
غیر مقرر library versionsآج دوبارہ وہی نتیجہ ملے، نومبر میں نہیںڈیٹا snapshot hash کے ساتھ manifest میں lockfile hash بھی محفوظ کریں

چوتھی قطار کا مسئلہ اتنی بار نہیں آتا جتنا لوگ ڈرتے ہیں، جبکہ پہلی قطار بار بار نقصان پہنچاتی ہے۔

بٹ بہ بٹ یکساں نتائج ایک آلہ ہیں، خوبی نہیں

آپ کو ڈیٹرمنزم اس لیے چاہیے کہ جب آپ ایک سطر بدلیں تو ایکویٹی منحنی میں آنے والی تبدیلی کا سبب اسی سطر کو ٹھہرا سکیں۔ بس یہی وجہ ہے۔ اب Stratmill پر ہر agent run ڈیٹا snapshot hash، lockfile hash اور ہر seed کے ساتھ ایک manifest لکھتا ہے، اور اگر دوبارہ چلانے سے پچھلا منحنی بٹ بہ بٹ نہ ملے تو وہ تجسس کی بات نہیں، ناکام build ہے۔

لیکن جب نتیجہ دہرانا ممکن ہو جائے تو جان بوجھ کر اسے بدل کر دیکھیں۔ 64 الگ seeds کے ساتھ وہی چیز 64 بار چلائیں اور نتائج کا پھیلاؤ دیکھیں:

jitter band۔ وہی حکمتِ عملی، وہی ڈیٹا، tie-break اور fill order کی 64 مختلف seeded ترتیبیں۔ Sharpe p5 1.12، median 1.27، p95 1.41۔ band کی چوڑائی 0.29۔

اب parameter sweep پر واپس جائیں۔ بہترین config کا اسکور 1.46 تھا۔ 96 میں 40ویں نمبر پر آنے والے config کا اسکور 1.31 تھا۔ ان کے درمیان فرق 0.15 ہے، یعنی band کا نصف۔ sweep نے ان دونوں configs کی درجہ بندی نہیں کی۔ اس نے ہر ایک کی distribution سے ایک نتیجہ لیا اور ان نتائج کو ترتیب دے دیا۔

اس نئی سمجھ نے ہمارے انتخاب کا طریقہ بدل دیا۔ sweep کا نتیجہ تبھی درجہ بندی ہے جب configs کے درمیان فرق، ایک config کے jitter سے زیادہ ہو؛ اور 1,400 ٹریڈز والی راستے پر منحصر حکمتِ عملی میں jitter عموماً اتنا بڑا ہوتا ہے کہ leaderboard کے اوپری تہائی کے فرق مٹا کر انہیں برابر کر دے۔ ایسا ہو تو کسی ایسی چیز کی بنیاد پر انتخاب کریں جسے band چھپا نہ سکے: کم turnover، کم parameters، لاگت سے متعلق ایسا مفروضہ جس کا آپ شک کرنے والے کے سامنے دفاع کر سکیں، یا walk-forward کے اس fold میں بہتر کارکردگی جو آپ کو سب سے کم پسند ہے۔ یہ حقیقی معیار ہیں۔ Sharpe میں 0.15 کا فرق نہیں۔

ایک اور کام کرنے کے لائق ہے، اس سے پہلے کہ آپ ان میں سے کسی نتیجے پر بھروسا کریں۔ ابھی اپنا بیک ٹیسٹ دو بار چلائیں، فی بار ایکویٹی کا فرق نکالیں، اور معلوم کریں کہ آپ کے نتائج بٹ بہ بٹ یکساں ہیں یا 0.15 والے گروہ میں آتے ہیں۔ اس تجربے میں پندرہ منٹ لگتے ہیں، اور یہ بتاتا ہے کہ آپ کی تحقیق کی تاریخ میں کتنا حصہ حکمتِ عملی کو ناپ رہا تھا اور کتنا hash seed کو۔

ڈیٹرمنزم، بیک ٹیسٹنگ، ڈیٹا انجینئرنگ، اوورفٹنگ، python
شیئر کریںXLinkedInFacebookRedditHacker NewsWhatsAppTelegramEmail
← تمام پوسٹس