Polymarket Bot Tutorial · Chapter 12 of 32

Polymarket phantom fills কীভাবে detect করতে হয় (যে order filled দেখায় কিন্তু আসলে নয়), idempotent retries implement করতে হয়, status=matched আর rested-on-book-এর মধ্যে পার্থক্য করতে হয়, এবং transient failures থেকে কীভাবে টিকে থাকতে হয়।

এই chapter-এ কী covered হয়েছে

Phantom fill হলো Polymarket-specific এক ধরনের failure mode, যেখানে CLOB match acknowledge করে কিন্তু chain এখনও ERC-1155 transfer confirm করেনি। এরপর প্রায় ~5 seconds-এর মধ্যে আরেকটা order দিলে misleading "balance: 0" error দিয়ে reject হয়। এর সমাধান হলো idempotence আর settlement wait। এই chapter-এ production playbook আছে, যেটার জন্য আমরা বাস্তব টাকা খরচ করেছি।

  • Phantom fill কী
  • status=matched বনাম status=delayed বনাম status=posted
  • Polling pattern: celebrate করার আগে status poll করুন
  • FOK as anti-phantom-fill
  • client_order_id দিয়ে idempotent retries
  • Real production incident: আমরা কীভাবে ours fix করেছি
  • Code: detect-then-act post-order pattern

Phantom fill কী

Phantom fill হলো যখন CLOB API আপনার order-এর জবাবে status: "matched" দেয়, কিন্তু on-chain ERC-1155 transfer এখনও settle হয়নি। CLOB matcher Polygon block production-এর চেয়ে দ্রুত (~2s per block)। API match-এর পর প্রায় 2-5 seconds ধরে আপনার wallet on-chain এ সেই tokens ধরে না, যেগুলো matcher বলে আপনি own করেন।

Bot-এর bug তখনই প্রকাশ পায়, যখন follow-up action - সাধারণত take-profit post করার জন্য একটি GTC sell - ওই window-এর মধ্যে চালানো হয়। তখন CLOB chain balance check করে, zero দেখে, এবং not enough balance / allowance: balance: 0, order amount: N দিয়ে reject করে। Error message-এ allowance-কে দোষ দেওয়া হয়; আসল কারণ settlement lag।

প্রথমবার এটা ঘটলে আপনি allowance bug ধরে নিয়ে এক ঘণ্টা নষ্ট করবেন। সমাধান খুবই সহজ: wait করুন, verify করুন, তারপর post করুন।

status=matched বনাম status=delayed বনাম status=posted

Order placement response-এ একটি status field থাকে, যার তিনটি value গুরুত্বপূর্ণ।

  • matched: orderটি সঙ্গে সঙ্গে book-এর সাথে match হয়েছে। Inventory 2-5 seconds-এর মধ্যে settle হবে। সফল হলে FOK/FAK এটাই return করে।
  • delayed: matcher synchronousভাবে settle করতে পারেনি এবং match queue-তে রেখেছে। বিরল; সাধারণত congestion বোঝায়। wait + verify pattern-এর জন্য এটিকে matched-এর মতোই treat করুন।
  • posted (এটিকে live-ও বলা হয়): orderটি book-এ resting আছে, fill হয়নি। GTC order থেকে আসে যা সঙ্গে সঙ্গে match হয়নি। Inventory অপরিবর্তিত থাকে; এখনও কোনো follow-up action দরকার নেই।

Decision rule: status যদি matched বা delayed হয়, তাহলে chain transfer verify না করা পর্যন্ত নতুন inventory-র ওপর নির্ভর করে এমন কোনো follow-up দেবেন না।

Polling pattern: celebrate করার আগে status poll করুন

Verification pattern: successful match-এর পর CTF balance poll করুন যতক্ষণ না নতুন tokens reflect করে, তারপর এগোন।

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

সাধারণ settlement: ভালো network conditions-এ 2-5 seconds, Polygon congestion-এর সময় 15s পর্যন্ত। 5-second wait 95% case cover করে; production-এ timeout 15s রাখুন এবং timeout হলে alert দিন।

যেসব high-frequency bot block করতে পারে না, তাদের জন্য একটি alternative হলো event-subscription: আপনার proxy address-এর জন্য CTF-এর TransferSingle event watch করুন এবং receipt-এর পর downstream action trigger করুন। এতে strategy loop block না হয়ে wait queue-তে push হয়।

FOK as anti-phantom-fill

FAK-এর বদলে FOK বেছে নেওয়া phantom-fill chaos-এর বিরুদ্ধে আংশিক defense। FOK হয় পুরো order fill করে, নয়তো cancelled return করে; FAK partial হলে filled_size return করতে পারে। Partial fill-এর পর যদি original order size অনুযায়ী GTC sell দেওয়া হয়, তাহলে settlement-lag আর size mismatch - এই দুই bug একসাথে কাজ করে - আর sell fail হয়।

FOK-এ size binary: হয় full size match হয়েছে, নয়তো একদম হয়নি। তাই follow-up posting logic সবসময় জানে কী expect করতে হবে।

এতে wait-এর প্রয়োজন শেষ হয় না - perfect FOK match-ও 2-5 second settlement window-এর অধীন। তবে এটি bookkeeping divergence-এর একটি class সরিয়ে দেয়।

client_order_id দিয়ে idempotent retries

Order placement চলাকালীন network failure worst-case scenario তৈরি করে: bot-এর HTTP call timeout হয়েছে, কিন্তু order পৌঁছেছে কি না বোঝা যাচ্ছে না। অন্ধভাবে retry করলে double-place হতে পারে; retry না করলে position হারাতে পারেন।

