Polymarket Bot Tutorial · บทที่ 22 จาก 32
บอท NegRisk หลายผลลัพธ์บน Polymarket: กลไก sum-to-1, leg arbitrage เมื่อ YES legs รวมกันไม่ถึง 1, การ hedge ข้ามหลาย legs, และข้อผิดพลาดในการ execution ที่เฉพาะกับตลาดหลายผลลัพธ์
บทนี้ครอบคลุมอะไรบ้าง
ตลาด NegRisk แบบหลายผลลัพธ์เป็นแบบ mutually exclusive-จะมีเพียงหนึ่งผลลัพธ์ที่ปิดเป็น YES เท่านั้น บทนี้คือเลเยอร์ด้านกลยุทธ์ที่ต่อยอดจากกลไกการ execution ในบทที่ 11: วิธี hedge ข้าม legs, เมื่อใดที่ sum-to-1 arb เป็นโอกาสจริง, และบั๊กที่บอท NegRisk ส่วนใหญ่เจอตั้งแต่ตอน deploy ครั้งแรก
- สรุป NegRisk vs binary
- invariant แบบ sum-to-1 และ arbitrage
- การสร้าง hedge แบบ leg-by-leg
- Execution: neg_risk flag ใน orders
- บั๊กที่พบบ่อยในบอท NegRisk
- โค้ด: snapshot ทุก legs และตรวจจับผลรวมต่ำกว่า 1.00
สรุป NegRisk vs binary
Binary: ตลาด yes/no หนึ่งตลาด, มีสอง tokens, ผลรวมเท่ากับ 1.0 NegRisk: มี N ผลลัพธ์ที่ mutually exclusive, มี N tokens, และ YES legs ทั้งหมดรวมกันใกล้เคียง 1.0 สำหรับเหตุการณ์นั้น
ในแง่ execution NegRisk ต้องใส่ negRisk: true ในทุก order (บทที่ 11) และส่งผ่าน exchange contract แยกต่างหาก ในแง่กลยุทธ์ NegRisk มีโอกาสเฉพาะสองอย่างที่ binary ไม่มี: cross-leg arb เมื่อผลรวมเบี่ยงจาก 1.0 และการสร้าง hedge โดยซื้อ YES หลาย legs
ต้นทุนที่เฉพาะกับ NegRisk: legs มากขึ้น = spread tax มากขึ้น (แต่ละ leg ที่เทรดมีต้นทุน spread ราว 0.5-1c), การเบี่ยงเบนจาก sum-to-1 ที่กว้างขึ้นในเหตุการณ์สภาพคล่องต่ำ (arb มักมีให้เห็นบ่อยกว่าแต่ขนาดเล็กกว่า)
invariant แบบ sum-to-1 และ arbitrage
หลักการของ arb: ถ้าการซื้อ YES legs ทั้ง N legs มีต้นทุนต่ำกว่า $1.00 คุณได้ล็อกกำไรที่การปิดตลาดอย่างแน่นอน (ต้องมีหนึ่ง leg จ่าย $1.00; ส่วนที่เหลือจะลงไปที่ $0)
ในทางปฏิบัติ ช่องว่างของ arb มักอยู่ที่ 0-3c และถูกกินไปด้วย spread + fees ของแต่ละ leg และมักหายไปภายในไม่กี่นาทีหลังเปิดตลาด ความจุถูกจำกัดโดย leg ที่มีสภาพคล่องบางที่สุด
arb นี้ยังขึ้นอยู่กับ failure modes ของการปิดตลาดเฉพาะเจาะจง: ผลลัพธ์แบบ "none of the above" ที่ปิดเป็น YES อย่างชัดเจนเมื่อไม่มีผู้สมัครที่ระบุคนใดเข้าเงื่อนไข หากเหตุการณ์มี leg แบบนี้และคุณไม่ได้ซื้อไว้ hedge "ครบชุด" ของคุณจะพลาด payout จริง
การสร้าง hedge แบบ leg-by-leg
ถ้าคุณถือสถานะใน NegRisk leg หนึ่ง leg คุณสามารถ hedge ได้โดยซื้อ YES ของ legs คู่แข่งในสัดส่วนที่เหมาะสม หากคุณถือ Trump-YES ที่ 0.50 และต้องการ hedge ความเสี่ยงจาก Trump แพ้ คุณจะซื้อพอร์ตของ legs อื่นที่มีชื่อระบุไว้
น้ำหนัก hedge ต่อ leg ≈ ความน่าจะเป็นโดยนัยปัจจุบันของ leg นั้นเมื่อเงื่อนไขเป็น Trump แพ้ สูตรประมาณ: weight_i = price_i / (1 - trump_price).
hedge นี้ไม่สมบูรณ์เพราะราคาที่ใช้เป็น point-in-time และความน่าจะเป็นแบบมีเงื่อนไขจะเปลี่ยนไปเมื่อมีข่าวเข้ามา ปรับ hedge ใหม่ทุกสัปดาห์หรือเมื่อมีข่าวสำคัญ อย่าทำให้ซับซ้อนเกินไป; จุดประสงค์ของ hedge คือการลด variance ไม่ใช่กำจัดมันทั้งหมด
Execution: neg_risk flag ใน orders
บั๊กที่พบบ่อยที่สุดและเฉพาะกับ NegRisk คือการลืมใส่ negRisk: true ใน payload ตอนส่ง order order จะถูก API รับ แต่ settlement จะผิดเพราะมันถูกส่งไปที่ standard CTF exchange แทนที่จะเป็น NegRisk exchange
// CORRECT for NegRisk markets:
await client.createAndPostOrder(
{ tokenID, price, size, side: Side.BUY },
{ tickSize: '0.01', negRisk: true }, // <-- REQUIRED
OrderType.FOK
);
แหล่งข้อมูลอ้างอิงที่ถูกต้อง: market.negRisk จาก Gamma API อ่านมัน แล้วส่งต่อไป อย่าตั้งค่า flag แบบเดาเอาเอง
บั๊กที่พบบ่อยในบอท NegRisk
จาก production debug logs ของบอทหลายตัว
- ลืม negRisk flag: order ถูกยอมรับ แต่ settlement ล้มเหลว วิธีแก้: บังคับตั้ง flag นี้ในทุก wrapper
- hedging โดยไม่รวม "Other" leg: ในเหตุการณ์ที่มีผลลัพธ์ "None of the above" พอร์ต hedge ที่ไม่รวมมันถือว่าไม่ครบ วิธีแก้: ตรวจสอบ Other leg ทุกครั้งเมื่อสร้าง hedge
- under-sizing ของ sum-to-1 arb: arber มองเห็น edge 1c แต่เทรดแค่ 5 shares ต่อ leg; กำไรรวมก่อนหัก spread มีแค่ 5 เซนต์ และสุทธิเป็นลบ วิธีแก้: ขนาด arb ควรใหญ่พอให้ได้กำไรเป็นดอลลาร์ที่มีนัยสำคัญ ไม่ใช่ไล่ตามเปอร์เซ็นต์ที่ดูดีบนหัวข้อข่าว
- ราคาของ leg ล้าสมัย: บอทดึงราคา 3 legs ใช้เวลารวม 200ms แล้วราคาของ leg สุดท้ายเปลี่ยนระหว่างดึงข้อมูล วิธีแก้: ดึงทุก legs แบบ parallel + มอง snapshot นั้นเป็นการสังเกตครั้งเดียว
โค้ด: snapshot ทุก legs และตรวจจับผลรวมต่ำกว่า 1.00
อ้างอิง: snapshot YES legs ทั้งหมดของเหตุการณ์ NegRisk แบบ parallel และตรวจหา arb
import asyncio, aiohttp
async def fetch_leg_ask(session, token_id):
async with session.get(f"https://clob.polymarket.com/book?token_id={token_id}") as r:
d = await r.json()
asks = d.get("asks", [])
return float(asks[0]["price"]) if asks else None
async def check_arb(event_slug):
event = await fetch_event(event_slug)
if not event["markets"][0]["negRisk"]: return None
legs = []
for m in event["markets"]:
toks = json.loads(m["clobTokenIds"])
yes_token = toks[0]
legs.append(yes_token)
async with aiohttp.ClientSession() as s:
asks = await asyncio.gather(*[fetch_leg_ask(s, t) for t in legs])
if any(a is None for a in asks): return None
total = sum(asks)
if total < 0.97:
return {"edge": 1 - total, "legs": list(zip(legs, asks))}
return None
การ execute ทุก legs แบบ atomic เป็นปัญหาที่ยากกว่า และต้องใช้ FOK แยกต่อ leg + rollback เมื่อ fill ไม่ครบ (รูปแบบคล้ายกับโค้ด stat-arb ในบทที่ 16)





