Polymarket Bot Tutorial · الفصل 12 من 32

كيف تكتشف phantom fills في Polymarket (الطلبات التي تبدو منفذة لكنها ليست كذلك)، وتنفّذ retries idempotent، وتفرّق بين status=matched و rested-on-book، وتنجو من الأعطال المؤقتة.

ما الذي يغطيه هذا الفصل

phantom fill هو نمط فشل خاص بـ Polymarket حيث يقرّ CLOB بالمطابقة لكن السلسلة لم تؤكد بعد نقل ERC-1155. أي أمر متابعة خلال نحو 5 ثوانٍ يُرفض برسالة مضللة "balance: 0". العلاج هو idempotence ووقت انتظار للتسوية. هذا الفصل هو دليل التشغيل الذي دفعنا ثمنه بمال حقيقي.

  • ما هو phantom fill
  • status=matched مقابل status=delayed مقابل status=posted
  • نمط polling: تحقّق من status قبل الاحتفال
  • FOK كآلية مضادة لـ phantom-fill
  • Retries idempotent باستخدام client_order_id
  • حادثة تشغيلية حقيقية: كيف أصلحنا مشكلتنا
  • الكود: نمط detect-then-act بعد الأمر

ما هو phantom fill

phantom fill يحدث عندما يردّ CLOB API على طلبك بـ status: "matched" لكن عملية نقل ERC-1155 على السلسلة لم تُسوَّ بعد. matcher في CLOB أسرع من إنتاج كتل Polygon (نحو 2 ثانية لكل كتلة). لمدة تقارب 2-5 ثوانٍ بعد مطابقة الـ API، لا تكون محفظتك على السلسلة حائزة بعد على الرموز التي يقول matcher إنك تملكها.

يظهر خلل bot عندما يحدث إجراء متابعة - غالبًا بيع 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: لم يتمكّن matcher من التسوية بشكل متزامن ووضع المطابقة في queue. هذا نادر؛ ويشير عادةً إلى ازدحام. تعامل معه مثل matched لأغراض نمط الانتظار + التحقق.
  • posted (ويُسمّى أيضًا live): الأمر معروض على الدفتر دون تنفيذ. تعيده أوامر GTC التي لم تُطابق فورًا. المخزون لا يتأثر؛ ولا حاجة بعد لإجراء متابعة.

قاعدة القرار: إذا كان status هو matched أو delayed، فلا تضع أي إجراء متابعة يحتاج إلى المخزون الجديد حتى تتحقق من نقل السلسلة.

نمط polling: تحقّق من status قبل الاحتفال

نمط التحقق: بعد نجاح المطابقة، راقب رصيد CTF حتى يعكس الرموز الجديدة، ثم تابع.

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 ثوانٍ في ظروف الشبكة الجيدة، وقد تصل إلى 15 ثانية أثناء ازدحام Polygon. انتظار 5 ثوانٍ يغطي 95% من الحالات؛ وفي الإنتاج اجعل timeout = 15 ثانية وأصدر alert عند انتهاء المهلة.

بالنسبة إلى bots عالية التردد التي لا تستطيع تحمل الحجب، يوجد بديل هو event-subscription: راقب حدث TransferSingle الخاص بـ CTF لعنوان proxy الخاص بك وفعّل الإجراءات اللاحقة عند الاستلام. هذا ينقل الانتظار إلى queue بدلًا من حجب حلقة الاستراتيجية.

FOK كآلية مضادة لـ phantom-fill

اختيار FOK بدل FAK هو دفاع جزئي ضد فوضى phantom-fill. FOK إما ينفّذ الأمر كاملًا أو يعيد cancelled؛ أما FAK فيمكن أن يعيد filled_size جزئيًا. عندما يتبع fill جزئي بيع GTC بحجم يساوي الطلب الأصلي، يفشل البيع بسبب تأخر التسوية بالإضافة إلى عدم تطابق الحجم - وهما خطآن يتراكبان.

مع FOK يكون الحجم ثنائيًا: إما أن الحجم الكامل قد طُابق أو لم يحدث شيء. منطق posting اللاحق يعرف دائمًا ما الذي يتوقعه.

هذا لا يلغي الحاجة إلى الانتظار - حتى مطابقة FOK المثالية تخضع لنافذة التسوية البالغة 2-5 ثوانٍ. لكنه يزيل فئة واحدة من انحرافات المحاسبة.

Retries idempotent باستخدام client_order_id

تخلق الأعطال الشبكية أثناء تنفيذ الأمر أسوأ سيناريو: مهلة في طلب HTTP الخاص بـ bot، لكن الأمر ربما وصل أو ربما لم يصل. إعادة المحاولة بشكل ساذج قد تؤدي إلى تنفيذ مزدوج؛ وعدم إعادة المحاولة قد يضيّع مركزًا.

الحل هو الحقل client_order_id في وضع الأمر. أنشئ UUID deterministic لكل أمر مقصود؛ فإذا كان الخادم قد رأى ذلك الـ ID من قبل، فإنه يعيد status الخاص بالأمر الموجود بدلًا من إنشاء نسخة مكررة.

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 أولًا، أعد المحاولة عند فشل النقل، ولا تُعد المحاولة عند الرفض المنطقي. dedup على مستوى الخادم يكون لكل API key ولمدة تقارب 5 دقائق.

