Bot mmbot 開発ログ

🛠️開発記録#275(2025/8/9)ShadowからCanaryへ──仮想通貨MM Bot本番移行の舞台裏と実運用ガード設計

1. はじめに

この記事では、私が開発している仮想通貨のマーケットメイク(MM)Botが、Shadow ModeからCanary Modeへ移行するまでのプロセスを振り返ります。
単に設定ファイルのモードを切り替えるだけではありません。本番稼働を視野に入れると、Botが安定して動き続けるための健全性確保、事故を未然に防ぐ安全ガード設計、そして現場で実際に起こり得るトラブルへの備えを組み込みながら進める必要があります。

本記事では、そうした技術的背景と実装の工夫を、具体的な改善ポイントとともに紹介します。


2. Shadow Modeの安定化と健全性確保

Canary Modeへ移行する前にやるべきことは、発注経路の健全性を徹底的に固めることです。ここが不安定なまま本番に入ると、思わぬ誤発注や不整合が即座に損失につながります。

発注系の型ずれ修正

まず取り組んだのは、内部で使っている発注リクエストの型ずれ解消です。

  • OrderRequest の定義を全経路で統一
  • factoryadapter 間での Shadow Mode 認識のブレを解消

これにより、モード判定の食い違いによる挙動不一致を防ぎました。

価格ガードの導入

次に、不健全な価格が板に乗らない仕組みを追加しました。
具体的には以下の条件をすべて満たすように制御します。

  • tick整合:JPYペアは1円刻みで丸め
  • ask > bid 保証:価格逆転を防止
  • 最小bp設定:設定未指定時は自動的に5bpを下限に採用
  • 上限制限の是正ask = min(ask, mid × 1.5) に修正
    (以前は誤って“下限”として扱っており、常時 mid×1.5 に引き上げられてしまっていた)

結果

この一連の修正により、恒常的に発生していた
[QUOTE_SANITY_FAIL] が完全に収束しました。
また、implied_bp(見込みスプレッド)はおおむね5bp前後に安定し、戦略設計時の想定通りの値で発注できる状態になりました。


このフェーズでの学びは、モード移行前に“価格の健全性”を数値で保証できる状態まで詰めることが重要だということです。
これが整えば、Canary Mode移行後の挙動確認やKPI測定も、安心して行える基盤になります。

3. 署名エラーと発注エラーの攻略

Shadow Modeでの健全性確保が整ったあと、次に立ちはだかったのがAPI呼び出し時のエラーです。
特に厄介だったのは、Binance系API特有の -1022(署名不正)と -2010(残高不足または銘柄制限)でした。

-1022(署名不正)の対応

まずは -1022 を解消するために、発生原因を一つずつ切り分けることから始めました。

  1. recvWindow=5000 の設定
    タイムスタンプの許容範囲を広げ、通信遅延やサーバ時刻との差異による拒否を回避。
  2. クエリ順序とフォーマットの固定
    Binanceはパラメータ順序や文字列フォーマットの揺れにも敏感なため、署名前に必ずソート順と整形ルールを統一。
  3. 科学記法の禁止
    数値が 1e-06 のような科学記法にならないよう、小数点以下の桁数を固定し文字列化。
  4. HMAC生成ロジックの検証
    実際の string_to_sign をログに出力し、ドキュメント記載の署名例と突き合わせて確認。

これらの修正により、署名の一貫性が保証され、 -1022 は完全に解消しました。


-2010(残高・銘柄制限)の対応

次に直面したのが -2010 です。
これは「口座残高不足」または「APIキーで許可されていない銘柄を指定」した場合に返されます。

対策として、発注前に全条件を満たすようにするバリデータ関数を導入しました。

  • round_price_qty_for_btcjpy の実装
    発注価格と数量を、
    • tickサイズ(JPY=1円)
    • stepサイズ(例: 0.000001BTC)
    • minNotional(最小約定金額=JPY100)
    • 残高(BTC.free / JPY.free)
      の全条件を事前にクリアするよう丸めと上限調整を行う。
  • 対象ペアの限定
    SOLJPYはAPIキーの銘柄許可外だったため、当面は BTCJPY のみに戦略対象を絞り込み。

これにより、実発注時に残高不足や銘柄制限で弾かれるリスクをなくしました。


4. Canary Modeの設計と安全ガード

署名と発注のエラーを解消した段階で、いよいよCanary Modeへの移行準備が整いました。
Canary Modeは、極小のポジションで実際に注文を出し、その挙動と市場反応を計測するためのモードです。本番稼働前の「試運転」フェーズといえます。

