1-মিনিটের বারের এক ক্যালেন্ডার বছরের ডেটায় 525,600টি সারি থাকার কথা। 2025 সালে BTCUSDT পার্পসের ডেটা টেনে আমরা পেয়েছিলাম 525,557টি—43 মিনিট কম, পূর্ণতার হার 99.992%। একই ভেন্যু থেকে, একই পদ্ধতিতে, একই কোডপাথ ব্যবহার করে একটি মিড-ক্যাপ অল্ট পার্পের ডেটায় 1,247টি সারি কম ছিল: 99.76%। আর একই বছরের US ইকুইটির মিনিট-সিরিজে ক্রিপ্টো সিরিজের তুলনায় প্রায় 427,000টি সারি কম ছিল; সেটা অবশ্য কোনো ফাঁক নয়, বাজার বন্ধ থাকার ফল।
এই সংখ্যাগুলোর মধ্যে 3টি তেমন উল্লেখযোগ্য নয়। হাজার কথা বলার মতো সংখ্যাটি হলো 43।
আপনার ডেটাফ্রেমে ফাঁকের মতো দেখায়, এমন 4টি আলাদা ঘটনা
43টির প্রসঙ্গে যাওয়ার আগে, এই শ্রেণিবিভাগটা বলি—কারণ এই স্তরেই ফাঁক সামলানোর বেশিরভাগ কোড ভুল। আপনার লোকাল parquet-এ কোনো সারি না থাকলে, এই ঘটনাগুলোর কোনটি সেটি ঘটিয়েছে তা বোঝার উপায় থাকে না; আর প্রতিটির জন্য সঠিক করণীয়ও আলাদা।
| ধরন | আসলে কী ঘটেছে | ডেটায় যেভাবে দেখা যায় | সঠিক করণীয় |
|---|---|---|---|
| কোনো ট্রেড হয়নি | বাজার খোলা ছিল; ওই মিনিটে কেউ স্প্রেড পেরিয়ে অর্ডার দেয়নি | কিছু এন্ডপয়েন্টে শূন্য-ভলিউম বার (OHLC সব সমান, trades=0), অন্যগুলোতে সারি অনুপস্থিত | রেখে দিন। এটাই প্রকৃত তথ্য: কেউ ট্রেড করতে চায়নি। |
| ভেন্যু বন্ধ | ম্যাচিং ইঞ্জিন বন্ধ, নির্ধারিত হোক বা না হোক | সারি অনুপস্থিত, কখনো একসঙ্গে অনেকগুলো | stale হিসেবে চিহ্নিত করুন। এর মধ্যে ট্রেড করবেন না। |
| ইনস্ট্রুমেন্টে ট্রেড স্থগিত | LULD ব্যান্ড ভাঙা, খবরের অপেক্ষা, ডিলিস্টিংয়ের নোটিশ | সারি অনুপস্থিত, এরপর পুনরায় খোলার নিলামের ট্রেড | stale হিসেবে চিহ্নিত করুন, আর পুনরায় খোলার ঘটনাকে বিচ্ছিন্নতা হিসেবে ধরুন। |
| আপনার ভুল | রেট-লিমিটে আটকে যাওয়া পেজিনেশন, কোনো পেজ বাদ পড়া রিট্রাই, লুপে টাইমজোন-সীমানার বাগ | সারি অনুপস্থিত, ওপরের ঘটনাগুলোর থেকে আলাদা করে চেনা যায় না | শনাক্ত করে আবার ডেটা আনুন। এই সমস্যাটাই আপনি ঠিক করতে পারেন। |
টেবিলের প্রথম সারি নিয়ে ভেন্যুগুলোর মধ্যে মতভেদ আছে, আর সমস্যাটা ঠিক সেখানেই। Coinbase-এর ক্যান্ডেল এন্ডপয়েন্ট খালি বালতি পুরোপুরি বাদ দেয়, তাই কম লেনদেন হওয়া কোনো পেয়ারের ইতিহাসে আক্ষরিক অর্থেই অনেক ফাঁক থাকে। Binance-এর klines সাধারণত ভলিউম 0-সহ একটি কৃত্রিম বার দেয়, যেখানে open=high=low=close আগের ট্রেডের দামে স্থির থাকে। মূল ঘটনা একই, কিন্তু ডেটার রূপ আলাদা; আর পুরো মিনিটের গ্রিডে reindex করা loader আপনাকে না জানিয়েই একটিকে অন্যটিতে বদলে দেয়। ফাঁক পূরণের কোড লেখার আগে আপনার ভেন্যু কী করে তা যাচাই করুন, পরে নয়।
অল্টে অনুপস্থিত 1,247 মিনিটের প্রায় সবই প্রথম শ্রেণির ঘটনা: রবিবার 04:00 UTC-তে অগভীর অর্ডারবুক, কেউ ট্রেড করছে না। বিরক্তিকর, বোঝা সহজ, আর বেশিরভাগই ক্ষতিহীন—কারণ তখন কৌশলটি এমনিতেও ট্রেড করত না। আর ঠিক সে কারণেই আমি সেটি দেখা বন্ধ করে 43টির দিকে ফিরে গিয়েছিলাম।
43 মিনিট ছড়িয়ে-ছিটিয়ে ছিল না
অনুপস্থিতির হার যদি সমানভাবে ছড়াত, তাহলে এক বছরে 43 মিনিট 43টি আলাদা একক ফাঁক হিসেবে দেখা যেত—প্রতি সাড়ে 8 দিনে একটি করে, যার প্রতিটিকে সামান্য ত্রুটি বলে উড়িয়ে দেওয়া যেত। কিন্তু আমরা তা পাইনি। ফাঁকগুলো এসেছিল 6টি দফায়: একটি টানা 19 মিনিটের, একটি 11 মিনিটের, দুটি 4 মিনিটের, আর কয়েকটি জোড়া মিনিটের। দুর্ঘটনাবশত 43টি ফাঁক নয়, বরং 6টি ঘটনা।
আর ঘটনাগুলো নির্ভর করে আপনার কাছে কোন বিষয়টি গুরুত্বপূর্ণ তার ওপর। ভেন্যুগুলো চাপের সময় বন্ধ হয়ে যায়, আর দাম ওঠানামা করার সময়ই চাপ বাড়ে। বছরের প্রতিটি ঘণ্টাকে বাস্তবায়িত ভোলাটিলিটি অনুযায়ী ভাগ করে দেখেছিলাম, অনুপস্থিত মিনিটগুলো কোথায় ছিল: সেগুলোর 61% ছিল সর্বোচ্চ ডেসাইলে। যেকোনো একটি মিনিট অনুপস্থিত থাকার সামগ্রিক সম্ভাবনা 0.008%। সর্বোচ্চ-ডেসাইল ভোলাটিলিটির ঘণ্টার মধ্যে থাকলে সেই সম্ভাবনা প্রায় 0.05%—6 গুণ বেশি, তাও আবার গুচ্ছাকারে।
তাই আপনার ডেটা-মানের ড্যাশবোর্ডে পূর্ণতার যে মেট্রিকটি দেখানো হয়, সেটি ভুল জিনিস মাপছে। 99.992% শুনলে মনে হয় ডেটাসেটটি নিয়ে আর ভাবতে হবে না। কিন্তু বাস্তবে এর মানে, যে ঘণ্টাগুলোতে আপনার কৌশল কিছুই করে না তখন সিরিজটি সম্পূর্ণ থাকে, আর যে ঘণ্টাগুলোতে সবকিছু করে তখনই ফাঁকে ভরে যায়। ভোলাটিলিটি বাড়লে ট্রিগার হয় এমন একটি মোমেন্টাম সিস্টেমের ফাঁকে পড়ার সম্ভাবনা হেডলাইন সংখ্যাটি যা বোঝায় তার চেয়ে অনেক বেশি, আর ফাঁকটি আসে ট্রেড চলার মাঝখানে।
কয়েক লাইন পরেই forward-fill কী করে
এই ব্যর্থতার কারণেই লেখাটা লিখছি। 19 মিনিটের দফাটি ধরুন। প্রচলিত ডেটা-পরিষ্কারের পদ্ধতি: পুরো মিনিটের গ্রিডে reindex করুন, শেষ ক্লোজ থেকে OHLC-তে forward-fill করুন, ভলিউম শূন্য দিন। এবার সিরিজটি ধারাবাহিক, আর কোনো NaN চোখে না পড়ায় আপনার ইন্ডিকেটরগুলো ঠিকমতো চলে।
ওই 19টি বারে high == low == close। প্রতিটি বারের True Range শূন্য। ওই উইন্ডোতে গণনা করা ATR(14), ভেন্যু বন্ধ হওয়ার আগের প্রায় 240 USDT-এর রিডিং থেকে কমতে কমতে ভেন্যু ফিরে আসার সময় প্রায় 34-এ নামে—উইন্ডোতে টিকে থাকা 5টি প্রকৃত বারই পুরো গড়টিকে বয়ে নিয়ে চলে। এবার সেটি ভোলাটিলিটি-স্কেল করা পজিশন সাইজারে দিলে, সাধারণ size = risk_budget / ATR ধরনের, পজিশনের আকার 7 গুণ বেড়ে যায়।
পরের প্রকৃত বারটি হলো ভেন্যু খোলার ট্রেড, আর সেটি মোটেও শান্ত বার নয়। আমাদের ক্ষেত্রে এটি শেষ প্রি-আউটেজ ক্লোজ থেকে 1.8% দূরে খুলেছিল। ব্যাকটেস্ট দিব্যি 7 গুণ বড় পজিশন নিয়ে 1.8% ফাঁকের মধ্যে ঢুকে পড়ল—এমন দামে, যে দামে ওই ফিল হওয়া সম্ভব ছিল না, আর কেউ কোটও দিচ্ছিল না। ইকুইটি কার্ভে ওই একটিমাত্র কৃত্রিম ট্রেডের মূল্য ছিল এক মাসের বৈধ P&L-এর চেয়েও বেশি, ভুল দিকে—আর এর পুরো কারণ ছিল ডেটাফ্রেমটি পরিপাটি করতে লেখা ডেটা-পরিষ্কারের একটি লাইন।
সারি বাদ দিয়ে দেওয়াও সমাধান নয়; টুপি বদলে একই বাগ হাজির হয়। সারিগুলো বাদ দিলে আপনার পূর্ণসংখ্যাভিত্তিক ইনডেক্সে হিসাব করা lookback ভুল দেখায়: একটি "20-bar EMA" এখন আউটেজজুড়ে দেয়ালঘড়ির হিসাবে 39 মিনিট জুড়ে বিস্তৃত; সীমা পেরোনো বার-টু-বার রিটার্নে পুরো 1.8% লাফকে এক মিনিটের ওঠানামা হিসেবে ধরা হয়; আর প্রতি-বারের যেকোনো ভোলাটিলিটির হিসাব একে 60-sigma ঘটনা পড়ে। কোনো সতর্কবার্তা আসে না। ইনডেক্স তখনও একমুখীভাবে বাড়তে থাকে।
রিস্যাম্পলিংয়ে সমস্যাটা অদৃশ্য হয়ে যায়
বেশিরভাগ গবেষণা 1-মিনিটের বারে চলে না, চলে একাধিক বার একত্র করে বানানো বারে; আর একত্রীকরণে সমস্যাটা ধুয়েমুছে যায়। 5-মিনিটে resample করলে 19 মিনিটের ফাঁক 4টি বারে পরিণত হয়, যার প্রথম ও শেষটি আংশিক। টিকে থাকা 2 মিনিট থেকে Pandas দেখতে একদম যুক্তিসংগত OHLC হিসাব করবে, আর সেটিকে ঠিক 5 মিনিটের ডেটা দিয়ে তৈরি বারের মতোই লেবেল দেবে। আউটপুটে কোনো চিহ্ন থাকবে না, যা দিয়ে এই দুই বার আলাদা করা যায়।
আমার জানা সবচেয়ে সহজ সমাধান: প্রতিটি resample-এর পরও একটি bars_in_window কলাম সঙ্গে রাখুন, কখনো বাদ দেবেন না। প্রতি সারিতে একটি পূর্ণসংখ্যা রাখলেই, পরের প্রতিটি ধাপে কোনো বার বিশ্বাসযোগ্য কি না—এ প্রশ্নের উত্তর দেওয়া যাবে। আমরা seconds_since_last_real_print-ও সঙ্গে রাখি, যাতে একই তথ্য এমন রূপে থাকে, যা এক্সিকিউশন স্তর কাজে লাগাতে পারে।
এখনকার নীতিমালা
আমাদের এজেন্টরা এখন এই ক্রমে যা প্রয়োগ করে:
- কখনো নীরবে reindex করবেন না। loader একটি গ্যাপ ম্যানিফেস্ট তৈরি করে—শুরু, শেষ, দৈর্ঘ্য এবং চারটি শ্রেণির মধ্যে কোনটি বলে মনে হচ্ছে। অনুপস্থিত মিনিটের দফা যদি 3 বারের কম হয় এবং আশপাশের বারের ভলিউম কম থাকে, সেটিকে কোনো ট্রেড না হওয়া মিনিট ধরা হয়। সক্রিয় সময়ে এর চেয়ে দীর্ঘ কিছু হলে, প্রমাণ না পাওয়া পর্যন্ত আউটেজ হিসেবে ধরা হয়।
- ব্যাখ্যার আগে আবার ডেটা আনুন। শুরুর দিকের ফাঁকের অর্ধেকই ছিল পেজিনেশন বাগ। অন্য এন্ডপয়েন্ট বা অন্য ভেন্ডর থেকে দ্বিতীয়বার ডেটা আনলে চতুর্থ শ্রেণির সমস্যা মেটে এবং কোনো বিচার করার আগেই সমস্যা কমে আসে।
- ফাঁক পূরণ নয়, stale অবস্থা যাচাই করুন। কৌশলটি একটি
data_ageইনপুট পায়, সঙ্গে একটি কঠোর নিয়ম: শেষ প্রকৃত ট্রেডের পর N বারের বেশি পেরিয়ে গেলে নতুন পজিশনে ঢোকা যাবে না; আর খোলা পজিশন পুনরায় খোলার সময় শুধু মার্কেট অর্ডারে বন্ধ হবে, যেখানে দামের সঙ্গে স্পষ্ট গ্যাপ-ঝুঁকির ছাড় হিসাব করা থাকবে। যে ফিল ঘটাই সম্ভব ছিল না, তা এমন ট্রেডের চেয়েও খারাপ যা করা হয়নি। - ইন্ডিকেটরে কাল্পনিক দাম নয়, NaN যাবে। forward-fill করা দাম কখনো ফিচার স্তরে পৌঁছায় না। ATR হিসাব করা না গেলে তার মান অনির্ধারিত; আর অনির্ধারিত মানে কোনো পজিশন নয়। চুপিসারে 7 গুণ হয়ে যাওয়ার চেয়ে স্পষ্ট ব্যর্থতা ভালো।
- ফাঁকের আশপাশের পারফরম্যান্স আলাদা করে দেখান। আমরা যে প্রতিটি ব্যাকটেস্ট প্রকাশ করি, তাতে হেডলাইন ফলাফলের পাশাপাশি আউটেজের কাছাকাছি সময়ে হওয়া ট্রেড বাদ দিয়ে হিসাব করা P&L-ও দেখানো হয়। ওই ট্রেডগুলোই যদি ফল বয়ে আনে, তাহলে ফলটি ডেটার কৃত্রিম প্রভাব।
আজই চালাতে পারেন এমন একটি দ্রুত অডিট: আপনার অনুপস্থিত মিনিটগুলোকে একটানা দফায় ভাগ করুন, তারপর দেখুন আপনার ব্যাকটেস্টের কত ভাগ ট্রেড কোনো দফার শুরু বা শেষের 30 মিনিটের মধ্যে খোলে বা বন্ধ হয়। হার 1%-এর কম হলে ফাঁকগুলো সম্ভবত বিশেষ কিছু প্রভাব ফেলছে না। 5% বা তার বেশি হলে আপনার ইকুইটি কার্ভের অংশবিশেষ আসলে এক্সচেঞ্জ বন্ধ থাকার গল্প।
অনুপস্থিতির ধরনটাই এখন আমার কাছে সংখ্যার চেয়ে বেশি নির্ভরযোগ্য সংকেত। নিষ্ক্রিয় সময়ে ছড়িয়ে থাকা হাজারো ফাঁকসহ ডেটাসেট সাধারণত ঠিক থাকে। অল্প কয়েকটি ঘন গুচ্ছসহ ডেটাসেট বলছে, চাপের মধ্যে কিছু একটা ভেঙে পড়ে; আর চাপের মধ্যে যা ভেঙে পড়ে, আপনার কৌশল কাজ করে ঠিক সেখানেই।
← সব পোস্ট


