Polymarket Bot Tutorial · Chương 29 trong 32
Xây dựng một paper trading engine cho Polymarket trước khi chạy live: mô phỏng lệnh theo giá thực, theo dõi P&L, áp dụng cửa 30 giao dịch (>=55% win rate, +PnL) trước khi dùng vốn live, và khung code.
Chương này bao gồm những gì
Paper trading là bước không thể bỏ qua giữa ý tưởng chiến lược và triển khai live. Chương này là paper engine đơn giản đã chặn mọi live bot mà chúng tôi từng triển khai - dưới 200 dòng Python, theo dõi mọi giao dịch trong một diary JSONL, áp dụng cùng fee/slippage như luồng live.
- Vì sao phải paper trước khi live (luôn luôn)
- Cửa 30 giao dịch (được xác minh +55% WR + positive PnL)
- Xây dựng một paper engine đơn giản
- Theo dõi paper diary song song với live diary
- Khi paper lệch so với live (và vì sao)
- Chuyển sang live: khoản nạp nhỏ đầu tiên
- Code: paper engine tối giản
Vì sao phải paper trước khi live (luôn luôn)
Cửa 30 giao dịch paper là kỷ luật duy nhất phân tách 7.6% trader Polymarket có lợi nhuận với 84.1% người thua lỗ. Phần lớn builder bỏ qua bước này và phải trả học phí. Lý do thật sự khiến nó hiệu quả: paper trading cho thấy win rate thực của chiến lược trên đủ số mẫu để phân biệt tín hiệu với may rủi.
Bỏ qua paper tốn nhiều hơn số tiền tiết kiệm được. Một chiến lược trông có vẻ có lãi trong backtest nhưng thực ra chỉ là tung đồng xu sẽ đốt $200-500 vốn live trước khi tạo ra đủ 30 mẫu dữ liệu live. Paper-trade cùng 30 giao dịch đó tốn $0.
Paper engine không cần quá tinh vi. Nó cần trung thực - cùng fee, cùng slippage, cùng độ trễ fill như luồng live. Càng đơn giản càng tốt, vì bất cứ thứ gì mang tính tùy chọn đều sẽ bị cắt bớt và bot sẽ lên live sớm hơn đáng lẽ.
Cửa 30 giao dịch (được xác minh +55% WR + positive PnL)
Cửa này là dạng nhị phân: 30 paper trades đã đóng, tiêu chí thành công được viết sẵn từ trước (thường là WR ≥ 55% với một chiến lược positive-EV), hoặc không triển khai live.
30 là cỡ mẫu tối thiểu để khoảng tin cậy 95% của win rate thực đủ hẹp nhằm phân biệt tín hiệu với nhiễu. Dưới 30, một tỷ lệ quan sát 60% có thể tương ứng với tỷ lệ thực từ 45-75%. Từ 30 trở lên, khoảng này thu hẹp còn khoảng 50-70% - vẫn rộng, nhưng đủ để loại trừ chuyện "chiến lược chỉ là tung đồng xu."
Các tiêu chí thành công phải được đặt RA TRƯỚC khi paper run bắt đầu. Đặt sau đó sẽ tạo ra sự hợp lý hóa hậu nghiệm (bạn sẽ tìm ra cách diễn giải bất kỳ 30 giao dịch nào là "đủ tốt").
Xây dựng một paper engine đơn giản
Paper engine về cơ bản là code giao dịch live, chỉ thay hàm đặt lệnh bằng một fill mô phỏng. Phần mô phỏng:
- Đọc live order book: cùng lời gọi như bot live sẽ thực hiện.
- Mô phỏng fill: nếu mua theo FOK với giá ≥ best ask, fill lệnh ở mức volume-weighted average của các ask đã bị consume; ghi fill vào paper diary.
- Áp dụng fee: trừ cùng mức fee mà luồng live sẽ trả.
- Theo dõi inventory: duy trì một paper-balance và paper-positions dictionary song song.
Cả engine chỉ gói trong 100-200 dòng Python. Kỷ luật quan trọng: mọi giả định mà luồng live dùng (fill rate, latency, fee) đều phải được tái hiện trong paper, kể cả khi hơi xấu hơn thực tế - paper nên là sàn, không phải trần.
Theo dõi paper diary song song với live diary
Paper trading run tạo ra một diary JSONL không khác gì về cấu trúc so với live diary mà bot sẽ ghi sau này. Cùng các field: timestamp, action, market_slug, side, size, price, expected_fill_price, simulated_pnl_at_exit.
Có hai lý do để dùng cùng định dạng. Thứ nhất, các công cụ phân tích đọc live trades (P&L reports, win-rate calculators) sẽ chạy trên paper mà không cần chỉnh sửa. Thứ hai, việc so sánh paper với live sau này sẽ phát hiện những lệch pha cho thấy bug.
Mẹo production: để paper engine ghi vào per_trade_paper.jsonl trong cùng thư mục với live per_trade.jsonl. Một lệnh duy nhất sẽ so sánh cả hai: diff -y <(jq -r .market_slug per_trade.jsonl) <(jq -r .market_slug per_trade_paper.jsonl).
Khi paper lệch so với live (và vì sao)
Sự lệch giữa paper và live là điều không tránh khỏi. Ba nguyên nhân phổ biến.
- Slippage: paper fill theo snapshot của ask; live sẽ đi qua book và có thể fill xấu hơn 1-2c ở các thị trường mỏng. Giải pháp: mô phỏng slippage trong paper bằng cách thêm một penalty cho mỗi giao dịch bằng một nửa spread.
- Fill latency: paper fill ngay lập tức; live mất 200-500ms, trong lúc đó giá có thể di chuyển. Giải pháp: mô phỏng bằng cách chờ rồi đọc lại book trước khi "fill" trong paper.
- Adverse selection: paper giả định bạn lấy được best ask; live phải cạnh tranh với các bot khác vốn có thể đã lifting ask đó. Giải pháp: khó mô phỏng hơn; hãy thẳng thắn với chính mình rằng paper đang đánh giá quá cao.
Khi paper nói +5%/tháng còn live chạy ở -2%/tháng, chênh lệch thường là một trong ba thứ này. Hãy audit từng mục thay vì mặc định rằng bản thân chiến lược đã sai.
Chuyển sang live: khoản nạp nhỏ đầu tiên
Paper vượt qua 30 giao dịch. Kế hoạch triển khai live:
- Nạp $25-50 làm vốn smoke-test. Hãy coi đó là học phí; nếu mất nó, bài học vẫn đáng giá.
- Chạy bot ở chế độ live trong 5-10 giao dịch với vị thế ở size tối thiểu (5 shares).
- Xác minh mỗi fill khớp với kỳ vọng paper trong phạm vi 2c. Điều tra mọi khoảng lệch lớn hơn trước khi tiếp tục.
- Nếu 5-10 giao dịch live khớp với paper, nạp $200-500 và chạy vị thế kích thước bình thường.
- Nếu không khớp, dừng lại, debug, sửa lỗi, rồi bắt đầu lại từ bước 1.
Khoảng lệch live-paper phổ biến nhất ở lần triển khai đầu tiên là thiếu fee hoặc ước tính slippage sai. Sửa những thứ đó khá đơn giản; kỷ luật nằm ở việc phát hiện khoảng lệch trước khi tăng vốn.
Code: paper engine tối giản
Tham khảo: paper engine đơn giản đọc live book + mô phỏng fill FOK.
import json, time
PAPER_BAL = 10_000.0 # USD starting
positions = {} # token_id -> shares
def paper_fok_buy(token_id, max_price, size):
book = fetch_book(token_id)
# Walk asks, fill what we can within max_price
filled = 0; cost = 0
for level in book.asks:
px = float(level["price"])
if px > max_price: break
avail = float(level["size"])
take = min(avail, size - filled)
filled += take
cost += take * px
if filled >= size: break
if filled < size:
return {"status":"rejected","filled":0} # FOK semantics
global PAPER_BAL
PAPER_BAL -= cost
positions[token_id] = positions.get(token_id, 0) + filled
log_paper({"ts": int(time.time()), "action":"buy",
"token": token_id, "size": filled, "price": cost/filled})
return {"status":"matched","filled":filled,"cost":cost}
Bổ sung cho production: paper sell function (mirror của buy), paper GTC simulation (post lên book ở một mức giá, mô phỏng fill khi mid chạm giá đó), reconciliation giữa paper diary và "would-have-been" live diary.





