26 اگست، 2026 · مارکیٹ ڈیٹا

ایک 1 منٹ کی بار کی ساخت: OHLCV آپ کے بیک ٹیسٹ کو کیا کچھ نہیں بتاتا

ایک 1 منٹ کی بار کی ساخت: OHLCV آپ کے بیک ٹیسٹ کو کیا کچھ نہیں بتاتا

یہ رہی ایک بار۔ Binance USDⓈ-M perpetual، BTCUSDT، 13:46:00 UTC سے شروع ہونے والا منٹ۔ یہ klines endpoint سے بارہ قدروں کی ایک سادہ array کے طور پر آئی، اور retail اور اس سے ملتی جلتی quant research میں input کی سب سے عام اکائی یہی ہے۔ ہماری تقریباً ہر generated strategy کسی نہ کسی ایسی ہی ساخت کو چھوتی ہے۔

تو آئیے اسے index بہ index کھولتے ہیں اور دیکھتے ہیں کہ بیک ٹیسٹ اس میں سے کتنا کچھ غلط سمجھتا ہے۔

IndexValueName
01773495960000اوپن ٹائم (ms)
1"84120.50"اوپن
2"84177.90"ہائی
3"84098.10"لو
4"84163.40"کلوز
5"38.417"بیس والیوم (BTC)
61773496019999کلوز ٹائم (ms)
7"3232108.94"کوٹ والیوم (USDT)
81204ٹریڈز کی تعداد
9"21.883"taker buy base volume
10"1841203.55"taker buy quote volume
11"0"نظرانداز کریں

[0] اور [6]: یہ کون سا منٹ ہے، اور گھڑی کس کی ہے

اوپن ٹائم 1773495960000، کلوز ٹائم 1773496019999۔ دوسرے پر غور کریں: یہ 19999 پر ختم ہوتا ہے، یعنی اگلی بار کے اوپن سے ایک ملی سیکنڈ پہلے۔ یہ window half-open ہے، اور exchange آپ کو یہ بات صاف صاف بتا رہا ہے۔ جن data bugs کا میں نے پیچھا کیا ہے، ان میں سے نصف کی ابتدا اس وقت ہوئی جب کسی نے دونوں endpoints کو inclusive سمجھ کر boundary پر ایک trade دو بار گن لیا، یا resample کو اس طرح align کیا کہ ہر 5-minute bar اپنے پڑوسی سے ایک ملی سیکنڈ ادھار لے رہی تھی۔

یہ timestamps exchange کے matching-engine time ہیں۔ نہ آپ کی گھڑی، نہ vendor کی ingestion clock، اور نہ وہی clock جو funding snapshot استعمال کرتا ہے۔ جب آپ kline series کو کسی دوسرے endpoint سے حاصل کی گئی funding-rate series یا open-interest series کے ساتھ join کرتے ہیں، تو آپ ایسے sources کو ملا رہے ہوتے ہیں جو زیادہ تر وقت تقریباً ایک سیکنڈ تک متفق ہوتے ہیں۔ 1-minute strategy کے لیے ہر joined row میں یہ 1.7% timing error ہے۔ sub-minute چیزوں کے لیے یہ مہلک ہے۔

اور یہ bar اپنے اوپن پر stamped ہے۔ اس میں موجود معلومات 13:46:59.999 تک معلوم نہیں ہو سکتیں۔ ہر indexing convention کا سوال — کیا میں ایک بار shift کروں، event time کے طور پر bar-close timestamp استعمال کروں یا bar-open — دراصل وہی lookahead کا سوال ہے، بس بھیس بدلا ہوا ہے۔

[1] "84120.50": وہ اوپن جس پر آپ trade نہیں کر سکتے

اوپن اس window کے اندر ہونے والی پہلی trade کی قیمت ہے۔ یہ دو دوسرے لوگوں کے درمیان مکمل ہو چکا transaction ہے۔ جب تک bar آپ کے dataframe میں ایک row کے طور پر موجود ہوتی ہے، یہ قیمت ساٹھ سیکنڈ پرانی ہو چکی ہوتی ہے۔

