Polymarket Bot Tutorial · Kabanata 12 ng 32

Paano i-detect ang Polymarket phantom fills (orders na mukhang filled pero hindi), mag-implement ng idempotent retries, paghiwalayin ang status=matched mula sa rested-on-book, at malampasan ang transient failures.

Ano ang sinasaklaw ng kabanatang ito

Ang phantom fill ay Polymarket-specific failure mode kung saan kinikilala ng CLOB ang isang match pero hindi pa kinukumpirma ng chain ang ERC-1155 transfer. Ang follow-up order sa loob ng ~5 segundo ay tinatanggihan na may misleading na "balance: 0" error. Ang lunas ay idempotence at isang settlement wait. Ang kabanatang ito ay ang production playbook na binayaran namin nang totoong pera.

  • Ano ang phantom fill
  • status=matched vs status=delayed vs status=posted
  • Polling pattern: i-poll ang status bago magdiwang
  • FOK bilang anti-phantom-fill
  • Idempotent retries gamit ang client_order_id
  • Tunay na production incident: paano namin inayos ang sa amin
  • Code: detect-then-act post-order pattern

Ano ang phantom fill

Ang phantom fill ay kapag tumutugon ang CLOB API sa iyong order na may status: "matched" ngunit hindi pa nag-settle ang on-chain ERC-1155 transfer. Ang CLOB matcher ay mas mabilis kaysa sa Polygon block production (~2s bawat block). Sa loob ng halos 2-5 segundo pagkatapos ng API match, ang iyong wallet ay hindi on-chain humahawak sa mga tokens na sinasabi ng matcher na pag-aari mo.

Ang bug ng bot ay lumalabas kapag ang follow-up action - karaniwang GTC sell para mag-post ng take-profit - ay tumatakbo sa loob ng window na iyon. Sine-check ng CLOB ang chain balance, nakikita ang zero, at tinatanggihan na may not enough balance / allowance: balance: 0, order amount: N. Ang error message ay sinisisi ang allowance; ang sanhi ay settlement lag.

Sa unang pagkakataon na nangyari ito, ipinagpapalagay mo ang isang allowance bug at sinasayang ang isang oras. Ang lunas ay simple: maghintay, i-verify, pagkatapos i-post.

status=matched vs status=delayed vs status=posted

Ang order placement response ay may kasamang status field na may tatlong values na mahalaga.

  • matched: ang order ay tumugma sa book kaagad. Ang inventory ay mag-settle sa 2-5 segundo. Ito ang ibinabalik ng FOK/FAK kapag nagtagumpay.
  • delayed: hindi maaaring mag-settle ng matcher nang synchronous at na-queue ang match. Bihira; karaniwang nagpapahiwatig ng congestion. Tratuhin tulad ng matched para sa layunin ng wait + verify pattern.
  • posted (tinatawag ding live): ang order ay nagpapahinga sa book na hindi pa filled. Ibinabalik ng GTC orders na hindi agad nag-match. Ang inventory ay hindi naaapektuhan; walang kailangang follow-up action sa ngayon.

Ang decision rule: kung ang status ay matched o delayed, huwag maglagay ng anumang follow-up na nangangailangan ng bagong inventory hanggang sa ma-verify mo ang chain transfer.

Polling pattern: i-poll ang status bago magdiwang

Ang verification pattern: pagkatapos ng matagumpay na match, i-poll ang CTF balance hanggang sa makita nito ang mga bagong tokens, pagkatapos magpatuloy.

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

Karaniwang settlement: 2-5 segundo sa magandang network conditions, hanggang 15s sa panahon ng Polygon congestion. Ang 5-segundong paghihintay ay sumasaklaw sa 95% ng mga kaso; para sa production itakda ang timeout sa 15s at mag-alerto sa timeout.

Para sa high-frequency bots na hindi kayang mag-block, ang alternatibo ay event-subscription: panoorin ang TransferSingle event ng CTF para sa iyong proxy address at i-trigger ang downstream actions sa pagtanggap. Itinutulak nito ang paghihintay sa queue sa halip na harangan ang strategy loop.

FOK bilang anti-phantom-fill

Ang pagpili ng FOK kaysa FAK ay partial defense laban sa phantom-fill chaos. Ang FOK ay alinman fully fill ang buong order o ibabalik ang cancelled; ang FAK ay maaaring magbalik ng filled_size na partial. Kapag ang partial fill ay sinundan ng GTC sell na sukat sa orihinal na order, ang sell ay nabigo sa settlement-lag plus size mismatch - dalawang compounding bugs.

Sa FOK, ang sukat ay binary: alinman buong sukat ang tumugma o wala. Ang follow-up posting logic ay palaging alam kung ano ang aasahan.

Hindi nito inaalis ang pangangailangan ng paghihintay - kahit ang perpektong FOK match ay napapailalim sa 2-5 segundong settlement window. Ngunit tinatanggal nito ang isang klase ng bookkeeping divergence.

Idempotent retries gamit ang client_order_id

