Polymarket Bot Tutorial · Bab 12 dari 32

Cara mendeteksi phantom fills Polymarket (order yang terlihat filled tetapi sebenarnya tidak), menerapkan idempotent retries, membedakan status=matched dari rested-on-book, dan bertahan dari transient failures.

Apa yang dibahas bab ini

Phantom fill adalah failure mode khusus Polymarket di mana CLOB mengakui match, tetapi chain belum mengonfirmasi transfer ERC-1155. Order lanjutan dalam ~5 detik akan ditolak dengan error yang menyesatkan, "balance: 0". Solusinya adalah idempotence dan wait untuk settlement. Bab ini adalah playbook produksi yang kami bayar dengan uang sungguhan.

  • Apa itu phantom fill
  • status=matched vs status=delayed vs status=posted
  • Polling pattern: poll status sebelum merayakan
  • FOK sebagai anti-phantom-fill
  • Idempotent retries dengan client_order_id
  • Insiden produksi nyata: bagaimana kami memperbaikinya
  • Code: detect-then-act post-order pattern

Apa itu phantom fill

Phantom fill adalah saat CLOB API merespons order Anda dengan status: "matched" tetapi transfer ERC-1155 on-chain belum selesai terselesaikan. Matcher CLOB lebih cepat daripada produksi block Polygon (~2 detik per block). Selama kira-kira 2-5 detik setelah API match, wallet Anda belum secara on-chain memegang token yang menurut matcher sudah Anda miliki.

Bug bot muncul ketika aksi lanjutan - biasanya GTC sell untuk memasang take-profit - dijalankan dalam jendela waktu itu. CLOB memeriksa balance di chain, melihat nol, lalu menolak dengan not enough balance / allowance: balance: 0, order amount: N. Pesan error menyalahkan allowance; penyebabnya adalah settlement lag.

Pertama kali ini terjadi, Anda mengira ada bug allowance dan membuang satu jam. Solusinya sederhana: tunggu, verifikasi, lalu post.

status=matched vs status=delayed vs status=posted

Respons penempatan order mencakup field status dengan tiga nilai yang penting.

  • matched: order langsung match terhadap book. Inventory akan settle dalam 2-5 detik. Ini yang dikembalikan FOK/FAK saat berhasil.
  • delayed: matcher tidak bisa settle secara sinkron dan menaruh match ke queue. Jarang; biasanya menandakan congestion. Perlakukan seperti matched untuk pola wait + verify.
  • posted (juga disebut live): order tetap resting di book dan belum terisi. Dikembalikan oleh order GTC yang tidak langsung match. Inventory tidak terpengaruh; belum perlu aksi lanjutan.

Aturan keputusan: jika status adalah matched atau delayed, jangan melakukan follow-up apa pun yang membutuhkan inventory baru sampai Anda memverifikasi transfer di chain.

Polling pattern: poll status sebelum merayakan

Pola verifikasi: setelah match berhasil, poll balance CTF sampai mencerminkan token baru, lalu lanjutkan.

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 tipikal: 2-5 detik dalam kondisi jaringan yang baik, hingga 15 detik saat Polygon congestion. Wait 5 detik menutup 95% kasus; untuk produksi, set timeout ke 15 detik dan beri alert saat timeout.

Untuk bot high-frequency yang tidak bisa menunggu secara blocking, alternatifnya adalah event-subscription: pantau event TransferSingle dari CTF untuk alamat proxy Anda dan trigger aksi downstream saat event diterima. Ini memindahkan wait ke queue alih-alih memblokir strategy loop.

FOK sebagai anti-phantom-fill

Memilih FOK daripada FAK adalah pertahanan sebagian terhadap kekacauan phantom-fill. FOK akan terisi seluruh order atau mengembalikan cancelled; FAK bisa mengembalikan filled_size yang parsial. Saat partial fill diikuti GTC sell dengan ukuran sebesar order asli, sell gagal karena settlement lag ditambah size mismatch - dua bug yang saling memperparah.

Dengan FOK, ukurannya bersifat biner: seluruh ukuran match atau tidak sama sekali. Logika posting lanjutan selalu tahu apa yang harus diharapkan.

Ini tidak menghilangkan kebutuhan untuk menunggu - bahkan match FOK yang sempurna tetap tunduk pada jendela settlement 2-5 detik. Tetapi ini menghilangkan satu kelas divergence dalam bookkeeping.

Idempotent retries dengan client_order_id

