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 รูปแบบที่เกิดซ้ำ

  1. API time ≠ chain time. settlement lag, RPC lag, WebSocket lag-ทั้งหมดสร้างช่องว่างที่โค้ด bot ต้องรับมืออย่างชัดเจน
  2. Retries ต้องมี idempotence. การ retry โดยไม่มี client-order-id คือความเสี่ยงต่อ duplicate order เสมอ
  3. อ่าน property ของตลาดทุกตัวอย่างชัดเจน. NegRisk flag, tick size, expiration อย่าตั้งค่าเริ่มต้น; ให้อ่านจาก source of truth เสมอ
  4. kill switch คือพื้นล่าง ไม่ใช่ฟีเจอร์. risk controls จำกัดการขาดทุนจากบั๊ก กลยุทธ์ไม่ได้ป้องกันบั๊ก; มันแค่สมมติว่า bot ทำงานถูกต้อง แต่ bot จะไม่ได้ทำงานถูกต้องเสมอไป

ทุกบทในซีรีส์นี้มีหนึ่งในรูปแบบเหล่านี้แฝงอยู่ somewhere มันคือหลักการรับน้ำหนักของ production bot ข้ามมันไป แล้วคุณจะเจอมันอีกครั้งใน postmortem ของคุณเอง

คำถามที่พบบ่อย

ความผิดพลาดที่ทำให้เสียเงินมากที่สุดของ Polymarket bot คืออะไร?
การ go live ก่อนที่ paper trading จะผ่านเกณฑ์ 30 trades เราเคยทำมาแล้ว ความผิดพลาดไม่ใช่แค่การเสียเงิน - แต่คือการเสียโอกาสเรียนรู้จากกลยุทธ์ในสภาพแวดล้อมที่ควบคุมได้ bot ที่ go live เร็วเกินไปมักจะโดนล้างพอร์ตแล้วถูกทิ้ง หรือไม่ก็เสียเวลาหลายเดือนเพื่อฟื้นตัวก่อนจะกลับไป paper trade ใหม่
phantom fill bug คืออะไร?
คือเมื่อ bot เชื่อว่าคำสั่งถูก fill แล้ว แต่ exchange บันทึกว่ายังไม่ fill อาการ: position ปรากฏใน state ของ bot แต่ไม่อยู่บน-chain ทำให้ retry แล้วกลายเป็น double order แก้ใน trader ของเราผ่าน 3 commits (e68a087, 8bb7761, 06deaef): ใช้ FOK สำหรับ buy, poll สถานะจนกว่าจะ matched, และอย่าเชื่อว่า status=delayed คือ filled
whipsaw incident: lol-ctg-ccg คืออะไร?
เป็นตลาด esports ที่ order book บางมาก ซึ่ง trader ของเรายิง stop-loss ที่ -$2.55 ที่ 0.14 แล้วดูราคาฟื้นกลับไป 0.325 ภายใน 2 นาที เราตั้ง stop-loss ไว้ที่ -4 จุดเปอร์เซ็นต์ ซึ่งตึงเกินไปสำหรับ order book esports ที่บาง วิธีแก้: ขยาย SL เป็น -8pp สำหรับตลาดสภาพคล่องต่ำ และใช้ SL ที่ตึงกว่าสำหรับตลาดหนาแน่นเท่านั้น (NBA, ฟุตบอลสภาพคล่องสูง) ดู memory/trader-sl-wider.md
บั๊ก NegRisk flag แสดงอาการอย่างไร?
bot วางคำสั่งโดยไม่ตั้งค่า neg_risk=true ในตลาด multi-outcome คำสั่งถูก reject ด้วยข้อความผิด ๆ ที่ทำให้สับสน นำไปสู่การหน่วงหลายวินาทีก่อน retry และทำให้พลาด fills วิธีแก้ใน commit 06deaef: ตั้งค่า neg_risk ตาม metadata ของแต่ละตลาดเสมอ อย่าคิดเอาเอง
เหตุการณ์ sleep-through-bug คืออะไร?
wallet ติดค้างด้วยคำสั่งที่ stuck ตอนตี 4 เจ้าของสั่งให้ bot หยุด โดยแตะไฟล์ data/halt_autobuy bot ตรวจพบไฟล์นี้ก่อนจะพยายามเทรดครั้งถัดไปและปฏิเสธการวางคำสั่ง เจ้าของตื่นขึ้นมาเจอสถานะที่สะอาด แทนที่จะเป็นสถานะที่แย่กว่าเดิม ยืนยันรูปแบบ halt-sentinel; ตอนนี้เรา ship มันเป็นค่าเริ่มต้นในทุก bot
บทเรียนที่นำไปใช้ได้ทั่วไปที่สุดจาก postmortems เหล่านี้คืออะไร?
อย่าเชื่อ happy path เด็ดขาด บั๊กทุกตัวที่เรา ship มาจากการสมมติว่าคำขอสำเร็จ, fill เป็นของจริง, หรือราคาจะไม่ขยับ เขียนโค้ดแบบ defensive: สมมติว่า orders ล้มเหลว, reconciliation diverge, และสมมติว่าตลาดใดตลาดหนึ่งกำลังจะทำอะไรแปลก ๆ ค่า paranoia นั้นเล็กน้อย; ต้นทุนของการข้ามมันคือ postmortem ที่คุณต้องเขียนในภายหลัง