پچھلی bar 84109.80 پر بند ہوئی تھی، اس لیے دو متصل 1-minute bars کے درمیان موجود gap میں $10.70 کی price movement ہے۔ یہ 1.3 basis points ہے، chart پر نظر نہیں آتی، اور تقریباً taker fee کا ایک چوتھائی ہے۔ ٹھیک ہے۔ لیکن US CPI print کے دوران یہی series کسی alt perp پر دیکھیں، تو inter-bar gaps 15 سے 40 bps تک پہنچ جاتے ہیں۔ جو backtest bar close پر signal دے کر next-bar open پر fill کرتا ہے، وہ خاموشی سے فرض کر رہا ہے کہ gap صفر ہے — اور یہ عین اس وقت صفر ہے جب اس کی کوئی اہمیت نہیں ہونی چاہیے۔

[2] اور [3]: "84177.90" ہائی، "84098.10" لو

یہ وہ دو اعداد ہیں جہاں زیادہ تر fill engines دم توڑ دیتے ہیں، اس لیے یہ حصہ طویل ہے۔

ہائی اور لو انتہائی چھوئی گئی قیمتیں ہیں۔ ان میں نہ size ہوتا ہے نہ duration۔ اسی منٹ کی raw trade tape سے معلوم ہوتا ہے کہ 84100.00 یا اس سے کم کی ہر چیز 1.4 سیکنڈ کی window میں نو prints کے دوران مجموعی طور پر 0.62 BTC تھی۔ تو 84100 پر موجود backtest stop "fill" ہو جاتا ہے — اور اگر آپ کی position 3 BTC ہے، تو نیچے جاتے ہوئے آپ نے پوری visible book کھا لی، جبکہ آپ کے size کا باقی حصہ 84105–84130 کی واپسی کے دوران کہیں مکمل ہوا۔ bar کہتی ہے کہ low 84098.10 تھا۔ bar یہ نہیں بتاتی کہ وہاں صرف $52,000 notional trade ہوا تھا۔

1204منٹ میں trades
0.032 BTCاوسط print size (~$2.7k)
0.62 BTClow tick پر یا اس سے کم کل traded مقدار
57%والیوم buyer-initiated تھا

دوسری سمت زیادہ خراب ہے، کیونکہ وہ آپ کو خوش فہمی میں مبتلا کرتی ہے۔ فرض کریں 84175 پر آپ کی resting limit sell تھی۔ bar کی high 84177.90 ہے، اس لیے ایک سادہ engine آپ کو 84175 پر fill کر کے taker cost کے بجائے maker rebate درج کر دے گا۔ مگر واقعی وہ fill ہوا یا نہیں، اس کا انحصار اس price level پر queue position پر ہے، جسے bar جان ہی نہیں سکتی اور غالباً آپ نے کبھی record بھی نہیں کیا۔ چھو لیا جانا fill ہونا نہیں ہے۔

ہمارے fill engine میں طے شدہ اصول یہ ہے: resting limit تبھی fill ہوگی جب bar اس level سے آگے trade کرے، محض اسے چھو لینا کافی نہیں۔ exact extreme tick پر fills کے لیے trade tape سے volume-at-price evidence درکار ہے، ورنہ انہیں reject کر دیا جاتا ہے۔ اس سے ایک عام mean-reversion book کی تقریباً 6% trades ختم ہوئیں اور ایک candidate کا backtested Sharpe 1.9 سے 1.1 رہ گیا۔ وہ candidate کبھی حقیقی تھا ہی نہیں؛ fill rule ہی پہلی چیز تھی جو اتنی ایماندار تھی کہ یہ بات کہہ سکے۔

متعلقہ trap: intrabar path۔ اگر bar کی range آپ کے stop اور take-profit دونوں کو سمیٹتی ہو، تو OHLCV یہ نہیں بتا سکتا کہ پہلے کون سا آیا۔ ہر engine کو کوئی convention اختیار کرنا پڑتا ہے۔ ہمارا ہمیشہ stop-first فرض کرتا ہے، جو محتاط ہے، کبھی کبھار غلط بھی، مگر کبھی fake win نہیں بناتا۔ اگر آپ کا engine take-profit-first فرض کرتا ہے، تو wide-range bars ابہام سے profit گھڑیں گی — اور یہی wide-range bars آپ کی PnL distribution پر غالب ہوتی ہیں۔

[4] "84163.40": bar کا سب سے کم robust عدد