সমাধান হলো order placement-এ client_order_id field ব্যবহার করা। Intended প্রতিটি order-এর জন্য deterministic UUID generate করুন; server যদি আগেই সেই ID দেখে থাকে, তাহলে duplicate তৈরি না করে existing order-এর status return করবে।

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")

Patternটি হলো: আগে ID generate করুন, transport failure-এ retry করুন, logical rejection-এ কখনও না। Server-side dedup প্রতি-API-key ভিত্তিক, প্রায় 5 minutes পর্যন্ত থাকে।

Real production incident: আমরা কীভাবে ours fix করেছি

আমাদের own production diary থেকে, মে 2025। 60-minute window-এ trader bot 22টি buy order দিয়েছিল, সবগুলো matched হয়েছিল, কিন্তু মাত্র 14টি GTC sell accepted হয়েছিল। আটটি position-এর কোনো exit post হয়নি।

Root cause: buy match-এর 800ms-এর মধ্যেই bot GTC sell post করেছিল, chain ERC-1155 transfer confirm করার অনেক আগেই। CLOB "balance: 0" message দিয়ে reject করেছিল; bot error log করেছিল, কিন্তু retry করেনি। আটটি position silently resolution পর্যন্ত ধরে ছিল, take-profit protection ছাড়াই। তিনটি out of the money-তে close হয়েছিল; একটি luck-এর কারণে 0.99-এ close হয়েছিল।

Fix হিসেবে একই token-এর ওপর যেকোনো buy fill আর GTC post-এর মাঝখানে 5-second blocking wait যোগ করা হয়। 30টি paper trade plus 30টি live trade দিয়ে verify করা হয়েছে; তারপর থেকে zero balance-zero errors।

Lesson: loud error-এর চেয়ে silent error path বেশি expensive। এরপর আমরা সব phantom-fill error-এ Telegram alert trigger করেছি, যাতে ভবিষ্যতের drift mode seconds-এর মধ্যে visible হয়।

Code: detect-then-act post-order pattern

Production buy-then-post pattern.

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}

Patternটি common failure modes থেকে বাঁচে: phantom-fill, transient network drop, under-minimum GTC size। Strategy layer-কে যথেষ্ট information দেয়, যাতে retry, log, বা alert - কোনটা করতে হবে তা ঠিক করা যায়।

প্রায়শই জিজ্ঞাসিত প্রশ্ন

Polymarket-এ phantom fill কী?
Phantom fill হলো যখন আপনার bot ভাবে order fill হয়েছে, কিন্তু exchange সেটিকে এখনও not yet filled (অথবা partially filled) হিসেবে record করে। এটা হয় যখন client code একটি HTTP 200 response-কে confirmation হিসেবে ধরে নেয়, অথচ response শুধু বলে orderটি matching engine-এ accepted হয়েছে। আমরা এটা কঠিনভাবে শিখেছি - আমাদের trader history-র commits 06deaef, 8bb7761, আর e68a087 ঠিক এই সমস্যাটাই fix করে।
Phantom fill কীভাবে avoid করব?
তিনটি rule: (1) Buy-এর জন্য FOK order ব্যবহার করুন - order হয় পুরোপুরি fill হবে, নয়তো পুরোপুরি gone, কখনও ambiguous নয়। (2) যেকোনো non-matched status-কে not filled ধরে নিন - status=matched বা amount_filled > 0 না হওয়া পর্যন্ত order status poll করুন। (3) Idempotence-এর জন্য client_order_id (V2-এ clientOrderId) ব্যবহার করুন, যাতে retry double-fill না করে।
status=delayed মানে কী?
Orderটি matching engine-এ আছে, কিন্তু এখনও fully matched হয়নি। এটা কয়েক সেকেন্ডের মধ্যে match হতে পারে, বা resting-ও থাকতে পারে। সবসময় poll করুন - যদি status 5-10 seconds-এর বেশি delayed থাকে এবং amount_filled 0 হয়, তাহলে এটিকে unfilled ধরে cancel করার কথা ভাবুন।
Double-filling ছাড়া safely retry কীভাবে করব?
প্রতিটি logical trade attempt-এর জন্য একটি unique client_order_id generate করুন এবং প্রতিটি retry-তে সেটিই pass করুন। Exchange client_order_id অনুযায়ী dedupe করে, তাই একই id-সহ retried order duplicate হিসেবে reject হয়, আবার place হয় না। Implementations: Python OrderArgs.client_order_id, Node CreateOrderOptions.clientOrderId.
Order endpoint-এর 200 OK response কি বিশ্বাস করা যায়?
না - 200 OK শুধু মানে "আপনার request matching engine গ্রহণ করেছে," "আপনার order fill হয়েছে" নয়। Submission-এর পরে orderId দিয়ে order status poll করতে হবে, এবং শুধু status=matched (অথবা amount_filled > 0) কেই real fill হিসেবে ধরতে হবে।
Order পাঠানোর পর আর response দেখার আগে bot crash হলে কী করব?
Restart-এর পর SDK দিয়ে open orders আর recent fills query করুন। Local diary/state-এর সঙ্গে reconcile করুন - যদি T সময়ে order পাঠিয়ে থাকেন কিন্তু কোনো record না থাকে, তাহলে T-এর পরের orders query করুন এবং client_order_id দিয়ে match করুন। এখনও missing থাকলে order matching engine-এ পৌঁছায়নি, তাই safely re-send করা যাবে।