บทเรียนบอท โพลีมาร์เก็ต · บทที่ 32 จาก 32

ความผิดพลาดจริงของโพลีมาร์เก็ต bot และ postmortems: phantom fills, sticky-fail dedup, lol-ctg-ccg whipsaw, บั๊ก NegRisk flag, go-live ก่อนเวลา - พร้อม commits และวันที่ที่แก้แต่ละปัญหา

บทนี้ครอบคลุมอะไรบ้าง

บันทึกการใช้งานจริงของเราที่มีบั๊กซึ่งทำให้เสียเงินจริง รูปแบบของปัญหาสำคัญกว่ารายละเอียดเฉพาะ - บั๊กประเภทเดียวกันเกิดซ้ำได้ใน bot หลายตัว และวิธีแก้มักไม่ใช่กลยุทธ์ที่ดีกว่า แต่คือ watchdog ที่หายไป บทนี้ตั้งใจจะช่วยคุณประหยัดค่าเล่าเรียนตรงนั้น

  • ฟิลลวง (คอมมิต e68a087, 8bb7761)
  • NegRisk flag bug (commit)
  • Sticky-fail dedup (commit)
  • เหตุการณ์ราคาสวิงกลับ: lol-ctg-ccg
  • เปิดใช้งานจริงเร็วเกินไป: การล้างข้อมูล 2025
  • บั๊กที่หลับไม่รู้: สวิตช์หยุดฉุกเฉินทำงาน
  • บทเรียนที่นำไปใช้ได้ทั่วไป

ฟิลลวง (คอมมิต 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)

เหตุการณ์ 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 เรา rerun arbitrage โดยตั้ง flag แล้ว และอีก 2 รายการที่เหลือก็ settle ถูกต้อง

บทเรียน: อย่าตั้งค่าเริ่มต้นให้ property ของตลาด ต้องอ่านจาก source of truth ทุกครั้งอย่างชัดเจน

Sticky-fail dedup (commit)

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

บทเรียน: retries ที่ไม่มี idempotence คือ duplicate เสมอ ทุก order ต้องมี stable identifier ฝั่ง client

เหตุการณ์ราคาสวิงกลับ: 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 ก่อนลงมือ

เปิดใช้งานจริงเร็วเกินไป: การล้างข้อมูล 2025

กลยุทธ์ 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 ต่อรองไม่ได้

บั๊กที่หลับไม่รู้: สวิตช์หยุดฉุกเฉินทำงาน

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 ของคุณเอง

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

ความผิดพลาดที่ทำให้เสียเงินมากที่สุดของโพลีมาร์เก็ต 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: ตั้งค่า 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 ที่คุณต้องเขียนในภายหลัง