実装のポイント

  • Canary戦略モジュールの追加
    canary_strategy.py を実装し、通常戦略と差し替え可能に。
  • 環境変数による制御
    • CANARY_MODE : 有効化/無効化
    • CANARY_FRACTION : 全発注のうち実発注に回す割合(例: 1%)
    • 損失上限、建玉上限などのリスクパラメータ
  • post_only のトグル機能
    必要に応じて短時間だけ post_only=false に切り替え、実Fillを狙う。
  • Kill Switch / 連続失敗での停止
    発注エラーやFill失敗が一定回数続いた場合、自動的に実発注を停止。
  • 最小ノーション・上限制限
    • 最小約定金額:n
    • 最大建玉上限:n
    • 日次損失上限:n

こうして、最小限のリスクで実運用に踏み出せる安全なCanary環境が整いました。
次のステップは、この環境で小規模な実Fillを通し、Cancel計測などのKPIを取得することです。

5. 運用中のトラブルと対策

Canary Modeへの移行準備が整い、「いざパイロットFillを」と思った矢先、思わぬトラブルに遭遇しました。
それが IP一時BAN(HTTP 418 / -1003) です。

原因

調査の結果、REST APIの呼び出し頻度が高すぎて、Binance側のレート制限を超過していたことが原因でした。
一時的にIPが遮断され、一定時間(数十分)リクエストが通らなくなる状態に。

対策

再発防止のため、以下の改善を行いました。

  • 再起動ループの停止
    エラー時に自動再起動すると、BAN解除時間が延びるため、まずは再起動を止める仕組みを追加。
  • 二段レートリミッタ(1秒/1分)
    1秒間あたり・1分間あたりのリクエスト数を制限するトークンバケツ方式を導入。
  • 特定エラー時の自動クールダウン
    418 / 429 / -1003 を検知すると、一定時間(Retry-Afterや安全マージン)リクエストを停止。
  • 単一 aiohttp セッション+Keep-Alive+固定UA
    接続の再確立コストを削減し、API側からの拒否リスクを低減。
  • 起動時REST最小化
    • /time:起動時に1回だけ
    • /exchangeInfo:TTL 1時間
    • /account:TTL 30秒(残高更新はキャッシュを利用)
  • フラグ制御による不要REST抑止
    • ACC_LOG_ON_START=0:起動時残高ログOFF
    • SKIP_REST_CONNECT_TEST=1:接続テストスキップ
    • SYNC_SERVER_TIME_ON_START=1:サーバ時刻同期は必要な場合のみ

これらの対策で、BANリスクは大幅に低減しました。


6. 最初のFillへの手順設計

IP BANが解除されたら、いよいよ本番に向けた「最初の実Fill」へ進みます。
このパイロットFillは、極小の取引で挙動を確認し、計測指標(KPI)を取得するための重要な一歩です。

手順

  1. 起動
    [ACC] ログが1回だけ出力されればOK(残高確認)。
  2. 発注前チェック
    PRE_TRADE ログで notional >= 100 を満たしていることを確認。
    (BTCJPYのminNotionalはJPY100)
  3. パイロットFill
    • 安全版
      post_only=true のまま、最良価格に最小量を置き、10分間観察。
    • 迅速版
      60秒だけ post_only=false にして極小IOC/LIMIT注文を1発出す。
      60秒後に自動で post_only=true に復帰させる。
  4. キャンセル試験
    Fillが通ったら、直後にCancelを1〜2回発行。
  5. 計測
    • [CANCEL_METRIC] のp95/p99を集計(初回目標:p99 < 400ms)
    • implied_bp の分布を確認(4.9〜5.3bpに収束していれば合格)

このプロセスを経て初回KPIを取得すれば、次はCanary拡大量やレイテンシ分解によるボトルネック特定に進めます。
安全に一歩ずつ本番へ近づくための“通過儀礼”とも言えるフェーズです。

7. 次のチャレンジ

Canary Modeでの初回KPI取得が終わったら、次はいよいよ計測環境の高度化と改善サイクルに入ります。

Ember流レイテンシ分解

Emberのアプローチを参考に、発注処理を5つの段階に分解して計測します。

  • md(Market Data)→ 市場データ受信から
  • decide → 売買判断完了まで
  • prelat(Pre-Latency)→ OMSキュー投入からリクエスト準備まで
  • req(Request)→ HTTPリクエスト送信直前まで
  • ack(Acknowledge)→ サーバから応答を受信するまで

