Polymarket Bot Tutorial · Bölüm 12 / 32

Polymarket phantom fill'leri nasıl tespit edilir (dolu gibi görünen ama aslında dolu olmayan emirler), idempotent retries nasıl uygulanır, status=matched ile rested-on-book nasıl ayırt edilir ve geçici failures nasıl atlatılır.

Bu bölüm neleri kapsıyor

Phantom fill, CLOB'un bir match'i kabul ettiği fakat chain'in henüz ERC-1155 transferını doğrulamadığı Polymarket'e özgü bir failure mode'dur. Yaklaşık 5 saniye içinde yapılacak bir takip emri yanıltıcı bir "balance: 0" hatasıyla reddedilir. Çözüm idempotence ve settlement beklemesidir. Bu bölüm, gerçek para ödeyerek öğrendiğimiz production playbook'tur.

  • Phantom fill nedir
  • status=matched vs status=delayed vs status=posted
  • Polling pattern: kutlamadan önce status'u poll et
  • Anti-phantom-fill olarak FOK
  • client_order_id ile idempotent retries
  • Gerçek production olayı: bizimkini nasıl düzelttik
  • Code: detect-then-act order sonrası pattern

Phantom fill nedir

Phantom fill, CLOB API'nin emrinize status: "matched" ile yanıt verdiği ancak on-chain ERC-1155 transferinin henüz settle olmadığı durumdur. CLOB matcher, Polygon block production'dan (~blok başına 2 saniye) daha hızlıdır. API match'inden sonraki yaklaşık 2-5 saniye boyunca wallet'ınız, matcher'ın size ait olduğunu söylediği token'ları on-chain olarak tutmaz.

Bot bug'ı, genellikle take-profit'i post etmek için yapılacak bir GTC sell gibi takip işlemi bu pencere içinde çalıştığında ortaya çıkar. CLOB chain balance'ı kontrol eder, zero görür ve not enough balance / allowance: balance: 0, order amount: N ile reddeder. Hata mesajı suçu allowance'a atar; sebep ise settlement lag'dir.

Bu ilk kez olduğunda bir allowance bug'ı olduğunu varsayar ve bir saat harcarsınız. Çözüm basit: bekle, doğrula, sonra post et.

status=matched vs status=delayed vs status=posted

Order placement response, önemli olan üç değere sahip bir status alanı içerir.

  • matched: emir book'a hemen match oldu. Inventory 2-5 saniye içinde settle olur. Başarılı olduğunda FOK/FAK bunu döndürür.
  • delayed: matcher senkron biçimde settle edemedi ve match'i queue'ya aldı. Nadir görülür; genellikle congestion anlamına gelir. wait + verify pattern'i açısından matched gibi ele alın.
  • posted (aynı zamanda live olarak da adlandırılır): emir doldurulmadan book üzerinde bekliyor. Hemen match olmayan GTC emirleri tarafından döndürülür. Inventory etkilenmez; henüz takip işlemi gerekmez.

Karar kuralı: status matched veya delayed ise, chain transfer'ı doğrulayana kadar yeni inventory gerektiren hiçbir takip emri vermeyin.

Polling pattern: kutlamadan önce status'u poll et

Doğrulama pattern'i: başarılı bir match'ten sonra CTF balance'ı yeni token'ları yansıtana kadar poll edin, sonra devam edin.

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

Tipik settlement: iyi network koşullarında 2-5 saniye, Polygon congestion sırasında 15 saniyeye kadar. 5 saniyelik bekleme vakaların %95'ini karşılar; production için timeout'u 15 saniyeye ayarlayın ve timeout durumunda alert üretin.

Bloklama yapamayan yüksek frekanslı bot'lar için alternatif bir yöntem event-subscription'dır: CTF'nin proxy adresiniz için TransferSingle event'ini izleyin ve downstream actions'ı alındığında tetikleyin. Bu, beklemeyi strategy loop'u yerine bir queue'ya taşır.

Anti-phantom-fill olarak FOK

FAK yerine FOK seçmek, phantom-fill kaosuna karşı kısmi bir savunmadır. FOK ya tüm emri doldurur ya da cancelled döndürür; FAK ise kısmi bir filled_size döndürebilir. Kısmi bir fill'i, orijinal emir boyutunda bir GTC sell izlediğinde, sell settlement lag + size mismatch nedeniyle başarısız olur - birbirini büyüten iki bug.

FOK ile size binary'dir: ya tam size match olur ya da hiçbir şey olmaz. Takip eden posting logic'i her zaman ne beklemesi gerektiğini bilir.

Bu, wait ihtiyacını ortadan kaldırmaz - kusursuz bir FOK match bile 2-5 saniyelik settlement penceresine tabidir. Ancak bookkeeping divergence'larının bir sınıfını ortadan kaldırır.

client_order_id ile idempotent retries

Order placement sırasında yaşanan network failures en kötü senaryoyu yaratır: bot'un HTTP call'u timeout olur, fakat emir alınmış da olabilir alınmamış da. Körlemesine retry yapmak iki kez place etmeye yol açabilir; retry yapmamak ise bir pozisyonun kaybolmasına neden olabilir.