کلوز window کا آخری print ہے۔ بس اتنی سی بات۔ یہ 0.002 BTC کا کوئی odd-lot بھی ہو سکتا ہے جو کسی bot نے 13:46:59.8 پر اپنی position مکمل کرنے کے لیے کیا ہو۔ یہی ایک ساختی طور پر من مانا tick ہے جسے زیادہ تر research pipelines ہر signal نکالنے، ہر position mark کرنے، اور ہر exit کا جائزہ لینے کے لیے استعمال کرتی ہیں۔

BTC perps پر اس کی اہمیت معمولی ہے؛ مگر 04:00 UTC پر کسی thin altcoin perp میں یہ بہت اہم ہو جاتی ہے، اور اسی منٹ کے لیے دو venues کے closes کا فرق آپ کے پورے per-trade edge سے زیادہ ہو سکتا ہے۔ جب کسی strategy کی PnL خاص طور پر close پر منحصر ہو، تو ہم اسے exchange mark price پر دوبارہ چلاتے ہیں، جو index-derived اور چھیڑ چھاڑ کے لحاظ سے کہیں زیادہ مشکل ہے۔ اگر نتائج مختلف ہوں، تو strategy artifact trade کر رہی تھی۔

[5] اور [7]: "38.417" اور "3232108.94"، volume آخر کس چیز کی اکائی میں ہے

Base volume BTC ہے؛ quote volume USDT۔ یہاں دونوں موجود ہیں، جو ایسی سہولت ہے جو ہر venue نہیں دیتا۔ اس کی اہمیت venues کے درمیان aggregation میں ہے۔ Coin-margined contracts کو $100 notional کے contracts میں quote کیا جاتا ہے۔ کچھ equities feeds round lots report کرتے ہیں۔ Prediction market venues share counts report کرتے ہیں، جہاں ہر share ڈالر میں متعین binary claim ہوتا ہے۔ کسی mixed universe میں "volume" کو ایک ہی notional unit پر normalize کیے بغیر جمع کرنا liquidity ranking کو خالص بکواس بنا دیتا ہے — ایسی بکواس جو مستقل دکھائی دیتی ہے اور review سے بھی بچ نکلتی ہے۔

Ingest کے وقت ہر چیز کو quote notional پر normalize کریں۔ raw field بھی محفوظ رکھیں، مگر strategy کو اسے کبھی دکھانے نہ دیں۔

[8] 1204: وہ field جسے آپ کا impact model طے کرنا چاہیے

Trade count کو base volume میں تقسیم کریں تو اوسط print 0.032 BTC، یعنی تقریباً $2,700 بنتا ہے۔ اگر آپ کی candidate strategy ایک ساتھ $250,000 داخل ہونا چاہتی ہے، تو وہ اس منٹ کے typical transaction سے تقریباً 92 گنا بڑے سائز کی ہے۔ یہی عدد، کسی generic 5-bps slippage constant کے بجائے، آپ کی square-root impact term کو drive کرنا چاہیے۔ ہم ہر bar کے لیے participation rate کو first-class column کے طور پر compute کرتے ہیں اور ان strategies کو reject کرتے ہیں جن کی median entry bar notional کے چند فیصد سے زیادہ ہو، کیونکہ اس کے بعد کی ہر چیز افسانہ ہے۔

Trade count عجیب منٹوں کی بھی سستی نشاندہی کرتا ہے۔ Volume معمول کے مطابق، مگر trade count 11 تک گر گیا؟ کسی نے block کیا ہے۔ Volume معمول کے مطابق، trade count 9,000 پر؟ یہ liquidation cascade ہے جو ننھے ٹکڑوں میں چبائی جا رہی ہے۔

[9] اور [10]: "21.883"، وہ field جسے سب پھینک دیتے ہیں

Taker buy base volume۔ اس bar کے 38.417 BTC میں سے 21.883 buyer-initiated تھا، اس لیے signed volume delta +5.349 BTC ہے اور aggressor split buyers کی طرف 57/43 ہے۔ Exchange آپ کو order flow imbalance مفت دے رہا ہے، ایسی field میں جسے زیادہ تر لوگ کبھی نہیں پڑھتے کیونکہ pandas نے ان کے لیے column کا نام نہیں رکھا۔