各段階の P50 / P95 / P99Prometheus に出力し、Grafanaなどで可視化します。
これにより、ボトルネックがネットワークなのか、戦略ロジックなのか、アダプタ層のI/O処理なのかを瞬時に特定できるようになります。


A/B実験設計

改善の効果を正しく評価するため、条件を変えたA/B実験を行います。

  • 条件軸
    • post_only の ON/OFF
    • notionalバケット(~100円 / 5k円 / 10k円など)
    • ボラティリティ regime(低 / 中 / 高)
  • 評価指標
    • Cancel p99(キャンセル応答の遅延分布)
    • Fill品質(Fill率、滑り幅、リジェクト率)

条件別に統計を取り、どの条件が高速・安定なFillを実現するかを検証します。


p99=250ms を目指す改善

初期目標の p99 < 400ms をクリアしたら、次は 250ms以下 をターゲットにします。

  • ネットワーク最適化
    • 低遅延ルートや近距離サーバへの配置
    • DNS固定やTCPコネクション再利用の徹底
  • Adapter I/O改善
    • 非同期処理の並列度調整
    • 不要なロックや待機の削減
  • 内部処理の短縮
    • quote生成ロジックの高速化(Rustバインディングの再有効化など)
    • JSONパースや数値変換の最適化

これらの改善は一度に行わず、A/B実験を繰り返しながら一つずつ効果を測定して進めます。


8. おわりに

ShadowからCanaryへの移行は、単なるモード切り替えではありませんでした。
安全に本番へ踏み込むための、基盤固めの工程です。

  • 発注精度の安定化
    tick整合や最小bp保証で、不健全な価格が市場に出ないようにしました。
  • APIの正しい使い方
    署名・フォーマット・レート制限の扱いを見直し、-1022や-2010のエラーを根絶。
  • 運用時のトラブルガード
    レートリミッタやクールダウン、フラグ制御で、運用中のBANや障害の影響を最小化。

これらは一度整備すれば、将来の戦略追加や市場拡張にも活きる長期的な資産になります。

次は、計測環境を整えてレイテンシの細部を可視化し、実験と改善のサイクルを回します。
その第一歩が、最初のFillとCancel p99採取です。
ここから、より速く・より安定した本番運用へ向けて走り出します。

9. おまけ:気になる点(改善余地)

  1. 価格ガードの一部条件が静的
    • 最小bp(5bp固定)や ask = min(ask, mid × 1.5) は市場状況によっては硬直的になり得る。
      → ボラティリティ連動で動的に閾値を変える設計があると、Canary拡大量フェーズでFill率を高めやすい。
  2. 発注前バリデーションの境界条件テスト不足が懸念
    • round_price_qty_for_btcjpy の丸めは、float誤差やPythonの丸め挙動で境界条件(minNotionalぴったり)を外す可能性。
      → Decimal型統一か、Binance側の LOT_SIZE/MIN_NOTIONALを元にしたstrict checkが欲しい。
  3. レート制限対策がREST前提
    • 現状REST呼び出し削減には取り組んでいるが、WebSocketの活用範囲拡大や深度更新イベントの差分利用が記載されていない。
      → 将来的にレイテンシ改善&API負荷軽減につながる。
  4. Kill Switchの条件設計が固定的
    • 「一定回数失敗で停止」というルールは、偶発的な一時エラーでも停止するリスクあり。
      → エラー種類別・時間窓別に閾値を変えるロジックの方が運用時の柔軟性が高い。
  5. 計測項目にネットワークパスの粒度不足
    • reqack の間の遅延だけでは、DNS解決・TCPハンドシェイク・TLSネゴの各フェーズの影響が見えない。
      → ネットワーク層のさらに細分化計測を追加すると、p99短縮の手が打ちやすい。

追加提案(やっておくと未来が楽になるやつ)

  • Configのハッシュ固定化
    Canary/Shadowの切替時に使用した戦略・パラメータをgitコミットハッシュやJSONスナップショットで残す。KPIと紐づけると分析しやすい。
  • 注文シミュレーションの事前検証パイプライン
    実発注前に、シミュレータに対してバリデーション通過率やFill予測を自動検証。回帰テストにも流用可能。
  • Alertの閾値自動調整
    KPIやエラー頻度に応じて自動で通知レベルを変えるAdaptive Alertにすると、運用負担が減る。
  • Canary Fractionの動的拡張
    p99やFill率が安定してきたら、自動でFractionを増やす仕組みを入れると、手動オペが減る。

-Bot, mmbot, 開発ログ