Çözüm, order placement'ta kullanılan client_order_id alanıdır. Her amaçlanan emir için deterministik bir UUID üretin; server bu ID'yi daha önce gördüyse duplicate oluşturmak yerine mevcut emrin status'unu döndürür.

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 şu şekildedir: önce ID'yi üretin, transport failure durumunda retry edin, logical rejection durumunda asla retry etmeyin. Server-side dedup her API key için geçerlidir ve yaklaşık 5 dakika sürer.

Gerçek production olayı: bizimkini nasıl düzelttik

Kendi production günlüğümüzden, Mayıs 2025. 60 dakikalık bir pencerede trader bot 22 buy order verdi, hepsi match oldu, ancak yalnızca 14 GTC sell kabul edildi. Sekiz pozisyon için exit post edilmedi.

Kök neden: bot, buy match'inden 800ms sonra GTC sell post ediyordu; chain'in ERC-1155 transfer'ını doğrulamasından çok önce. CLOB "balance: 0" mesajıyla reddetti; bot hatayı logladı ama retry etmedi. Sekiz pozisyon, take-profit koruması olmadan sessizce resolution'a kadar taşındı. Üçü kârda olmadan kapandı; biri şans eseri 0.99'dan kapandı.

Çözüm, aynı token üzerinde herhangi bir buy fill ile herhangi bir GTC post arasına 5 saniyelik blocking wait eklenerek yayınlandı. 30 paper trade + 30 live trade ile doğrulandı; o zamandan beri zero-balance error yok.

Çıkarım: sessiz bir error path, gürültülü bir path'ten daha pahalıdır. Bundan sonra tüm phantom-fill hatalarını Telegram alert tetikleyecek şekilde ayarladık, böylece gelecekteki bir drift mode saniyeler içinde görünür olur.

Code: detect-then-act order sonrası pattern

Production buy-then-post pattern'i.

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}

Bu pattern, yaygın failure mode'lara karşı dayanıklıdır: phantom-fill, geçici network düşüşü, minimum altı GTC size. Strategy layer'ın neyi retry edeceğine, neyi loglayacağına ve neyi alert'e dönüştüreceğine karar verebilmesi için yeterli bilgi döndürür.

Sık sorulan sorular

Polymarket'te phantom fill nedir?
Phantom fill, bot'un bir emrin dolduğunu düşünmesi ancak exchange'in bunu henüz dolmamış (veya kısmen dolmuş) olarak kaydetmesidir. Bu, client code bir HTTP 200 response'u onay olarak yorumladığında olur; oysa response yalnızca emrin matching engine'e kabul edildiği anlamına gelir. Bunu acı şekilde öğrendik - trader geçmişimizdeki 06deaef, 8bb7761 ve e68a087 commit'leri tam olarak bunu düzeltir.
Phantom fill'lerden nasıl kaçınırım?
Üç kural: (1) Buy'lar için FOK order kullanın - emir ya tamamen dolar ya da tamamen yok olur, asla belirsiz kalmaz. (2) matched olmayan herhangi bir status'u filled değil olarak kabul edin - status=matched OLANA YA DA amount_filled > 0 olana kadar order status'unu poll edin. (3) Retry'lerin double-fill oluşturmaması için idempotence adına client_order_id (V2'de clientOrderId) kullanın.
status=delayed ne anlama gelir?
Emir matching engine'dedir ancak henüz tamamen match olmamıştır. Saniyeler içinde match olabilir ya da book'ta bekleyebilir. Her zaman poll edin - status 5-10 saniyeden uzun süre delayed kalır ve amount_filled 0 ise, bunu unfilled olarak ele alın ve cancel etmeyi düşünün.
Double-fill yapmadan güvenli şekilde nasıl retry ederim?
Her mantıksal trade attempt'i için benzersiz bir client_order_id üretin ve her retry'da onu kullanın. Exchange, client_order_id'ye göre dedupe eder; bu nedenle aynı id ile retry edilen emir tekrar place edilmek yerine duplicate olarak reddedilir. Uygulamalar: Python OrderArgs.client_order_id, Node CreateOrderOptions.clientOrderId.
Order endpoint'inden gelen 200 OK response'una güvenebilir miyim?
Hayır - 200 OK yalnızca "request'iniz matching engine tarafından kabul edildi" anlamına gelir, "emriniz doldu" anlamına gelmez. Submission'dan sonra orderId ile order status'unu poll etmelisiniz ve yalnızca status=matched (veya amount_filled > 0) gerçek fill olarak kabul edilmelidir.
Bot'um bir emir gönderip response'u görmeden çökerse ne olur?
Restart sonrası SDK ile open orders ve recent fills sorgulayın. Local diary/state ile reconcile edin - T anında bir emir gönderdiyseniz ama kayıt yoksa, T'den beri olan emirleri sorgulayın ve client_order_id ile eşleştirin. Hâlâ bulunamıyorsa emir matching engine'e hiç ulaşmamıştır ve güvenle yeniden gönderebilirsiniz.