Polymarket Bot Tutorial · 32章中29章目
本番稼働前にPolymarketのペーパートレーディングエンジンを構築しましょう。実際の価格に対して注文をシミュレートし、P&Lを追跡し、実資金投入の前に30トレードゲート(勝率55%以上、プラスのPnL)を適用し、コードの骨組みを作ります。
この章で扱う内容
ペーパートレーディングは、戦略アイデアと本番デプロイの間にある必須のステップです。この章では、これまで公開したすべての本番ボットにゲートとして使ってきたシンプルなペーパーエンジンを紹介します。Python 200行未満で、すべての取引をJSONLの日誌に記録し、本番ルートと同じ手数料・スリッページを適用します。
- なぜ本番前にペーパーが必要か(常に)
- 30トレードゲート(検証済みの勝率55%以上 + 正のPnL)
- シンプルなペーパーエンジンの構築
- ライブの日誌と並行してペーパージャーナルを追跡する
- ペーパーが本番と乖離する場合(その理由)
- 本番へ移行するタイミング:最初は少額入金
- コード:最小限のペーパーエンジン
なぜ本番前にペーパーが必要か(常に)
30トレードのペーパーゲートは、利益を出しているPolymarketトレーダー7.6%と損失を出している84.1%を分ける、たった一つの規律です。多くのビルダーはこれを省略し、授業料を払います。正直に言うと、これが機能する理由は、ペーパートレーディングによって十分なサンプル数で戦略の真の勝率を明らかにでき、シグナルと運の差を見分けられるからです。
ペーパーを飛ばすと、節約できる以上のコストが発生します。バックテストでは利益が出ているように見えても、実際はコイントス程度の戦略なら、ライブ資金の$200-500を燃やしてから、30サンプル分のライブデータが得られるだけです。同じ30トレードをペーパーで回せばコストは$0です。
ペーパーエンジンは高度である必要はありません。必要なのは誠実さです。つまり、本番ルートと同じ手数料、同じスリッページ、同じ約定遅延を再現することです。シンプルであるほど良いです。というのも、任意機能があると削られ、本来より早くボットが本番投入されてしまうからです。
30トレードゲート(検証済みの勝率55%以上 + 正のPnL)
このゲートは二択です。30件のクローズ済みペーパートレード、事前に定義した成功条件(通常は正の期待値戦略で勝率≥55%)、これを満たさなければ本番デプロイはしません。
30は、真の勝率に対する95%信頼区間が、シグナルとノイズを区別できるほど十分に狭くなる最小サンプルサイズです。30未満では、観測上60%の勝率でも真の勝率は45〜75%に相当し得ます。30を超えると、この区間はおよそ50〜70%に狭まります。まだ広いですが、「この戦略はコイントスだ」という判断は排除できます。
成功条件は、ペーパー開始前に必ず設定しなければなりません。後から設定すると、事後的な正当化につながります(どんな30トレードでも「十分良い」と解釈する余地を作ってしまうためです)。
シンプルなペーパーエンジンの構築
ペーパーエンジンは基本的に、本番の売買コードから注文発行部分をシミュレーション約定に差し替えたものです。シミュレーションでは次の処理を行います。
- ライブの板情報を読む: 本番ボットが行うのと同じ呼び出しを使う。
- 約定をシミュレートする: FOKで買い、価格が最良売り気配以上なら、消費した売り板の出来高加重平均で約定させ、ペーパー日誌に記録する。
- 手数料を適用する: 本番ルートで支払うのと同じ手数料を差し引く。
- 在庫を追跡する: ペーパー残高とペーパー保有ポジションの辞書を並行して維持する。
エンジン全体はPython 100〜200行に収まります。重要なのは、本番ルートが前提にしているあらゆる仮定(約定率、遅延、手数料)を、ペーパーでも再現することです。たとえ現実より少し悪くしてでもです。ペーパーは上限ではなく下限であるべきです。
ライブの日誌と並行してペーパージャーナルを追跡する
ペーパートレーディングの実行結果は、後でボットが書くライブ日誌と構造上区別できないJSONL日誌を生成します。フィールドは同じです。timestamp, action, market_slug, side, size, price, expected_fill_price, simulated_pnl_at_exit。
同じ形式を使う理由は2つあります。第一に、ライブ取引を読む分析ツール(PnLレポート、勝率計算機)が、そのままペーパーでも使えること。第二に、後でペーパーとライブを比較することで、バグを示す乖離を見つけられることです。
運用のコツ: ペーパーエンジンは、ライブのper_trade.jsonlと同じディレクトリにper_trade_paper.jsonlを書き出すようにします。1コマンドで両者を比較できます。diff -y <(jq -r .market_slug per_trade.jsonl) <(jq -r .market_slug per_trade_paper.jsonl)
ペーパーが本番と乖離する場合(その理由)
ペーパーと本番の乖離は避けられません。よくあるものを3つ挙げます。
- スリッページ: ペーパーはスナップショットの売り気配で約定しますが、本番では板を食い進めるため、薄い市場では1〜2セント不利になることがあります。解決策: ペーパーで1回の取引ごとにスプレッドの半分に相当するペナルティを加えてスリッページをシミュレートする。
- 約定遅延: ペーパーは即時約定しますが、本番では200〜500msかかり、その間に価格が動く可能性があります。解決策: ペーパーでも待機してから板を再読込し、その後に「約定」させる。
- 逆選択: ペーパーは最良売り気配で取れると仮定しますが、本番ではその気配をすでに他のボットが取り去っていることがあります。解決策: シミュレーションは難しいので、ペーパーが過大評価しがちだと自分に正直に認識すること。
ペーパーでは月+5%なのに、本番では月-2%になる場合、その差はたいていこのどれかです。戦略自体が間違っていたと決めつけず、1つずつ監査してください。
本番へ移行するタイミング:最初は少額入金
ペーパーで30トレードを通過したら、本番デプロイの計画は以下の通りです。
- $25-50を検証用資金として入金する。授業料だと考え、失っても学びの価値があったとみなす。
- 最小サイズ(5シェア)で、5〜10トレードだけボットを本番モードで動かす。
- 各約定がペーパーの予想と2セント以内で一致するか確認する。差がそれ以上なら先に進まず調査する。
- 5〜10回の本番トレードがペーパーと一致したら、$200-500を入金し、通常サイズのポジションで運用する。
- 一致しない場合は停止し、デバッグして修正し、ステップ1からやり直す。
初回デプロイで最も多いペーパーと本番の差は、手数料の抜け落ちかスリッページ見積もりの誤りです。そこを修正するのは難しくありません。重要なのは、資金を拡大する前にその差を見つける規律です。
コード:最小限のペーパーエンジン
参考: ライブの板を読み込み、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}
本番用の追加要素: ペーパー売却関数(買いの反対)、ペーパーGTCシミュレーション(価格を付けて板に出し、ミッドがその価格に達したら約定をシミュレート)、ペーパー日誌と「起こり得た」ライブ日誌の照合。