Gangguan jaringan saat penempatan order menciptakan skenario terburuk: panggilan HTTP bot Anda timeout, tetapi order mungkin sudah diterima atau belum. Retry secara naif bisa membuat double-place; tidak retry bisa membuat posisi hilang.

Solusinya adalah field client_order_id pada penempatan order. Generate UUID deterministik per order yang dimaksud; jika server sudah pernah melihat ID itu, server akan mengembalikan status order yang sudah ada alih-alih membuat 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")

Pola ini: generate ID terlebih dahulu, retry saat transport failure, jangan pernah saat logical rejection. Dedup di sisi server berlaku per API key, bertahan ~5 menit.

Insiden produksi nyata: bagaimana kami memperbaikinya

Dari diary produksi kami sendiri, Mei 2025. Sebuah window 60 menit saat trader bot memasang 22 buy order, semuanya match, tetapi hanya 14 GTC sell yang diterima. Delapan posisi tidak memiliki exit yang diposting.

Akar masalah: bot memposting GTC sell dalam 800ms setelah buy match, jauh sebelum chain mengonfirmasi transfer ERC-1155. CLOB menolak dengan pesan "balance: 0"; bot mencatat error tetapi tidak retry. Delapan posisi diam-diam berjalan sampai resolusi tanpa perlindungan take-profit. Tiga ditutup out of the money; satu ditutup pada 0.99 karena keberuntungan.

Perbaikannya dirilis sebagai wait blocking 5 detik antara buy fill dan GTC post pada token yang sama. Diverifikasi lewat 30 paper trades plus 30 live trades; sejak itu nol balance-zero errors.

Pelajarannya: jalur error yang diam-diam lebih mahal daripada yang berisik. Setelah ini kami membuat semua phantom-fill errors memicu Telegram alert, sehingga future drift mode akan terlihat dalam hitungan detik.

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}

Pola ini bertahan dari failure mode umum: phantom-fill, transient network drop, GTC size di bawah minimum. Mengembalikan cukup informasi agar strategy layer bisa memutuskan mana yang perlu retry, mana yang perlu log, dan mana yang perlu alert.

Pertanyaan yang sering diajukan

Apa itu phantom fill di Polymarket?
Phantom fill adalah saat bot Anda mengira order sudah filled tetapi exchange mencatatnya sebagai belum filled (atau partially filled). Ini terjadi ketika code client memperlakukan respons HTTP 200 sebagai konfirmasi, padahal respons itu hanya berarti order diterima oleh matching engine. Kami belajar ini dengan cara yang pahit - commit 06deaef, 8bb7761, dan e68a087 dalam riwayat trader kami memperbaiki tepat masalah ini.
Bagaimana cara menghindari phantom fills?
Tiga aturan: (1) Gunakan order FOK untuk buy - order akan fully filled atau fully gone, tidak pernah ambigu. (2) Perlakukan status selain matched sebagai belum filled - poll status order sampai status=matched ATAU amount_filled > 0. (3) Gunakan client_order_id (clientOrderId di V2) untuk idempotence agar retry tidak double-fill.
Apa arti status=delayed?
Order berada di matching engine tetapi belum fully matched. Order bisa match dalam hitungan detik atau bisa resting. Selalu poll - jika status tetap delayed lebih dari 5-10 detik dan amount_filled adalah 0, perlakukan sebagai unfilled dan pertimbangkan untuk cancel.
Bagaimana cara retry dengan aman tanpa double-filling?
Generate client_order_id unik per logical trade attempt dan kirim pada setiap retry. Exchange melakukan dedupe berdasarkan client_order_id sehingga order yang di-retry dengan id yang sama akan ditolak sebagai duplicate, bukan dipasang lagi. Implementasi: Python OrderArgs.client_order_id, Node CreateOrderOptions.clientOrderId.
Apakah saya bisa mempercayai respons 200 OK dari order endpoint?
Tidak - 200 OK hanya berarti "permintaan Anda diterima oleh matching engine," bukan "order Anda filled." Anda harus poll order status berdasarkan orderId setelah submission dan hanya menganggap status=matched (atau amount_filled > 0) sebagai real fill.
Bagaimana jika bot saya crash antara mengirim order dan melihat respons?
Saat restart, query open orders dan recent fills melalui SDK. Rekonsiliasikan dengan diary/state lokal Anda - jika Anda mengirim order pada waktu T tetapi tidak ada record, query orders sejak T dan cocokkan berdasarkan client_order_id. Jika masih hilang, order memang tidak pernah mencapai matching engine dan Anda bisa mengirim ulang dengan aman.