آموزش ربات پلیمارکت ·
چطور پرشدنهای فانتوم پلیمارکت را تشخیص دهید (سفارشهایی که ظاهراً پر شدهاند اما نشدهاند)، تلاشهای مجدد idempotent را پیادهسازی کنید، status=matched را از وضعیت باقیمانده روی دفتر (rested-on-book) تشخیص دهید، و از شکستهای گذرا جان سالم به در ببرید.
این فصل چه چیزهایی را پوشش میدهد
پرشدن فانتوم یک حالت شکست مخصوص پلیمارکت است که در آن CLOB یک تطبیق را تأیید میکند، اما زنجیره (chain) هنوز انتقال مربوط به ERC-1155 را تأیید نکرده است. یک سفارش بعدی در حدود ~۵ ثانیه بعد با یک خطای گمراهکنندهی "balance: 0" رد میشود. درمان، idempotence و یک انتظار برای تسویه (settlement) است. این فصل، راهنمای عملیاتیای است که برایش پول واقعی پرداخت کردیم.
این فصل ۱۲ از سری ۳۲ بخشی ما درباره ساختن یک ربات معاملهگری پلیمارکت است. موضوع را در بخشهای زیر بهصورت عمیق پوشش میدهیم. محتوای اصلی هر بخش در حال نوشتهشدن و انتشار مرحلهبهمرحله است؛ پاسخهای پرسشهای متداول و منابع از قبل کامل شدهاند و تجربهی production حاصل از اجرای معاملهگر خودمان را منعکس میکنند.
- پرشدن فانتوم چیست
- status=matched در برابر status=delayed در برابر status=posted
- الگوی Polling: قبل از خوشحالی، status را poll کنید
- FOK بهعنوان سازوکار ضدِ پرشدن فانتوم
- تلاشهای مجدد idempotent با client_order_id
- یک رخداد واقعی در production: چطور مشکل خودمان را حل کردیم
- کد: الگوی detect-then-act پس از ثبت سفارش
پرشدن فانتوم چیست
پرشدن فانتوم زمانی است که CLOB API به سفارش شما با status: "matched" پاسخ میدهد، اما انتقال on-chain مربوط به ERC-1155 هنوز تسویه نشده است. تطبیقدهنده در CLOB از تولید بلاک در Polygon سریعتر است (~۲ ثانیه برای هر بلاک). حدود ۲ تا ۵ ثانیه پس از تطبیق در API، کیف پول شما هنوز on-chain توکنهایی را که تطبیقدهنده میگوید مالک آنها هستید، در اختیار ندارد.
باگِ ربات وقتی ظاهر میشود که یک اقدام بعدی - معمولاً یک فروش GTC برای ثبت حد سود - در همان بازه اجرا شود. CLOB موجودیِ زنجیره را بررسی میکند، صفر میبیند، و با not enough balance / allowance: balance: 0, order amount: N رد میکند. پیام خطا تقصیر را گردن allowance میاندازد؛ علت، تأخیر تسویه (settlement lag) است.
اولینبار که این اتفاق میافتد، فکر میکنید مشکل allowance است و یک ساعت وقت تلف میکنید. درمان ساده است: صبر کنید، تأیید کنید، سپس ثبت کنید.
status=matched در برابر status=delayed در برابر status=posted
پاسخِ ثبت سفارش شامل یک فیلد status است که سه مقدار مهم دارد.
matched: سفارش فوراً با دفتر تطبیق داده شده است. موجودی ظرف ۲ تا ۵ ثانیه تسویه میشود. این همان چیزی است که FOK/FAK در صورت موفقیت برمیگردانند.delayed: تطبیقدهنده نتوانسته تسویه را بهصورت همگام (synchronous) انجام دهد و تطبیق را در صف قرار داده است. نادر است؛ معمولاً نشانهٔ ازدحام است. برای الگوی «انتظار + تأیید» مثلmatchedبا آن رفتار کنید.posted(کهliveهم نامیده میشود): سفارش روی دفتر باقی مانده و پر نشده است. این وضعیت توسط سفارشهای GTC برمیگردد که فوراً تطبیق نیافتهاند. موجودی تحت تأثیر قرار نمیگیرد؛ هنوز اقدام بعدی لازم نیست.
قانون تصمیمگیری: اگر status برابر matched یا delayed است، تا زمانی که انتقال روی زنجیره را تأیید نکردهاید، هیچ اقدام بعدیای که به موجودی جدید نیاز دارد را ثبت نکنید.
الگوی Polling: قبل از خوشحالی status را 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
تسویهٔ معمول: در شرایط خوب شبکه ۲ تا ۵ ثانیه، و هنگام ازدحام در Polygon تا ۱۵ ثانیه. یک انتظار پنجثانیهای ۹۵٪ موارد را پوشش میدهد؛ برای production مقدار timeout را ۱۵ ثانیه بگذارید و هنگام timeout هشدار بدهید.
برای رباتهای پربسامد که نمیتوانند بلاک شوند، یک جایگزین این است که از اشتراک رویداد استفاده کنید: رویداد TransferSingle در CTF را برای آدرس پراکسی خود رصد کنید و به محض دریافت، اقدامهای پاییندستی را فعال کنید. این کار انتظار را به یک صف منتقل میکند بهجای اینکه حلقهٔ استراتژی را بلاک کند.
FOK بهعنوان سازوکار ضدِ پرشدن فانتوم
انتخاب FOK بهجای FAK یک دفاع نسبی در برابر آشوبِ ناشی از پرشدن فانتوم است. FOK یا کل سفارش را پر میکند یا cancelled برمیگرداند؛ FAK میتواند filled_size جزئی برگرداند. وقتی یک پرشدن جزئی با یک فروش GTC به اندازهٔ سفارش اصلی دنبال شود، فروش بهخاطر تأخیر تسویه بهعلاوهٔ ناهمخوانی اندازه شکست میخورد - دو باگ که همزمان روی هم اثر میگذارند.
با FOK، اندازه دودویی است: یا کل اندازه تطبیق شده یا هیچچیز. منطق ثبتِ بعدی همیشه میداند باید چه انتظاری داشته باشد.
این کار نیاز به انتظار را از بین نمیبرد - حتی یک تطبیق کامل FOK هم در معرض پنجرهٔ تسویهٔ ۲ تا ۵ ثانیهای است. اما یک دسته از واگرایی در دفترداری را حذف میکند.
تلاشهای مجدد idempotent با client_order_id
شکستهای شبکه هنگام ثبت سفارش بدترین سناریو را ایجاد میکنند: فراخوانی HTTP ربات تایماوت شده، اما ممکن است سفارش دریافت شده باشد یا نباشد. تلاش مجددِ کورکورانه میتواند سفارش را دو بار ثبت کند؛ تلاشنکردن مجدد میتواند یک موقعیت را از دست بدهد.
راهحل، فیلد client_order_id در ثبت سفارش است. برای هر سفارش موردنظر یک UUID قطعی تولید کنید؛ اگر سرور قبلاً آن 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 را تولید کنید، روی شکست انتقال تلاش مجدد کنید، و هرگز روی رد منطقی تلاش مجدد نکنید. حذف موارد تکراری سمت سرور برای هر API key اعمال میشود و حدود ۵ دقیقه دوام دارد.
یک رخداد واقعی در production: چطور مشکل خودمان را حل کردیم
از دفترچهٔ production خودمان، مه 2025. یک بازهٔ ۶۰ دقیقهای که ربات معاملهگر ۲۲ خرید انجام داد، همه matched شدند، اما فقط ۱۴ فروش GTC پذیرفته شد. هشت موقعیت هیچ خروجیای نداشتند.
علت ریشهای: ربات فروش GTC را فقط ۸۰۰ میلیثانیه بعد از تطبیق خرید ارسال میکرد، خیلی قبل از اینکه زنجیره انتقال ERC-1155 را تأیید کند. CLOB با پیام "balance: 0" رد میکرد؛ ربات خطا را لاگ میکرد اما تلاش مجدد نمیکرد. هشت موقعیت بدون حفاظت حد سود تا زمان حلوفصل پیش رفتند. سه مورد با ضرر بسته شدند؛ یکی هم از شانس با 0.99 بسته شد.
راهحل بهصورت یک انتظارِ بلاککنندهی ۵ ثانیهای بین هر پرشدن خرید و هر ثبت GTC روی همان توکن منتشر شد. با ۳۰ معاملهٔ کاغذی بهعلاوهٔ ۳۰ معاملهٔ زنده تأیید شد؛ از آن زمان هیچ خطای balance-zero نداشتهایم.
درس: یک مسیر خطای خاموش از یک مسیر خطای پرصدا گرانتر است. بعد از این، همهٔ خطاهای پرشدن فانتوم را مجبور کردیم یک هشدار Telegram فعال کنند تا هر حالت انحراف آینده ظرف چند ثانیه قابل مشاهده باشد.
کد: الگوی detect-then-act پس از ثبت سفارش
الگوی «خرید سپس ثبت» در production.
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 کمتر از حداقل. اطلاعات کافی برمیگرداند تا لایهٔ استراتژی بتواند تصمیم بگیرد چه چیزی تلاش مجدد شود، چه چیزی لاگ شود، و چه چیزی هشدار بگیرد.


