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 بشأنه.





