Polymarket Bot Tutorial · Chapter 29 of 32
Побудуйте paper trading engine для Polymarket перед запуском у live: симулюйте orders за реальними цінами, відстежуйте P&L, впровадьте 30-trade gate (>=55% win rate, +PnL) перед будь-яким live capital, і створіть code skeleton.
Що охоплює ця глава
Paper trading - це обов'язковий крок між ідеєю стратегії та live deployment. Ця глава описує простий paper engine, який стояв за кожним live bot, що ми запускали - менше ніж 200 рядків Python, відстежує кожну trade у JSONL diary, застосовує ті самі fees/slippage, що й live path.
- Чому paper перед live (завжди)
- 30-trade gate (підтверджені +55% WR + positive PnL)
- Створення простого paper engine
- Ведення paper diary паралельно з live diary
- Коли paper diverges від live (і чому)
- Перехід до live: невеликий перший deposit
- Code: мінімальний paper engine
Чому paper перед live (завжди)
30-trade paper gate - це єдина дисципліна, що відділяє 7.6% прибуткових Polymarket traders від 84.1% тих, хто втрачає гроші. Більшість builder-ів пропускають цей крок і платять за навчання. Чесна причина, чому це працює: paper trading показує реальний win rate стратегії на достатній вибірці, щоб відрізнити сигнал від luck.
Пропуск paper коштує більше, ніж економить. Стратегія, яка виглядає прибутковою у backtest, але насправді є coin flip, спалить $200-500 live capital, перш ніж накопичиться 30-sample size live data. Paper-trading тих самих 30 trades коштує $0.
Paper engine не має бути складним. Він має бути чесним - ті самі fees, той самий slippage, та сама fill latency, що й у live path. Чим простіше, тим краще, бо все необов'язкове зріжеться, і bot вийде в live раніше, ніж слід.
30-trade gate (підтверджені +55% WR + positive PnL)
Gate є бінарним: 30 закритих paper trades, заздалегідь визначені критерії успіху (зазвичай WR ≥ 55% для стратегії з positive EV), або жодного live deployment.
30 - це мінімальний sample size, за якого 95% confidence interval для true win rate достатньо вузький, щоб відрізнити signal від noise. Нижче 30 спостережуваний rate 60% може відповідати true rate 45-75%. На 30+ інтервал звужується приблизно до ~50-70% - все ще широкий, але вже достатній, щоб відкинути думку «стратегія - це coin flip».
Критерії успіху потрібно встановити ДО початку paper run. Якщо робити це після, виникає post-hoc rationalization (ви знайдете спосіб інтерпретувати будь-які 30 trades як «достатньо добре»).
Створення простого paper engine
Paper engine - це, по суті, live trading code, у якому функцію order-placement замінено на simulated fill. Симуляція:
- Read live order book: той самий виклик, який зробив би live bot.
- Simulate fill: якщо buying на FOK із ціною ≥ best ask, заповніть order за volume-weighted average of asks consumed; запишіть fill у paper diary.
- Apply fees: відніміть ті самі fees, які сплатив би live path.
- Track inventory: підтримуйте паралельні paper-balance та paper-positions dictionary.
Увесь engine вміщується в 100-200 рядків Python. Головна дисципліна: кожне припущення, яке робить live path (fill rate, latency, fee), має бути відтворене в paper, навіть якщо трохи гірше за реальність - paper має бути floor, а не ceiling.
Ведення paper diary паралельно з live diary
Paper trading run створює JSONL diary, який за структурою не відрізняється від live diary, що bot писатиме пізніше. Ті самі поля: timestamp, action, market_slug, side, size, price, expected_fill_price, simulated_pnl_at_exit.
Є дві причини використовувати той самий формат. По-перше, analysis tools, які читають live trades (PnL reports, win-rate calculators), працюють на paper без змін. По-друге, пізніше порівняння paper із live допомагає виявити divergences, які вказують на bugs.
Порада для production: нехай paper engine записує в per_trade_paper.jsonl у тій самій директорії, що й live per_trade.jsonl. Одна команда порівнює обидва: diff -y <(jq -r .market_slug per_trade.jsonl) <(jq -r .market_slug per_trade_paper.jsonl).
Коли paper diverges від live (і чому)
Неминучі divergences між paper і live. Три найпоширеніші.
- Slippage: paper виконує fill за snapshot ask; live проходить по book і може заповнитися на 1-2c гірше на thin markets. Рішення: симулюйте slippage в paper, додаючи per-trade penalty, що дорівнює половині spread.
- Fill latency: paper заповнюється миттєво; live займає 200-500ms, за які price може змінитися. Рішення: симулюйте це, чекаючи й повторно читаючи book перед «fill» у paper.
- Adverse selection: paper припускає, що ви отримуєте best ask; live конкурує з іншими bots, які могли вже забрати той ask. Рішення: важче симулювати; чесно визнайте собі, що paper переоцінює результат.
Коли paper показує +5%/month, а live працює на -2%/month, розрив зазвичай спричиняє один із цих факторів. Перевіряйте їх по одному, а не припускайте, що сама strategy була неправильною.
Перехід до live: невеликий перший deposit
Paper проходить 30 trades. План live deployment:
- Deposit $25-50 як smoke-test capital. Сприймайте це як навчання; якщо втратите гроші, урок був того вартий.
- Запустіть bot у live mode на 5-10 trades з positions мінімального розміру (5 shares).
- Перевірте, що кожен fill відповідає paper expectations у межах 2c. Дослідіть будь-який більший розрив перед продовженням.
- Якщо 5-10 live trades збігаються з paper, внесіть $200-500 і запускайте positions нормального розміру.
- Якщо не збігаються, зупиніться, debug, виправте, перезапустіть із кроку 1.
Найпоширеніший live-paper gap під час першого deployment - це пропущені fees або неправильна оцінка slippage. Виправити це просто; дисципліна полягає в тому, щоб виявити gap до масштабування capital.
Code: мінімальний paper engine
Reference: простий paper engine, який читає live book + симулює FOK fill.
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}
Production additions: paper sell function (mirror of buy), paper GTC simulation (post on book at price, simulate fill when mid reaches price), reconciliation between paper diary and "would-have-been" live diary.





