Polymarket Bot Tutorial · Rozdział 12 z 32
Jak wykrywać phantom fills w Polymarket (zlecenia, które wyglądają na zrealizowane, ale nie są), wdrażać idempotent retries, odróżniać status=matched od rested-on-book i przetrwać transient failures.
Co obejmuje ten rozdział
Phantom fill to specyficzny dla Polymarket failure mode, w którym CLOB potwierdza match, ale chain nie potwierdził jeszcze transferu ERC-1155. Kolejne zlecenie po około 5 sekundach zostaje odrzucone mylącym błędem „balance: 0”. Lekarstwem są idempotence i wait na settlement. Ten rozdział to production playbook, za który zapłaciliśmy prawdziwymi pieniędzmi.
To jest rozdział 12 naszej 32-częściowej serii o budowie trading bota dla Polymarket. Temat omawiamy szczegółowo w sekcjach poniżej. Treść główna dla każdej sekcji jest pisana i publikowana rozdział po rozdziale; odpowiedzi FAQ i references są już kompletne i odzwierciedlają doświadczenie produkcyjne z uruchamiania naszego własnego tradera.
- Czym jest phantom fill
- status=matched vs status=delayed vs status=posted
- Polling pattern: sprawdzaj status, zanim zaczniesz świętować
- FOK jako anti-phantom-fill
- Idempotent retries z client_order_id
- Prawdziwy production incident: jak to naprawiliśmy
- Code: detect-then-act post-order pattern
Czym jest phantom fill
Phantom fill występuje wtedy, gdy API CLOB odpowiada na zlecenie status: "matched", ale on-chain transfer ERC-1155 nie został jeszcze rozliczony. Matcher w CLOB działa szybciej niż produkcja bloków na Polygon (~2 s na blok). Przez około 2–5 sekund po matchu z API Twój wallet nie ma on-chain tokenów, które według matchera już posiadasz.
Błąd bota pojawia się wtedy, gdy kolejne działanie — zwykle GTC sell ustawiane jako take-profit — uruchamia się w tym oknie. CLOB sprawdza on-chain balance, widzi zero i odrzuca z komunikatem not enough balance / allowance: balance: 0, order amount: N. Komunikat zrzuca winę na allowance; prawdziwą przyczyną jest opóźnienie settlement.
Za pierwszym razem zakładasz, że to bug allowance i tracisz godzinę. Lekarstwem jest prosty schemat: poczekaj, zweryfikuj, potem postuj.
status=matched vs status=delayed vs status=posted
Odpowiedź na złożenie zlecenia zawiera pole status z trzema istotnymi wartościami.
matched: zlecenie zostało od razu dopasowane do booka. Inventory rozliczy się w 2–5 sekund. To zwraca FOK/FAK, gdy są skuteczne.delayed: matcher nie mógł rozliczyć synchronicznie i zakolejkował match. Rzadkie; zwykle oznacza congestion. Traktuj to jakmatchedna potrzeby wzorca wait + verify.posted(nazywane teżlive): zlecenie siedzi w booku bez filla. Zwracane przez GTC, które nie dopasowały się od razu. Inventory nie jest zmienione; na razie nie trzeba wykonywać kolejnego działania.
Reguła decyzyjna: jeśli status to matched albo delayed, nie składaj żadnego follow-up, które wymaga nowego inventory, dopóki nie zweryfikujesz transferu on-chain.
Polling pattern: sprawdzaj status, zanim zaczniesz świętować
Wzorzec weryfikacji: po udanym matchu polluj balance CTF, aż pokaże nowe tokeny, a dopiero potem idź dalej.
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
Typowy settlement: 2–5 sekund przy dobrych warunkach sieciowych, do 15 s podczas congestion na Polygon. 5-sekundowy wait pokrywa 95% przypadków; w production ustaw timeout na 15 s i generuj alert przy timeout.
Dla botów high-frequency, które nie mogą blokować, alternatywą jest event-subscription: obserwuj event TransferSingle w CTF dla swojego proxy address i uruchamiaj downstream actions po jego otrzymaniu. Przenosi to wait do kolejki zamiast blokować strategy loop.
FOK jako anti-phantom-fill
Wybór FOK zamiast FAK to częściowa obrona przed chaosem phantom-fill. FOK albo realizuje całe zlecenie, albo zwraca cancelled; FAK może zwrócić częściowy filled_size. Gdy po częściowym fillu następuje GTC sell o rozmiarze równym pierwotnemu zleceniu, sell wywala się przez settlement-lag plus mismatch rozmiaru — dwa błędy nakładające się na siebie.
Przy FOK rozmiar jest binarny: albo dopasował się cały rozmiar, albo nic. Logika follow-up zawsze wie, czego się spodziewać.
To nie eliminuje potrzeby wait — nawet idealny match FOK podlega oknu settlement 2–5 sekund. Ale usuwa jedną klasę rozjazdu w bookkeeping.
Idempotent retries z client_order_id
Awaria sieci podczas składania zlecenia tworzy najgorszy możliwy scenariusz: HTTP call bota timed out, ale zlecenie mogło zostać odebrane albo nie. Naiwny retry może spowodować podwójne złożenie; brak retry może zgubić pozycję.
Naprawą jest pole client_order_id przy składaniu zlecenia. Generuj deterministyczny UUID dla każdego zamierzonego zlecenia; jeśli serwer widział już to ID, zwróci status istniejącego zlecenia zamiast tworzyć duplikat.
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")
Wzorzec: najpierw generuj ID, retry tylko przy failure transportu, nigdy przy logical rejection. Dedup po stronie serwera działa per API key i jest aktywny przez około 5 minut.
Prawdziwy production incident: jak to naprawiliśmy
Z naszego własnego production diary, maj 2025. Okno 60 minut, w którym trader bot złożył 22 buy orders, wszystkie zostały matched, ale tylko 14 GTC sell zostało przyjętych. Osiem pozycji nie miało wystawionego exit.
Przyczyna główna: bot postował GTC sell w ciągu 800 ms od buy matcha, długo przed tym, jak chain potwierdził transfer ERC-1155. CLOB odrzucał z komunikatem „balance: 0”; bot logował błąd, ale nie robił retry. Osiem pozycji bezgłośnie dotrwało do settlement bez ochrony take-profit. Trzy zamknęły się poniżej oczekiwań; jedna zamknęła się na 0,99 z czystego szczęścia.
Poprawka weszła jako 5-sekundowy blocking wait między każdym buy fill a każdym GTC postem na tym samym tokenie. Zweryfikowane na 30 paper trades plus 30 live trades; od tamtej pory zero błędów balance-zero.
Wniosek: cichy path błędu kosztuje więcej niż głośny. Po tym ustawiliśmy, że wszystkie phantom-fill errors uruchamiają alert w Telegramie, żeby przyszły drift mode był widoczny w ciągu sekund.
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}
Wzorzec przetrwa typowe failure modes: phantom-fill, transient network drop, under-minimum GTC size. Zwraca wystarczająco dużo informacji, by warstwa strategii mogła zdecydować, co retry’ować, co logować, a co zgłaszać alertem.











