Polymarket Bot Tutorial · Глава 12 из 32
Как обнаруживать phantom fills на Polymarket (ордера, которые выглядят как исполненные, но на самом деле нет), реализовывать idempotent retries, отличать status=matched от rested-on-book и переживать transient failures.
Что рассматривает эта глава
Phantom fill - это специфичный для Polymarket failure mode, при котором CLOB подтверждает match, но chain еще не подтвердил ERC-1155 transfer. Следующий order в течение примерно 5 секунд отклоняется с вводящей в заблуждение ошибкой "balance: 0". Решение - idempotence и ожидание settlement. Эта глава - production playbook, за который мы заплатили реальными деньгами.
- Что такое phantom fill
- status=matched vs status=delayed vs status=posted
- Polling pattern: poll status before celebrating
- FOK как anti-phantom-fill
- Idempotent retries с client_order_id
- Реальный production incident: как мы это исправили
- Code: detect-then-act post-order pattern
Что такое phantom fill
Phantom fill - это когда CLOB API отвечает на ваш order со status: "matched", но on-chain ERC-1155 transfer еще не settled. CLOB matcher работает быстрее, чем производство блоков Polygon (~2s на блок). В течение примерно 2-5 секунд после API match ваш wallet on-chain еще не хранит tokens, которые, как утверждает matcher, принадлежат вам.
Баг в bot проявляется, когда следующий шаг - обычно GTC sell для выставления take-profit - запускается в этом окне. CLOB проверяет chain balance, видит ноль и отклоняет с not enough balance / allowance: balance: 0, order amount: N. Сообщение об ошибке обвиняет allowance; реальная причина - settlement lag.
В первый раз, когда это случается, вы думаете, что проблема в allowance, и теряете час. Решение простое: подождать, проверить, затем выставить.
status=matched vs status=delayed vs status=posted
Ответ на размещение order включает поле status с тремя важными значениями.
matched: order немедленно matched against the book. Inventory settles через 2-5 seconds. Именно это возвращают FOK/FAK при успехе.delayed: matcher не смог синхронно выполнить settlement и поставил match в очередь. Редко; обычно указывает на congestion. Для wait + verify pattern относитесь к нему так же, как кmatched.posted(также называетсяlive): order resting on the book без исполнения. Возвращается GTC orders, которые не matched немедленно. Inventory не затронут; последующие действия пока не нужны.
Правило принятия решения: если status - matched или delayed, не размещайте никакие follow-up, которым нужен новый inventory, пока не проверите chain transfer.
Polling pattern: poll status before celebrating
Pattern проверки: после успешного match опрашивайте CTF balance, пока он не отразит новые tokens, а затем продолжайте.
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: 2-5 секунд при нормальных network conditions, до 15s во время congestion в Polygon. Пятисекундное ожидание покрывает 95% случаев; для production задайте timeout 15s и поднимайте alert при timeout.
Для high-frequency bots, которые не могут позволить себе block, есть альтернатива - event-subscription: отслеживайте событие CTF TransferSingle для вашего proxy address и запускайте downstream actions при получении. Это переносит ожидание в queue вместо блокировки strategy loop.
FOK как anti-phantom-fill
Выбор FOK вместо FAK - это частичная защита от хаоса phantom-fill. FOK либо исполняет весь order, либо возвращает cancelled; FAK может вернуть filled_size частичного объема. Когда за partial fill следует GTC sell, рассчитанный на исходный order, sell терпит неудачу из-за settlement-lag плюс mismatch размера - две ошибки, усиливающие друг друга.
С FOK размер бинарный: либо полностью matched нужный size, либо не произошло ничего. Логика follow-up posting всегда знает, чего ожидать.
Это не устраняет необходимость ожидания - даже идеальный FOK match зависит от 2-5 секундного окна settlement. Но это убирает один класс расхождений в bookkeeping.
Idempotent retries с client_order_id
Сетевые сбои во время размещения order создают worst-case scenario: HTTP-вызов bot истек по timeout, но order мог быть, а мог и не быть получен. Наивный retry может создать дубль; отсутствие retry может потерять позицию.
Решение - поле client_order_id при размещении order. Генерируйте deterministic UUID для каждого предполагаемого order; если сервер уже видел этот ID, он вернет статус существующего order вместо создания дубликата.
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, но никогда при logical rejection. Server-side dedup действует per-API-key и хранится примерно 5 minutes.
Реальный production incident: как мы это исправили
Из нашего собственного production diary, май 2025. 60-минутное окно, в течение которого trader bot выставил 22 buy orders, все matched, но только 14 GTC sells были приняты. У восьми позиций не было выставлено exit.
Root cause: bot размещал GTC sell через 800ms после buy match, задолго до того, как chain подтвердил ERC-1155 transfer. CLOB отклонял с сообщением "balance: 0"; bot логировал ошибку, но не повторял попытку. Восемь позиций молча дошли до resolution без take-profit protection. Три закрылись вне денег; одна закрылась на 0.99 по удаче.
Исправление вышло в виде 5-second blocking wait между любым buy fill и любым GTC post по тому же token. Проверено на 30 paper trades плюс 30 live trades; с тех пор ошибок balance-zero не было.
Урок: silent error path дороже, чем loud one. После этого мы сделали так, чтобы все phantom-fill errors вызывали Telegram alert, чтобы будущий 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 переживает common failure modes: phantom-fill, transient network drop, under-minimum GTC size. Возвращает достаточно информации, чтобы strategy layer решила, что retry, что log, а что alert.














