পলিমার্কেট বট টিউটোরিয়াল ·

পলিমার্কেট ফ্যান্টম ফিল কীভাবে শনাক্ত করবেন (যেসব অর্ডার ফিল হয়েছে দেখায় কিন্তু আসলে হয়নি), 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 আকার। কৌশল-স্তরকে যথেষ্ট তথ্য দেয়, যাতে রিট্রাই, লগ, না সতর্কবার্তা - কোনটা করতে হবে তা ঠিক করা যায়।

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

পলিমার্কেটে ফ্যান্টম ফিল কী?
ফ্যান্টম ফিল হলো যখন আপনার বট মনে করে অর্ডার ফিল হয়েছে, কিন্তু এক্সচেঞ্জ সেটিকে এখনও ফিল-হয়নি (বা আংশিক ফিল) হিসেবে রেকর্ড করে। এটা ঘটে যখন ক্লায়েন্ট কোড একটি HTTP 200 প্রতিক্রিয়াকে নিশ্চিতকরণ ধরে নেয়, অথচ প্রতিক্রিয়া কেবল বলে যে অর্ডারটি মিলকরণ ইঞ্জিনে গৃহীত হয়েছে। আমরা এটা কঠিন পথে শিখেছি - আমাদের ট্রেডার ইতিহাসের কমিট 06deaef, 8bb7761, এবং e68a087 ঠিক এটাই সমাধান করে।
ফ্যান্টম ফিল কীভাবে এড়াব?
তিনটি নিয়ম: (1) কেনার জন্য FOK অর্ডার ব্যবহার করুন - অর্ডার হয় সম্পূর্ণ ফিল হবে, নয়তো সম্পূর্ণ চলে যাবে, কখনও দ্ব্যর্থক নয়। (2) যেকোনো non-matched স্ট্যাটাসকে ফিল-হয়নি ধরুন - status=matched অথবা amount_filled > 0 না হওয়া পর্যন্ত অর্ডার-স্ট্যাটাস poll করুন। (3) idempotence-এর জন্য একটি client_order_id (V2-তে clientOrderId) ব্যবহার করুন, যাতে রিট্রাই ডাবল-ফিল না করে।
status=delayed মানে কী?
অর্ডারটি মিলকরণ ইঞ্জিনে আছে, কিন্তু এখনও সম্পূর্ণ মিলকৃত হয়নি। এটা কয়েক সেকেন্ডের মধ্যে মিলতে পারে, বা স্থির থাকতে পারে। সবসময় poll করুন - যদি স্ট্যাটাস 5-10 সেকেন্ডের বেশি delayed থাকে এবং amount_filled 0 হয়, তাহলে এটিকে ফিল-হয়নি ধরে বাতিলের কথা ভাবুন।
ডাবল-ফিল ছাড়া নিরাপদে কীভাবে রিট্রাই করব?
প্রতিটি যৌক্তিক ট্রেড-চেষ্টার জন্য একটি অনন্য client_order_id তৈরি করুন এবং প্রতিটি রিট্রাইয়ে সেটিই পাস করুন। এক্সচেঞ্জ client_order_id অনুযায়ী ডিডুপ করে, তাই একই id-সহ রিট্রাই করা অর্ডার ডুপ্লিকেট হিসেবে প্রত্যাখ্যাত হয়, আবার বসানো হয় না। বাস্তবায়ন: Python OrderArgs.client_order_id, Node CreateOrderOptions.clientOrderId।
অর্ডার এন্ডপয়েন্টের 200 OK প্রতিক্রিয়া কি বিশ্বাস করা যায়?
না - 200 OK কেবল মানে "আপনার অনুরোধ মিলকরণ ইঞ্জিন গ্রহণ করেছে," "আপনার অর্ডার ফিল হয়েছে" নয়। জমা দেওয়ার পরে orderId দিয়ে অর্ডার-স্ট্যাটাস poll করতে হবে, এবং কেবল status=matched (অথবা amount_filled > 0)-কেই বাস্তব ফিল হিসেবে ধরতে হবে।
অর্ডার পাঠানোর পর প্রতিক্রিয়া দেখার আগে বট ক্র্যাশ করলে কী করব?
রিস্টার্টের পর SDK দিয়ে খোলা অর্ডার ও সাম্প্রতিক ফিল কোয়েরি করুন। স্থানীয় ডায়েরি/অবস্থার সঙ্গে মিলিয়ে নিন - যদি T সময়ে অর্ডার পাঠিয়ে থাকেন কিন্তু কোনো রেকর্ড না থাকে, তাহলে T-এর পরের অর্ডার কোয়েরি করে client_order_id দিয়ে মেলান। এখনও অনুপস্থিত থাকলে অর্ডার মিলকরণ ইঞ্জিনে পৌঁছায়নি এবং আপনি নিরাপদে আবার পাঠাতে পারেন।