میں یہ دعویٰ نہیں کر رہا کہ یہ اکیلا returns کی پیش گوئی کرتا ہے؛ naive delta strategies fee ledger کو رقم عطیہ کرنے کے زیادہ قابلِ اعتماد طریقوں میں شامل ہیں۔ مگر یہ price سے واقعی مختلف measurement ہے، اسی request میں دستیاب ہے جو آپ پہلے ہی کر رہے تھے، اور یہ فرق بتاتا ہے کہ rally خریداری سے آئی یا اس لیے ہوئی کہ sellers پیچھے ہٹ گئے۔ OHLC میں دونوں ایک جیسے دکھتے ہیں، مگر دس منٹ بعد مختلف برتاؤ کرتے ہیں۔ ہمارا research agent کسی ایسی venue پر جو اسے شائع کرتی ہو، taker split کو نظرانداز کرنے والی hypothesis کو میز پر پڑے ثبوت چھوڑنے کے مترادف سمجھتا ہے۔

[11] "0": ignore — اور باقی سب کچھ جو یہاں موجود نہیں

Index 11 ایک deprecated field ہے، مستقل طور پر صفر۔ زیادہ دلچسپ فہرست ان چیزوں کی ہے جو اس bar میں شامل نہیں: نہ bid، نہ ask، نہ spread، نہ book depth، نہ funding rate، نہ open interest، نہ liquidations، نہ mark price، نہ index price۔ اور نہایت اہم بات، یہ جاننے کا کوئی طریقہ نہیں کہ آپ کا order maker ہوتا یا taker، یعنی اس venue پر 0.045% ادا کرنے اور 0.01% کمانے کا فرق۔

لہٰذا صرف klines پر بنایا گیا کوئی بھی fee model ایک مفروضہ ہے جو عدد کا لباس پہنے ہوئے ہے۔ ہم ہر strategy کو ابتدا ہی میں اپنا execution style بتانے پر مجبور کر کے، اور جس چیز کے برعکس ثبوت نہ ہو اس پر taker charge لگا کر، یہ مسئلہ حل کرتے ہیں۔

وہ bar جو کبھی دکھائی ہی نہیں دی

آخری نکتہ، اور majors سے باہر سب سے زیادہ نقصان دہ۔ جس منٹ میں zero trades ہوں، وہاں کوئی kline بنتی ہی نہیں۔ Vendors اور libraries عموماً اسے forward-fill کر دیتے ہیں: open = high = low = close = previous close، volume 0۔ آپ کا indicator خوشی سے compute ہوتا رہتا ہے۔ آپ کی strategy ایک valid row دیکھتی ہے اور ایسے منٹ میں signal بنا سکتی ہے جس میں دنیا میں کسی نے اس instrument کی trade ہی نہیں کی۔

ایک mid-cap perp میں جسے ہم نے ingest کیا، بارہ ماہ کی window میں 1-minute bars میں سے 4.1% پر zero trades تھیں۔ اس symbol پر ایک mean-reversion candidate اپنی 38% entries synthetic bars پر رکھ رہا تھا، کیونکہ flat synthetic prices ہر اس چیز کے لیے کشش رکھتی ہیں جو moving average سے deviation ناپتی ہو۔ backtest شاندار تھا۔ وہ data کے gaps trade کر رہا تھا۔

اسی لیے اب ingest layer ہر bar کے ساتھ ایک synthetic boolean رکھتی ہے، جو ہر resample میں آگے منتقل ہوتا ہے، اور verification gauntlet ایسی ہر strategy پر fail ہو جاتی ہے جس کی trades اس پر جمع ہوں۔ سستا column ہے۔ اس نے ہمارے لکھے ہوئے کسی بھی indicator سے زیادہ candidates ختم کیے ہیں۔

بارہ قدریں۔ ان میں سے چار کو معمول کے مطابق غلط سمجھا جاتا ہے، دو کو معمول کے مطابق ضائع کیا جاتا ہے، اور ایک پوری category ایسی ہے جو row میں موجود نہیں مگر engine اسے فرض کر لیتا ہے۔ اپنے اگلے backtest سے پہلے اپنے store سے ایک raw bar نکالیں اور ہر field کو بلند آواز میں اس کے مقابل پڑھیں جو آپ کی fill logic اس کے بارے میں فرض کرتی ہے۔ یہ بیس منٹ کی مشق ہے، اور میں نے کبھی کسی کو یہ کرتے اور کچھ نہ پاتے نہیں دیکھا۔

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