آموزش Polymarket Bot · فصل ۱۲ از ۳۲
چطور phantom fillهای Polymarket را تشخیص دهید (سفارشهایی که ظاهراً filled شدهاند اما نشدهاند)، retryهای idempotent را پیادهسازی کنید، status=matched را از rested-on-book تشخیص دهید، و از failureهای موقت جان سالم به در ببرید.
این فصل چه چیزهایی را پوشش میدهد
phantom fill یک failure mode مخصوص Polymarket است که در آن CLOB یک match را تأیید میکند، اما chain هنوز transfer مربوط به ERC-1155 را confirm نکرده است. یک order بعدی در حدود ~۵ ثانیه بعد با یک error گمراهکنندهی "balance: 0" رد میشود. درمان، idempotence و یک settlement wait است. این فصل، playbook عملیاتیای است که برایش پول واقعی پرداخت کردیم.
این فصل ۱۲ از سری ۳۲ بخشی ما درباره ساختن یک Polymarket trading bot است. موضوع را در بخشهای زیر بهصورت عمیق پوشش میدهیم. محتوای اصلی هر بخش در حال نوشتهشدن و انتشار مرحلهبهمرحله است؛ پاسخهای FAQ و منابع از قبل کامل شدهاند و تجربهی production حاصل از اجرای trader خودمان را منعکس میکنند.
- phantom fill چیست
- status=matched در برابر status=delayed در برابر status=posted
- Polling pattern: قبل از خوشحالی، status را poll کنید
- FOK بهعنوان anti-phantom-fill
- Idempotent retries با client_order_id
- یک incident واقعی در production: چطور مشکل خودمان را حل کردیم
- Code: detect-then-act post-order pattern
phantom fill چیست
phantom fill زمانی است که CLOB API به order شما با status: "matched" پاسخ میدهد، اما transfer on-chain مربوط به ERC-1155 هنوز settle نشده است. matcher در CLOB از تولید block در Polygon سریعتر است (~۲ ثانیه برای هر block). حدود ۲ تا ۵ ثانیه پس از API match، wallet شما هنوز on-chain توکنهایی را که matcher میگوید مالک آنها هستید، در اختیار ندارد.
bug ربات وقتی ظاهر میشود که یک اقدام بعدی - معمولاً یک GTC sell برای ثبت take-profit - در همان بازه اجرا شود. CLOB موجودی chain را check میکند، صفر میبیند، و با not enough balance / allowance: balance: 0, order amount: N رد میکند. پیام error تقصیر را گردن allowance میاندازد؛ علت، settlement lag است.
اولینبار که این اتفاق میافتد، فکر میکنید مشکل allowance است و یک ساعت وقت تلف میکنید. درمان ساده است: صبر کنید، verify کنید، سپس post کنید.
status=matched در برابر status=delayed در برابر status=posted
پاسخ placement order شامل یک status field است که سه مقدار مهم دارد.
matched: order فوراً با book match شده است. inventory ظرف ۲ تا ۵ ثانیه settle میشود. این همان چیزی است که FOK/FAK در صورت موفقیت برمیگردانند.delayed: matcher نتوانسته settlement را بهصورت synchronous انجام دهد و match را queue کرده است. نادر است؛ معمولاً نشانه congestion است. برای patternِ wait + verify مثلmatchedبا آن رفتار کنید.posted(کهliveهم نامیده میشود): order روی book rest شده و filled نشده است. این وضعیت توسط GTC orderهایی برمیگردد که فوراً match نشدهاند. inventory تحت تأثیر قرار نمیگیرد؛ هنوز اقدام بعدی لازم نیست.
قانون تصمیمگیری: اگر status برابر matched یا delayed است، تا زمانی که chain transfer را verify نکردهاید، هیچ follow-upای که به inventory جدید نیاز دارد را place نکنید.
Polling pattern: قبل از خوشحالی status را poll کنید
pattern verify: بعد از یک match موفق، CTF balance را 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
Settlement معمول: در شرایط خوب شبکه ۲ تا ۵ ثانیه، و هنگام congestion در Polygon تا ۱۵ ثانیه. یک wait پنجثانیهای ۹۵٪ موارد را پوشش میدهد؛ برای production timeout را ۱۵ ثانیه بگذارید و روی timeout alert بدهید.
برای botهای high-frequency که نمیتوانند block شوند، یک alternative این است که از event-subscription استفاده کنید: event TransferSingle در CTF را برای proxy address خود watch کنید و به محض دریافت، actionهای downstream را trigger کنید. این کار wait را به یک queue منتقل میکند بهجای اینکه loop استراتژی را block کند.
FOK بهعنوان anti-phantom-fill
انتخاب FOK بهجای FAK یک دفاع نسبی در برابر chaos ناشی از phantom fill است. FOK یا کل order را filled میکند یا cancelled برمیگرداند؛ FAK میتواند filled_size جزئی برگرداند. وقتی یک partial fill با یک GTC sell به اندازهی order اصلی دنبال شود، sell بهخاطر settlement lag بهعلاوه mismatch اندازه fail میشود - دو bug که همزمان روی هم اثر میگذارند.
با FOK، اندازه binary است: یا کل size match شده یا هیچچیز. منطق posting بعدی همیشه میداند باید چه انتظاری داشته باشد.
این کار نیاز به wait را از بین نمیبرد - حتی یک FOK match کامل هم در معرض window settlement ۲ تا ۵ ثانیهای است. اما یک class از divergence در bookkeeping را حذف میکند.
Idempotent retries با client_order_id
failureهای شبکه هنگام placement order بدترین سناریو را ایجاد میکنند: call HTTP ربات timeout شده، اما order ممکن است received شده باشد یا نباشد. retry کردنِ کورکورانه میتواند order را دو بار place کند؛ retry نکردن میتواند یک position را از دست بدهد.
راهحل، field client_order_id در placement order است. برای هر order موردنظر یک UUID deterministic تولید کنید؛ اگر server قبلاً آن ID را دیده باشد، status order موجود را برمیگرداند بهجای اینکه duplicate بسازد.
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 را تولید کنید، روی transport failure retry کنید، و هرگز روی logical rejection retry نکنید. dedup سمت server برای هر API key اعمال میشود و حدود ۵ دقیقه دوام دارد.
یک incident واقعی در production: چطور مشکل خودمان را حل کردیم
از دفترچه production خودمان، مه 2025. یک بازه ۶۰ دقیقهای که bot معاملهگر ۲۲ خرید انجام داد، همه matched شدند، اما فقط ۱۴ فروش GTC پذیرفته شد. هشت position هیچ exitای نداشتند.
علت ریشهای: bot فروش GTC را فقط ۸۰۰ میلیثانیه بعد از buy match ارسال میکرد، خیلی قبل از اینکه chain انتقال ERC-1155 را confirm کند. CLOB با پیام "balance: 0" رد میکرد؛ bot error را log میکرد اما retry نمیکرد. هشت position بدون حفاظت take-profit تا زمان resolution پیش رفتند. سه مورد با ضرر بسته شدند؛ یکی هم از شانس با 0.99 بسته شد.
راهحل بهصورت یک wait بلاککنندهی ۵ ثانیهای بین هر buy fill و هر GTC post روی همان token منتشر شد. با ۳۰ paper trade بهعلاوه ۳۰ live trade verify شد؛ از آن زمان هیچ balance-zero errorی نداشتهایم.
درس: یک مسیر error خاموش از یک مسیر error پرصدا گرانتر است. بعد از این، همه phantom-fill errorها را مجبور کردیم یک Telegram alert trigger کنند تا هر drift mode آینده ظرف چند ثانیه قابل مشاهده باشد.
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 در برابر failure modeهای رایج دوام میآورد: phantom-fill، قطع موقت شبکه، و اندازهی GTC کمتر از حداقل. اطلاعات کافی برمیگرداند تا لایه استراتژی بتواند تصمیم بگیرد چه چیزی retry شود، چه چیزی log شود، و چه چیزی alert بگیرد.














