পলিমার্কেট বট টিউটোরিয়াল ·
পলিমার্কেট ফ্যান্টম ফিল কীভাবে শনাক্ত করবেন (যেসব অর্ডার ফিল হয়েছে দেখায় কিন্তু আসলে হয়নি), idempotent রিট্রাই বসাবেন, status=matched ও বুকে-স্থির-থাকার মধ্যে পার্থক্য করবেন, এবং ক্ষণস্থায়ী ব্যর্থতা থেকে কীভাবে টিকে থাকবেন।
এই অধ্যায়ে কী আছে
ফ্যান্টম ফিল হলো পলিমার্কেট-নির্দিষ্ট এক ধরনের ব্যর্থতা, যেখানে CLOB মিল স্বীকার করে কিন্তু চেইন এখনও ERC-1155 ট্রান্সফার নিশ্চিত করেনি। এরপর প্রায় ~5 সেকেন্ডের মধ্যে আরেকটি অর্ডার দিলে বিভ্রান্তিকর "balance: 0" ত্রুটি দিয়ে প্রত্যাখ্যাত হয়। এর সমাধান হলো idempotence আর নিষ্পত্তির জন্য অপেক্ষা। এই অধ্যায়ে সেই প্রোডাকশন পদ্ধতি আছে, যার জন্য আমরা বাস্তব টাকা খরচ করেছি।
- ফ্যান্টম ফিল কী
- status=matched বনাম status=delayed বনাম status=posted
- পোলিং প্যাটার্ন: উদযাপনের আগে স্ট্যাটাস poll করুন
- ফ্যান্টম-ফিল প্রতিষেধক হিসেবে FOK
- client_order_id দিয়ে idempotent রিট্রাই
- বাস্তব প্রোডাকশন ঘটনা: আমরা কীভাবে আমাদেরটা ঠিক করেছি
- কোড: শনাক্ত-করে-তবে-কাজ করার অর্ডার-পরবর্তী প্যাটার্ন
ফ্যান্টম ফিল কী
ফ্যান্টম ফিল হলো যখন CLOB API আপনার অর্ডারের জবাবে status: "matched" দেয়, কিন্তু অন-চেইন ERC-1155 ট্রান্সফার এখনও নিষ্পত্তি হয়নি। CLOB মিলকারী Polygon ব্লক-উৎপাদনের চেয়ে দ্রুত (~2s প্রতি ব্লক)। API মিলের পর প্রায় 2-5 সেকেন্ড ধরে, মিলকারী যেসব টোকেন আপনার বলে জানায়, আপনার ওয়ালেট অন-চেইনে সেগুলো ধরে না।
বটের বাগ তখনই প্রকাশ পায়, যখন পরবর্তী পদক্ষেপ - সাধারণত টেক-প্রফিট বসানোর একটি GTC সেল - ওই জানালার মধ্যে চালানো হয়। তখন CLOB চেইন-ব্যালান্স যাচাই করে, শূন্য দেখে, এবং not enough balance / allowance: balance: 0, order amount: N দিয়ে প্রত্যাখ্যান করে। ত্রুটি-বার্তায় allowance-কে দোষ দেওয়া হয়; আসল কারণ নিষ্পত্তির বিলম্ব।
প্রথমবার এটা ঘটলে আপনি allowance বাগ ধরে নিয়ে এক ঘণ্টা নষ্ট করবেন। সমাধান খুবই সহজ: অপেক্ষা করুন, যাচাই করুন, তারপর বসান।
status=matched বনাম status=delayed বনাম status=posted
অর্ডার-বসানোর প্রতিক্রিয়ায় একটি status ক্ষেত্র থাকে, যার তিনটি মান গুরুত্বপূর্ণ।
matched: অর্ডারটি সঙ্গে সঙ্গে বুকের সঙ্গে মিলেছে। ইনভেন্টরি 2-5 সেকেন্ডের মধ্যে নিষ্পত্তি হবে। সফল হলে FOK/FAK এটাই ফেরত দেয়।delayed: মিলকারী সমকালীনভাবে নিষ্পত্তি করতে পারেনি এবং মিল-সারিতে রেখেছে। বিরল; সাধারণত ভিড় বোঝায়। অপেক্ষা-করে-যাচাই প্যাটার্নের জন্য এটিকেmatched-এর মতোই বিবেচনা করুন।posted(এটিকেlive-ও বলা হয়): অর্ডারটি বুকে স্থির আছে, ফিল হয়নি। যেসব GTC অর্ডার সঙ্গে সঙ্গে মেলেনি তা থেকে আসে। ইনভেন্টরি অপরিবর্তিত থাকে; এখনও কোনো পরবর্তী পদক্ষেপ দরকার নেই।
সিদ্ধান্ত-নিয়ম: status যদি matched বা delayed হয়, তাহলে চেইন-ট্রান্সফার যাচাই না করা পর্যন্ত নতুন ইনভেন্টরির ওপর নির্ভরশীল কোনো পরবর্তী পদক্ষেপ নেবেন না।
পোলিং প্যাটার্ন: উদযাপনের আগে স্ট্যাটাস poll করুন
যাচাই প্যাটার্ন: সফল মিলের পর CTF ব্যালান্স poll করুন যতক্ষণ না নতুন টোকেন প্রতিফলিত হয়, তারপর এগোন।
def wait_for_settlement(token_id, expected_size, timeout=15):
"""Block until on-chain balance reaches expected_size or timeout."""
start = time.time()
while time.time() - start < timeout:
bal = ctf_contract.functions.balanceOf(PROXY, token_id).call()
if bal >= expected_size:
return True
time.sleep(0.5)
return False
সাধারণ নিষ্পত্তি: ভালো নেটওয়ার্ক অবস্থায় 2-5 সেকেন্ড, Polygon ভিড়ের সময় 15s পর্যন্ত। 5-সেকেন্ডের অপেক্ষা 95% ক্ষেত্র কভার করে; প্রোডাকশনে timeout 15s রাখুন এবং timeout হলে সতর্কবার্তা দিন।
যেসব উচ্চ-কম্পাঙ্কের বট ব্লক করতে পারে না, তাদের জন্য একটি বিকল্প হলো ইভেন্ট-সাবস্ক্রিপশন: আপনার প্রক্সি ঠিকানার জন্য CTF-এর TransferSingle ইভেন্ট নজরে রাখুন এবং রিসিপ্ট পাওয়ার পর পরবর্তী পদক্ষেপ ট্রিগার করুন। এতে কৌশল-লুপ ব্লক না হয়ে অপেক্ষা-সারিতে ঠেলে দেওয়া হয়।
ফ্যান্টম-ফিল প্রতিষেধক হিসেবে FOK
FAK-এর বদলে FOK বেছে নেওয়া ফ্যান্টম-ফিল বিশৃঙ্খলার বিরুদ্ধে আংশিক প্রতিরক্ষা। FOK হয় পুরো অর্ডার ফিল করে, নয়তো cancelled ফেরত দেয়; FAK আংশিক হলে filled_size ফেরত দিতে পারে। আংশিক ফিলের পর যদি মূল অর্ডার-আকার অনুযায়ী GTC সেল দেওয়া হয়, তাহলে নিষ্পত্তির-বিলম্ব আর আকারের-অমিল - এই দুই বাগ একসঙ্গে কাজ করে - আর সেল ব্যর্থ হয়।
FOK-এ আকার দ্বিমুখী: হয় পূর্ণ আকার মিলেছে, নয়তো একদমই মেলেনি। তাই পরবর্তী-বসানোর যুক্তি সবসময় জানে কী আশা করতে হবে।
এতে অপেক্ষার প্রয়োজন শেষ হয় না - নিখুঁত FOK মিলও 2-5 সেকেন্ডের নিষ্পত্তি-জানালার অধীন। তবে এটি হিসাব-বিভ্রান্তির একটি শ্রেণিকে সরিয়ে দেয়।
client_order_id দিয়ে idempotent রিট্রাই
অর্ডার বসানোর সময় নেটওয়ার্ক-ব্যর্থতা সবচেয়ে খারাপ পরিস্থিতি তৈরি করে: বটের HTTP কল timeout হয়েছে, কিন্তু অর্ডার পৌঁছেছে কি না বোঝা যাচ্ছে না। অন্ধভাবে রিট্রাই করলে ডাবল-বসানো হতে পারে; রিট্রাই না করলে পজিশন হারাতে পারেন।
সমাধান হলো অর্ডার-বসানোয় client_order_id ক্ষেত্র ব্যবহার করা। উদ্দিষ্ট প্রতিটি অর্ডারের জন্য একটি নির্ধারক UUID তৈরি করুন; সার্ভার যদি আগেই সেই ID দেখে থাকে, তাহলে ডুপ্লিকেট তৈরি না করে বিদ্যমান অর্ডারের স্ট্যাটাস ফেরত দেবে।
import uuid
oid = str(uuid.uuid4()) # generate once, retry with same value
for attempt in range(3):
try:
resp = c.create_and_post_order(args, OrderType.FOK, client_order_id=oid)
return resp
except (TimeoutError, ConnectionError):
time.sleep(0.5 * (2 ** attempt))
raise RuntimeError("post failed after 3 attempts")
প্যাটার্নটি হলো: আগে ID তৈরি করুন, পরিবহন-ব্যর্থতায় রিট্রাই করুন, যৌক্তিক প্রত্যাখ্যানে কখনও নয়। সার্ভার-সাইড ডিডুপ প্রতি-API-কি ভিত্তিক, প্রায় 5 মিনিট পর্যন্ত থাকে।
বাস্তব প্রোডাকশন ঘটনা: আমরা কীভাবে আমাদেরটা ঠিক করেছি
আমাদের নিজেদের প্রোডাকশন ডায়েরি থেকে, মে 2025। 60-মিনিটের জানালায় ট্রেডার বট 22টি বাই অর্ডার দিয়েছিল, সবগুলো মিলেছিল, কিন্তু মাত্র 14টি GTC সেল গৃহীত হয়েছিল। আটটি পজিশনের কোনো এক্সিট বসানো হয়নি।
মূল কারণ: বাই মিলের 800ms-এর মধ্যেই বট GTC সেল বসিয়েছিল, চেইন ERC-1155 ট্রান্সফার নিশ্চিত করার অনেক আগেই। CLOB "balance: 0" বার্তা দিয়ে প্রত্যাখ্যান করেছিল; বট ত্রুটি লগ করেছিল, কিন্তু রিট্রাই করেনি। আটটি পজিশন নীরবে নিষ্পত্তি পর্যন্ত ধরে ছিল, টেক-প্রফিট সুরক্ষা ছাড়াই। তিনটি টাকা-না-পাওয়া অবস্থায় বন্ধ হয়েছিল; একটি ভাগ্যক্রমে 0.99-এ বন্ধ হয়েছিল।
সমাধান হিসেবে একই টোকেনে কোনো বাই ফিল আর GTC বসানোর মধ্যে 5-সেকেন্ডের ব্লকিং অপেক্ষা যোগ করা হয়। 30টি পেপার ট্রেড এবং 30টি লাইভ ট্রেড দিয়ে যাচাই করা হয়েছে; তারপর থেকে শূন্য balance-শূন্য ত্রুটি।
শিক্ষা: জোরালো ত্রুটির চেয়ে নীরব ত্রুটির পথ বেশি ব্যয়বহুল। এরপর আমরা সব ফ্যান্টম-ফিল ত্রুটিতে Telegram সতর্কবার্তা ট্রিগার করেছি, যাতে ভবিষ্যতের বিচ্যুতি কয়েক সেকেন্ডের মধ্যেই দৃশ্যমান হয়।
কোড: শনাক্ত-করে-তবে-কাজ করার অর্ডার-পরবর্তী প্যাটার্ন
প্রোডাকশন বাই-তারপর-বসানো প্যাটার্ন।
def buy_then_post_tp(token_id, size, buy_price, tp_price):
# 1. Place FOK buy
buy_args = OrderArgs(token_id=token_id, price=buy_price, size=size, side="BUY")
buy_resp = c.create_and_post_order(buy_args, OrderType.FOK)
if buy_resp.status != "matched":
return {"ok": False, "stage": "buy", "reason": buy_resp.status}
# 2. Wait for on-chain settlement (5s sane default; bump for congestion)
if not wait_for_settlement(token_id, expected_size=size, timeout=15):
return {"ok": False, "stage": "settle", "reason": "timeout"}
# 3. Confirm minimum size for GTC
if size < 5:
# GTC won't accept; fall back to ride-to-resolve or FOK sell at TP later
return {"ok": True, "stage": "buy_only", "note": "size<5, no GTC posted"}
# 4. Post GTC sell
sell_args = OrderArgs(token_id=token_id, price=tp_price, size=size, side="SELL")
sell_resp = c.create_and_post_order(sell_args, OrderType.GTC)
return {"ok": sell_resp.status in ("posted","live"), "buy": buy_resp, "sell": sell_resp}
প্যাটার্নটি সাধারণ ব্যর্থতাগুলো থেকে বাঁচে: ফ্যান্টম-ফিল, ক্ষণস্থায়ী নেটওয়ার্ক-বিচ্ছেদ, ন্যূনতমের-নিচে GTC আকার। কৌশল-স্তরকে যথেষ্ট তথ্য দেয়, যাতে রিট্রাই, লগ, না সতর্কবার্তা - কোনটা করতে হবে তা ঠিক করা যায়।