حادثة تشغيلية حقيقية: كيف أصلحنا مشكلتنا

من يوميات الإنتاج الخاصة بنا، مايو 2025. نافذة مدتها 60 دقيقة وضع فيها trader bot 22 أمر شراء، وكلها طُبقت، لكن لم يُقبل سوى 14 أمر بيع GTC. ثمانية مراكز لم يكن لها exit منشور.

السبب الجذري: نشر bot أمر بيع GTC خلال 800 مللي ثانية من مطابقة الشراء، أي قبل أن تؤكد السلسلة نقل ERC-1155. رفض CLOB الطلب برسالة "balance: 0"؛ سجّل bot الخطأ لكنه لم يُعد المحاولة. ثمانية مراكز ركبَت بهدوء حتى التسوية دون حماية take-profit. ثلاثة أُغلقت خارج المال؛ وواحد أُغلق عند 0.99 بالصدفة.

تم شحن الإصلاح على هيئة انتظار blocking لمدة 5 ثوانٍ بين أي fill شراء وأي نشر GTC على الرمز نفسه. تم التحقق منه عبر 30 paper trades بالإضافة إلى 30 live trades؛ ومنذ ذلك الحين لم تظهر أي أخطاء balance-zero.

الدرس: مسار خطأ صامت أغلى من مسار خطأ صاخب. بعد ذلك جعلنا جميع أخطاء phantom-fill تُطلق تنبيه Telegram، بحيث يصبح أي drift مستقبلي مرئيًا خلال ثوانٍ.

الكود: نمط detect-then-act بعد الأمر

نمط buy-then-post في الإنتاج.

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}

هذا النمط يصمد أمام أوضاع الفشل الشائعة: phantom-fill، انقطاع الشبكة المؤقت، وحجم GTC الأقل من الحد الأدنى. ويعيد معلومات كافية لكي تقرر طبقة الاستراتيجية ما الذي يجب إعادة المحاولة فيه مقابل ما الذي يجب تسجيله أو إطلاق alert بشأنه.

الأسئلة الشائعة

ما هو phantom fill في Polymarket؟
phantom fill هو عندما يعتقد bot الخاص بك أن أمرًا ما تم تنفيذه بينما يسجله البورصة على أنه لم يُنفذ بعد (أو نُفذ جزئيًا). يحدث هذا عندما يعامل كود العميل استجابة HTTP 200 على أنها تأكيد، بينما تعني الاستجابة فقط أن الأمر قُبل داخل matching engine. تعلمنا هذا بالطريقة الصعبة - commits 06deaef و8bb7761 وe68a087 في سجل trader الخاص بنا تصلح هذا بالضبط.
كيف أتجنب phantom fills؟
ثلاث قواعد: (1) استخدم أوامر FOK للشراء - فالأمر إما يُنفّذ بالكامل أو يختفي بالكامل، ولا يكون غامضًا أبدًا. (2) تعامل مع أي status غير matched على أنه غير منفذ - راقب status الأمر حتى status=matched أو amount_filled > 0. (3) استخدم client_order_id (clientOrderId في V2) لتحقيق idempotence كي لا تؤدي retries إلى تنفيذ مزدوج.
ماذا يعني status=delayed؟
يعني أن الأمر داخل matching engine لكنه لم يُطابق بالكامل بعد. قد يُطابق خلال ثوانٍ أو قد يُعرض على الدفتر. راقب دائمًا - إذا بقي status = delayed لأكثر من 5-10 ثوانٍ وكان amount_filled يساوي 0، فتعامل معه على أنه غير منفذ وفكّر في إلغائه.
كيف أعيد المحاولة بأمان دون تنفيذ مزدوج؟
أنشئ client_order_id فريدًا لكل محاولة تداول منطقية ومرره في كل retry. تقوم البورصة بعمل dedupe بواسطة client_order_id، لذا فإن الأمر المُعاد بنفس الـ id يُرفض باعتباره مكررًا بدلًا من تنفيذه مرة أخرى. أمثلة التنفيذ: Python OrderArgs.client_order_id، وNode CreateOrderOptions.clientOrderId.
هل يمكنني الوثوق باستجابة 200 OK من endpoint الخاص بالأمر؟
لا - 200 OK تعني فقط "تم قبول طلبك بواسطة matching engine"، وليس "تم تنفيذ أمرك". يجب أن تراقب status الأمر عبر orderId بعد الإرسال، وألا تعتبر status=matched فقط - أو amount_filled > 0 - تنفيذًا حقيقيًا.
ماذا لو تعطل bot الخاص بي بين إرسال الأمر ورؤية الاستجابة؟
عند إعادة التشغيل، استعلم عن الأوامر المفتوحة والتنفيذات الحديثة عبر SDK. طابِق ذلك مع السجل/الحالة المحلية لديك - إذا أرسلت أمرًا عند الوقت T لكن لا يوجد سجل له، فاستعلم عن الأوامر منذ T وطابقها بواسطة client_order_id. إذا بقي مفقودًا، فهذا يعني أن الأمر لم يصل أصلًا إلى matching engine ويمكنك إعادة إرساله بأمان.