Polymarket Bot Tutorial · บทที่ 32 จาก 32
ความผิดพลาดจริงของ Polymarket bot และ postmortems: phantom fills, sticky-fail dedup, lol-ctg-ccg whipsaw, บั๊ก NegRisk flag, go-live ก่อนเวลา - พร้อม commits และวันที่ที่แก้แต่ละปัญหา
บทนี้ครอบคลุมอะไรบ้าง
บันทึกการใช้งานจริงของเราที่มีบั๊กซึ่งทำให้เสียเงินจริง รูปแบบของปัญหาสำคัญกว่ารายละเอียดเฉพาะ-บั๊กประเภทเดียวกันเกิดซ้ำได้ใน bot หลายตัว และวิธีแก้มักไม่ใช่กลยุทธ์ที่ดีกว่า แต่คือ watchdog ที่หายไป บทนี้ตั้งใจจะช่วยคุณประหยัดค่าเล่าเรียนตรงนั้น
- Phantom fills (commits e68a087, 8bb7761)
- NegRisk flag bug (commit 06deaef)
- Sticky-fail dedup (commit 4c0bef1)
- Whipsaw incident: lol-ctg-ccg
- Premature go-live: 2025 wipe
- Sleep-through-bug: kill switch ใช้งานได้
- บทเรียนที่นำไปใช้ได้ทั่วไป
Phantom fills (commits e68a087, 8bb7761)
เหตุการณ์ phantom-fill ครั้งใหญ่ครั้งแรกใน trader ของเราเกิดขึ้นในเดือนพฤษภาคม 2025 Bot วางคำสั่งซื้อ FOK 22 รายการ และทั้งหมด matched ที่ CLOB จากนั้น bot พยายามโพสต์คำสั่งขาย GTC 22 รายการทันที 8 รายการถูก reject พร้อมข้อความ "balance: 0 / sum of active orders: 0 / order amount: 10000000."
สาเหตุหลัก: settlement lag (บทที่ 12) CLOB matched ภายใน 100ms bot โพสต์คำสั่งขายใน 200ms แต่การโอน Polygon ERC-1155 ใช้เวลาประมาณ 2 วินาที CLOB จึง reject คำสั่งขายเพราะบน chain ยังแสดงยอดคงเหลือเป็นศูนย์
วิธีแก้: ใส่การรอแบบบล็อก 5 วินาทีระหว่าง buy ที่สำเร็จทุกครั้งกับ GTC follow-up ใด ๆ บน token เดียวกัน commits e68a087 และ 8bb7761 ตั้งแต่นั้นมาไม่มีเหตุการณ์ phantom-fill อีกเลย
บทเรียน: เวลาใน API กับเวลาบน chain คือไทม์ไลน์คนละเส้น โค้ดที่สมมติว่ามัน synchronous กันจะเจอ failure mode นี้แบบตรงตัว
NegRisk flag bug (commit 06deaef)
เหตุการณ์ multi-outcome ของ NegRisk ที่มีผู้สมัคร 8 ราย มี arbitrage ชั่วคราว 1.8c (ผลรวม YES asks = 0.982) arber ของเราจึงยิงคำสั่งซื้อ FOK ทั้ง 8 รายการ 6 รายการ fill แล้ว 2 รายการถูก settle เข้า contract ของ exchange ผิดตัว
สาเหตุหลัก: bot เรียก createAndPostOrder โดยไม่ได้ตั้ง negRisk: true ใน object ของ flags ตลาดสองแห่งมีวันที่สร้างย้อนหลังต่างกันและต้องใช้ flag นี้; อีกหกแห่งไม่ต้องใช้ เพราะ underlying contract ของพวกมัน routing ผ่าน NegRisk อยู่แล้วตามค่าเริ่มต้น
วิธีแก้: อ่าน market.negRisk จาก Gamma สำหรับทุกตลาด แล้วส่งต่อไปยังทุกการเรียก order commit 06deaef เรา rerun arbitrage โดยตั้ง flag แล้ว และอีก 2 รายการที่เหลือก็ settle ถูกต้อง
บทเรียน: อย่าตั้งค่าเริ่มต้นให้ property ของตลาด ต้องอ่านจาก source of truth ทุกครั้งอย่างชัดเจน
Sticky-fail dedup (commit 4c0bef1)
bot พยายาม retry คำสั่งซื้อที่ล้มเหลว 5 ครั้งภายใน 12 วินาที ครั้งแรกจริง ๆ แล้วสำเร็จ (network timeout ทำให้ bot มองไม่เห็น response); retry 4 ครั้งถัดมาสร้างตำแหน่งเพิ่มอีก 4 ตำแหน่ง รวมทั้งหมดคือ 5 positions ในตลาดเดียวกัน ทั้งที่เราต้องการแค่ 1
สาเหตุหลัก: ไม่มี idempotent client-order-id กลไก retry ของ bot คือ "ถ้าล้มเหลว ลองใหม่ด้วย salt ใหม่" CLOB ไม่มีทางรู้ว่าการ retry เหล่านั้นเป็น duplicate
วิธีแก้: สร้าง deterministic UUID ต่อ order ที่ตั้งใจจะส่ง ก่อนความพยายามครั้งแรก retries ทั้งหมดใช้ client-order-id เดิม ทำให้ CLOB dedup ได้ commit 4c0bef1
บทเรียน: retries ที่ไม่มี idempotence คือ duplicate เสมอ ทุก order ต้องมี stable identifier ฝั่ง client
Whipsaw incident: lol-ctg-ccg
แมตช์ esports คู่หนึ่ง (CTG vs CCG) ทำให้ bot เข้าซื้อที่ 0.45 เมื่อ imbalance พลิกเป็นบวก ภายใน 30 วินาที imbalance พลิกเป็นลบ และคำสั่งขาย GTC ที่ 0.50 ของเราก็โดน order ของคนอื่น hit PnL: +5c × 10 shares = +$0.50
10 นาทีต่อมา imbalance ของตลาดเดียวกันพลิกเป็นบวกอีกครั้ง bot เข้าซื้ออีกครั้งที่ 0.42 คราวนี้ imbalance ไม่ฟื้นกลับ mid drift ลงไปที่ 0.18 และตำแหน่งถูกถือไปจน resolve ที่ 0
สาเหตุหลัก: กลยุทธ์มอง imbalance เป็นสัญญาณเชิงทิศทาง แต่ไม่ได้ติดตามว่า imbalance กำลังเด้งไปเด้งมา-ทั้งสองสัญญาณเป็น noise ไม่ใช่ข้อมูล bot ถูก whipsaw จากสัญญาณที่ล้มเหลวสองครั้งในตลาดเดียวกันภายใน 20 นาที
วิธีแก้: ใส่ cooldown ต่อหนึ่งตลาด-หลัง fill แล้ว ห้ามเปิดตำแหน่งใหม่ในตลาดเดียวกันเป็นเวลา 30 นาที ยังคงอนุญาตให้เข้าได้หลายตลาด แต่ไม่อนุญาตให้ติดกันในตลาดเดิม
บทเรียน: สัญญาณที่เด้งไปเด้งมาไม่ใช่สัญญาณ ต้องกรอง persistence ก่อนลงมือ
Premature go-live: 2025 wipe
กลยุทธ์ market-making ใหม่ผ่าน paper trades 12 ครั้ง ผู้สร้างไม่ได้รอให้ครบ 30 ครั้ง ตัดสินว่า "ดูดี" แล้ว deploy จริงด้วยทุน $500 ภายใน 18 ชั่วโมง wallet เหลือ $200
สาเหตุหลัก: trades แค่ 12 ครั้งไม่พอที่จะบอกความต่างระหว่าง WR 60% กับ WR 35% ได้ กลยุทธ์จริง ๆ มี WR 35%; ช่วง paper 12 ครั้งบังเอิญเป็นสตรีคที่ไม่เป็นตัวแทน
เกณฑ์ 30 trades มีเหตุผลรองรับ ความแปรปรวนของตัวอย่าง 12 trades ทำให้แยกไม่ออกจาก "กลยุทธ์นี้ใช้ไม่ได้"
บทเรียน: วินัยชนะความมั่นใจ เกณฑ์ 30 trades ต่อรองไม่ได้
Sleep-through-bug: kill switch ใช้งานได้
bot มี off-by-one ใน time-of-day filter-ตั้งใจให้พักที่ 02:00 UTC แต่จริง ๆ พักที่ 03:00 UTC ระหว่างชั่วโมง 02:00-03:00 ที่ไม่ได้พัก RPC ของ Polygon จำกัดอัตราคำขอของเราอย่างหนัก; read path ของ bot จึงส่งข้อมูล stale กลับมา
bot ยังคงเทรดบนราคาที่ stale PnL ของชั่วโมงนั้น: -$3.20 จาก 22 trades kill switch ของ daily loss ทำงานที่ -5% หยุด bot และส่ง Telegram alert เวลา 03:08 UTC ผู้สร้างตื่นมาเจอ bot ที่ถูกหยุดไว้ตอน 09:00 ความเสียหายรวมถูกจำกัดไว้ตาม threshold ของ kill switch
บทเรียน: บั๊กมีจริง แต่ kill switch ก็ทำงานได้ -$3.20 แทนที่จะเป็น -$50.00 risk control ไม่ได้ป้องกันบั๊ก; แต่มันจำกัดต้นทุนของบั๊กที่คุณไม่ทันเห็น
บทเรียนที่นำไปใช้ได้ทั่วไป
จาก postmortem ทั้งหมด มี 4 รูปแบบที่เกิดซ้ำ
- API time ≠ chain time. settlement lag, RPC lag, WebSocket lag-ทั้งหมดสร้างช่องว่างที่โค้ด bot ต้องรับมืออย่างชัดเจน
- Retries ต้องมี idempotence. การ retry โดยไม่มี client-order-id คือความเสี่ยงต่อ duplicate order เสมอ
- อ่าน property ของตลาดทุกตัวอย่างชัดเจน. NegRisk flag, tick size, expiration อย่าตั้งค่าเริ่มต้น; ให้อ่านจาก source of truth เสมอ
- kill switch คือพื้นล่าง ไม่ใช่ฟีเจอร์. risk controls จำกัดการขาดทุนจากบั๊ก กลยุทธ์ไม่ได้ป้องกันบั๊ก; มันแค่สมมติว่า bot ทำงานถูกต้อง แต่ bot จะไม่ได้ทำงานถูกต้องเสมอไป
ทุกบทในซีรีส์นี้มีหนึ่งในรูปแบบเหล่านี้แฝงอยู่ somewhere มันคือหลักการรับน้ำหนักของ production bot ข้ามมันไป แล้วคุณจะเจอมันอีกครั้งใน postmortem ของคุณเอง