Ang network failures sa panahon ng order placement ay lumilikha ng worst-case scenario: nag-timeout ang HTTP call ng bot, ngunit ang order ay maaaring natanggap o hindi. Ang naive na pag-retry ay maaaring dumoble sa pag-place; ang hindi pag-retry ay maaaring mag-drop ng position.

Ang fix ay ang client_order_id field sa order placement. Bumuo ng deterministic UUID bawat intended order; kung ang server ay nakakita na sa ID na iyon, ibabalik nito ang status ng umiiral na order sa halip na lumikha ng 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")

Ang pattern: bumuo ng ID muna, mag-retry sa transport failure, hindi kailanman sa logical rejection. Ang server-side dedup ay per-API-key, tumatagal ng ~5 minuto.

Tunay na production incident: paano namin inayos ang sa amin

Mula sa aming sariling production diary, Mayo 2025. Isang 60-minutong window kung saan naglagay ang trader bot ng 22 buy orders, lahat ay nag-match, pero 14 GTC sells lamang ang tinanggap. Walong positions ang walang na-post na exit.

Root cause: nag-post ang bot ng GTC sell sa loob ng 800ms ng buy match, bago pa kinumpirma ng chain ang ERC-1155 transfer. Tinanggihan ng CLOB sa pamamagitan ng "balance: 0" message; nag-log ang bot ng error pero hindi nag-retry. Walong positions ang tahimik na sumakay hanggang sa resolution na walang take-profit protection. Tatlo ang nagsara out of the money; isa ang nagsara sa 0.99 sa pamamagitan ng suwerte.

Ang fix ay ipinakalat bilang 5-segundong blocking wait sa pagitan ng anumang buy fill at anumang GTC post sa parehong token. Na-verify sa pamamagitan ng 30 paper trades plus 30 live trades; zero balance-zero errors mula noon.

Ang aral: ang silent error path ay mas mahal kaysa sa malakas. Pagkatapos nito ginawa naming mag-trigger ng Telegram alert ang lahat ng phantom-fill errors, kaya ang future drift mode ay makikita sa loob ng ilang segundo.

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}

Ang pattern ay nakakaligtas sa karaniwang failure modes: phantom-fill, transient network drop, under-minimum GTC size. Nagbabalik ng sapat na impormasyon para sa strategy layer na magdesisyon kung ano ang i-retry vs. i-log vs. i-alert.

Mga madalas na tanong

Ano ang phantom fill sa Polymarket?
Ang phantom fill ay kapag naniniwala ang iyong bot na nag-fill ang order pero ang exchange ay nagre-record nito bilang hindi pa filled (o partially filled). Nangyayari ito kapag tinrato ng client code ang HTTP 200 response bilang confirmation, kapag ang response ay nangangahulugan lamang na natanggap ang order sa matching engine. Natutunan namin ito sa mahirap na paraan - commits 06deaef, 8bb7761, at e68a087 sa aming trader history ay nag-aayos ng eksaktong ito.
Paano ko maiiwasan ang phantom fills?
Tatlong rules: (1) Gumamit ng FOK orders para sa buys - ang order ay alinman fully filled o fully gone, hindi kailanman ambiguous. (2) Tratuhin ang anumang non-matched status bilang hindi filled - i-poll ang order status hanggang status=matched O amount_filled > 0. (3) Gumamit ng client_order_id (clientOrderId sa V2) para sa idempotence upang ang mga retries ay hindi double-fill.
Ano ang ibig sabihin ng status=delayed?
Ang order ay nasa matching engine pero hindi pa fully matched. Maaaring mag-match sa loob ng segundo o maaari itong magpahinga. Palaging i-poll - kung ang status ay nananatiling delayed sa loob ng higit sa 5-10 segundo at ang amount_filled ay 0, tratuhin ito bilang unfilled at isaalang-alang ang pag-cancel.
Paano akong mag-retry nang ligtas nang walang double-filling?
Bumuo ng natatanging client_order_id bawat logical trade attempt at ipasa ito sa bawat retry. Ang exchange ay nag-dedupe sa pamamagitan ng client_order_id kaya ang retried order na may parehong id ay tinatanggihan bilang duplicate sa halip na inilagay muli. Implementations: Python OrderArgs.client_order_id, Node CreateOrderOptions.clientOrderId.
Maaari ko bang pagkatiwalaan ang 200 OK response mula sa order endpoint?
Hindi - ang 200 OK ay nangangahulugan lamang ng "natanggap ang iyong request ng matching engine," hindi "nag-fill ang iyong order." Dapat mong i-poll ang order status sa pamamagitan ng orderId pagkatapos ng submission at tratuhin lamang ang status=matched (o amount_filled > 0) bilang tunay na fill.
Paano kung mag-crash ang aking bot sa pagitan ng pagpapadala ng order at pagkita ng response?
Sa restart, i-query ang open orders at recent fills sa pamamagitan ng SDK. Ipagkasundo laban sa iyong lokal na diary/state - kung nagpadala ka ng order sa time T pero walang record na umiiral, i-query ang orders since T at i-match sa pamamagitan ng client_order_id. Kung wala pa rin, ang order ay hindi umabot sa matching engine at maaari mong ligtas na i-resend.