Bot CEX DEX 開発ログ

🛠️開発記録#280(2025/8/12)個人トレーダーのための“高速×安全”設計——p99、レートリミット、中央制御

この記事は、個人開発の高速ボットを“安全に勝たせる”ための設計原則をまとめた実戦ドキュメントです。対象は CEX 中心に自作ボットを運用している個人トレーダー/エンジニア。先に要点だけ:

  • p99 ≤ 250ms(md→decide→prelat→req→ack の往復)を安定達成
  • WSファーストTTLキャッシュで REST を最小化
  • レートリミットは中央ブローカーで一括吸収(Permit/Report 型)
  • 429/403/BANの“尾(p95/p99)”を潰す安全装置を先に入れる

“速さ”より“尾と構造”。これが個人でも届く勝ち筋です。

「尾(お)」って何?

  • この記事でのは、レイテンシ(や失敗率)の分布の“末端側”(テール)、つまり p95・p99 等の高分位を指します。
  • 例:p99 = 250ms は「99% のリクエストは 250ms 以下だが、残り 1% はそれ以上かかる」ことを意味。
  • なぜ重要?中央値(p50)が速くても、**まれな遅延スパイク(尾)**が起きると
    • キャンセルが遅れて逆選択で被弾
    • 見積りが古くなってスリッページ増加
    • 連鎖的に429/403を誘発
      …と P/Lを壊すのは尾だから。だから“速さ”より尾を詰める設計を重視しています。

TL;DR(要点だけ)

  • 測る→詰める→守るの順:レイテンシ5段の p95/p99 を計測→ボトルネックを潰す→事故らないガードを常時ON。
  • 最小SLOmm_total_latency_ms p99 ≤ 250ms / mm_cancel_latency_ms p99 / newOrder 2xx率 / 429・403率
  • 導入ステップ:①計測 ②WS化 ③地域最適VPS ④中央レート制御 ⑤安全装置 ⑥A/B最適化。

レイテンシを“分解して測る”

Ember流 5 段(名称は任意でOK、分解が重要)

  • md:WS差分を板に適用
  • decide:戦略判定(クォート/成行/キャンセル)
  • prelat:署名・整形・シリアライズ
  • req:送信(ネットワークへ発射)
  • ack:ACK受信(HTTP応答 or WS通知)

計測の作法

  • 5段すべてに Histogram(ms)を出す。一次指標は mm_total_latency_ms = ack(ts) - md(ts)
  • cancel p99newOrder 2xx率をペアで監視。
  • バケットは指数で 800ms 以上をカバー。
# Prometheus: metrics.py(例)
from prometheus_client import Histogram, Counter, Gauge

def exp_buckets(start=8.0, factor=1.10, count=45):
    v=[]; x=start
    for _ in range(count):
        v.append(float(f"{x:.6f}")); x *= factor  # 重複なしの単調増加
    return v

MM_TOTAL = Histogram("mm_total_latency_ms", "End-to-end latency (md->ack)", ["symbol"], buckets=exp_buckets())
MM_CANCEL = Histogram("mm_cancel_latency_ms", "Cancel RTT", ["symbol"], buckets=exp_buckets())
MM_HTTP_429 = Counter("mm_http_429_total", "HTTP 429 responses", ["endpoint"])
MM_HTTP_403 = Counter("mm_http_403_total", "HTTP 403 responses", ["endpoint"])
MM_CANCELLED = Counter("mm_cancelled_total", "Intended cancels", ["reason"])
関連記事
🛠️開発記録#278(2025/8/11)レイテンシ分解:CEX / CEX×DEX / DEXで違う“速さ”——個人botterのための周波数表と設計原則

続きを見る

p99 ≤ 250ms の“根拠”

この数値は根拠にもとづく逆算です。

  • 板更新周期:主要CEXの板更新は 100/250/500ms モードが一般的。2〜3サイクル跨ぎで見積もりが古くなる。
  • 地域RTTの現実:近傍リージョン(例:東京↔SG/HK)で req→ack p95≒120〜180ms が現実的。
  • 非同期ACK:HTTPのACKは確定ではなく、約定は WS で追う設計が多い。
    → これらの足し算+余白で p99 が 200〜270ms 帯250msに押し込めば“古い見積りで殴られにくい”安全域に入る。
    ※ イベント瞬発系は ≤150ms を狙い、マルチヘッジ前提なら ≤300ms を許容する、のが実務的な使い分け。

取引所別・レートリミットの実相(ドキュメント準拠の要点)

  • Binance系
    • 重み(REQUEST_WEIGHT)×IP が基本。使重量はレスポンスヘッダで返る。
    • 超過で 429、悪化で BAN相当(実装依存)。
    • WS はメッセージ/接続上限あり。再接続スパイクは即死因。
  • Bybit系
    • UIDの1秒ローリングIPの短期窓が併用。
    • 超過で 403、公式推奨は一定時間の完全クールダウン
    • 取引系は HTTPとWSのバッチが同一アカウント枠を共有(一元管理が前提)。

※ 数値は変更され得る。運用前に公式の現行値を再確認しよう。

深掘り;p99 ≤ 250ms の“根拠”(実務での逆算)

結論の 250ms は“雰囲気”ではなく、(A) 板更新の周期 × (B) 地域間RTT × (C) ACKの非同期性を足し合わせた工学的ラインです。順にいきます。

A. 板更新周期(100 / 250 / 500ms の世界)

主要CEX(例:Binance)の板ストリームは、差分(diff depth)/部分板(partial depth)ともに 100ms・250ms・500ms の更新モードが用意されています。JSONに加えて SBE 版でも diff depth 100ms が案内されています。2〜3 サイクル跨ぐとローカル見積りが古くなるため、250ms 以内に反応できれば「2〜3サイクル内追随」を満たしやすい、というのが最初のピース。
バイナンス開発者センター+2
GitHub

目安:100ms 更新なら2サイクル=200ms, 3サイクル=300ms。
250ms はこのレンジの安全側に置く設計。

B. 地域RTTの現実(東京↔SG/HK)

実ネットワークの往復遅延はクラウド事業者や国際回線に依存しますが、東アジア〜東南アジアのメジャールートはおおむねこのレンジです:

  • 東京↔香港:Azure の公式 VM↔VM 測定(P50)で ≈53ms(Japan East↔East Asia)。Microsoft Learn
  • 東京↔シンガポール:大手キャリアの月次 IP レイテンシ実測で ≈72ms(P50級)Verizon

これらは中央値(P50)の目安です。実務では混雑や経路変化でP95〜P99 は P50 の 1.3〜2.0 倍になることが珍しくありません(一般論)。つまり、“HTTPの req→ack” だけでも ~120–180ms 程度の裾(p95)は十分あり得る、という前提で積み上げます(この部分は公開統計からの推論)。

C. ACKは“確定”ではない(非同期ACK設計)

多くの取引所はHTTPの新規注文APIの応答=“受け付けた/受理できない”通知であり、約定の確定はユーザーデータのWSで追うのが基本設計です。

  • BinancenewOrderRespTypeACK/RESULT/FULL を選べるが、最終的なステータスは User Data Stream(executionReport / ORDER_TRADE_UPDATE)でリアルタイム受信する前提。バイナンス開発者センター+2
  • Bybit注文作成/キャンセルは非同期で、**“リアルタイムはWS推奨(order トピック)”**と公式に明記。同時充足(キャンセル投げた瞬間に約定)など非同期事象の注意書きもあります。Bybit Exchange+1

つまり、HTTP応答が 50–150ms で返っても、それは“確定”ではない
“md→decide→prelat→req→ACK→(成否はWS)”という非同期パイプを前提に、エンドツーエンドの反応時間を設計する必要があります。

足し算:なぜ「p99 ≤ 250ms」なのか

上の A/B/C を「古い見積もりで殴られない」観点で逆算します。

  • 板の鮮度:100ms 周期の板に対し 2〜3サイクル内で反応したい → 200〜300ms が“鮮度許容帯”。バイナンス開発者センター+1
  • ネット往復:近傍(東京基点で SG/HK)の P50 ≈ 50–75msP95 はその1.3〜2倍が現実的(推論)。Microsoft LearnVerizon
  • アプリ/署名/整形のオーバーヘッドと、取引所側の処理バラつき(キュー/スロットリング)を**+α**で見積もる。

→ 保守的に見ると、p99 を 200–270ms 帯に収めれば「板の 2〜3サイクル内追随」をほぼ満たし、古い見積りによる逆選択リスクを大きく減らせます。設計SLOとして 250msを置くのはこのためです(A のハード制約B/C を足し引きした現実解)。

実務Tips:

  • イベント瞬発系(清算通知/ラダーの一気食い)は≤150msを狙う(板 1〜1.5 サイクル内で刺す)。
  • マルチヘッジで価格ドリフトに強い構造なら**≤300msまで許容し、約定品質重視に振る。
    (この 150/300ms は実運用の
    ヒューリスティック**です。あなたの配備で p95/p99 を実測して再調整してください)

参考資料(一次情報中心)

  • Binance(WebSocket 市場データ)
  • Binance(注文応答とユーザーデータ)
  • Bybit(非同期とWS推奨)
    • 「注文作成/キャンセルは非同期。リアルタイムは WS 推奨」の明記、order トピックの挙動。Bybit Exchange+1
  • 地域RTTの実測
    • Azure 公式の地域間 P50 往復遅延(例:Japan East↔East Asia ≈53ms)。Microsoft Learn
    • Verizon 公開の月次IPレイテンシ(例:Singapore↔Tokyo ≈72ms、Hong Kong↔Tokyo ≈48–55ms)。Verizon

註:RTTは経路/時間帯/事業者で揺れます。**SLOは“測ってから決める”**が鉄則。まずは自分の配備(VPS/回線/署名パス)で md→ack のヒストグラムを取り、p95/p99で線を引き直してください。

実戦テンプレ(共通パターン)

  • WSファースト:板/約定はWS常時同期。RESTは初期スナップショット+例外補修のみに。
  • 二段レート制御
    1. グローバル窓(IP/UID×時間窓)
    2. エンドポイント別(重み/コスト差を反映)
      常に20%のヘッドルームを残す(スパイク吸収)。
  • TTL/SWR/singleflight:重複呼び出しを吸収。
  • 429/403:指数バックオフ+ジッター、Retry-After やリセット秒読みを素直に尊重
  • 可視化429・403率 / used_weight比 / wait_ms / BAN/クールダウン状態 をダッシュボード常設。

取引所別・実装メモ(Binance/Bybit)

Binance

  • 重みヘッダを読む→動的スロットリング
  • /depth の大 limit 連打はやめて WS増分+小さめ limit に置換。
  • BAN相当は即クールダウン→段階復帰(低QPS→通常)。
cost = COST[endpoint]
if used_weight_1m + cost > limit_1m * 0.8:
    sleep(until_reset())
resp = http_call()
if resp.status in (429, 418):
    sleep(retry_after_or_default()); backoff.exponential()
update_used_weight_from_headers(resp.headers)

Bybit

  • UID 1秒窓 × IP短期窓の両方をチェック。
  • 403 検知は全HTTP停止→所定時間後に段階復帰
  • 小バッチ注文で枠効率を最大化(HTTP/WSの枠を共有する前提)。
if uid_tokens_1s < cost or ip_tokens_short < cost:
    sleep(next_refill())
resp = http_call()
if resp.status == 403:
    freeze_all_http(minutes=10)
update_tokens_from_headers(resp.headers)

中央レート制御レイヤー(Rate-Limit Broker)

複数ボットが取引所を叩くに通る許可ゲートexchange×uid(API鍵)×ip×endpoint_class の残枠を一括管理し、Permit/Report で安全に配給します。

graph LR
  BA[Bot A] -->|Permit cost| RL[Rate-Limit Broker]
  BB[Bot B] -->|Permit cost| RL
  RL -->|lease or wait_ms| BA
  RL -->|lease or wait_ms| BB
  BA -->|HTTP / WS| EX[Exchange]
  BB -->|HTTP / WS| EX
  BA -->|Report headers & status| RL
  BB -->|Report headers & status| RL

キー設計

  • Binance:(binance, ip, endpoint_class)1分重み
  • Bybit:(bybit, uid, endpoint_class)1秒窓(bybit, ip)短期窓同時チェック

運用ロジック

  • 優先度付き公平キュー(発注系>参照系)
  • 学習:429/403 とヘッダから窓サイズ/余白を補正
  • 自動クールダウン:403/418 をアカウント単位で即反映
  • 観測permit_q_depth / wait_ms_p95 / cooldown_active / used_ratio

メモ:もう一段上の解決(構造で勝つ)

  • Market Data Hub:WS購読を1本に集約→共有メモリ/IPC/Redis で全ボットへ配給(REST多重取りを絶滅)。
  • Connection Orchestrator:接続プール+サブスク切替で再接続スパイクを排除。
  • Adaptive Budgeting:ボラ/時間帯/イベントでレート枠を動的再配分(利用率80%を目標にPID的制御)。

戦略別の“必要速度と勝ち筋(&根拠)”

  • 板寄りMMp99 ≤ 250ms。根拠=板更新 100〜250ms × 地域RTTの足し算。cancel p99 厳格管理/毒度フィルタ在庫スキュー
  • CEX–DEX 裁定比較 ≤ 250ms両建て ≤ 500–700ms(例:Solana スロット粒度+DEX実行)。実効コスト(滑り/ガス/ブリッジ)見積り精度が命。
  • 押し引きスキャル≤ 150ms を狙う(FIFO下の Queue 価値、1〜1.5周期内反応)。
  • 清算スナイプ(CEX)≤ 200–250ms+誤爆最小化(通知粒度+地域RTTを踏まえた反応設計)。
  • DEX系MEV:速度装備ゲー(PBS/Jito 等の専用経路)。構造エッジ(JIT LP/再平衡/特殊ルール)で勝つ。

安全装置(Fail-Closed / Fail-Open)

  • 在庫上限LIVE_MAX_NOTIONAL_USD, LIVE_MAX_PARALLEL
  • 連敗/日次PnLガード:JST日次リセットで0化
  • stalenessゲート:ボラ上昇でクォートTTL短縮&キャンセル強化
  • 自動クールダウン:429/403/BAN検知→指数バックオフ→段階復帰
  • 発注=Fail-Closed / 参照=Fail-Open の原則で作る

観測とSLO運用

最小メトリクス

  • mm_total_latency_ms(md→ack) / mm_cancel_latency_ms
  • mm_http_429_total, mm_http_403_total
  • mm_used_weight_1m, mm_uid_rate_1s, mm_ip_rate_short(Gauge)
  • mm_cancelled_total(命名は統一)

PromQL例

histogram_quantile(0.99, sum(rate(mm_total_latency_ms_bucket[5m])) by (le))
histogram_quantile(0.99, sum(rate(mm_cancel_latency_ms_bucket[5m])) by (le))
sum(rate(mm_http_429_total[5m])) by (endpoint)
sum(rate(mm_http_403_total[5m])) by (endpoint)

よくある落とし穴&アンチパターン

  • 同一IPで多ボット衝突 → 中央レート制御で一括管理
  • 再接続スパイク → 接続プール+サブスク切替
  • /depth 大 limit 乱用 → WS増分+小 limit
  • 公式数値を“固定値”と信じる → リリース毎に再確認&ブローカーで自動学習

導入ロードマップ(個人向け)

  1. 測る:5段 Histogram と SLO を出す
  2. WS ファースト化:REST は初期化+補修のみに
  3. ネット最適:地域最適VPS、HTTP/2常駐、署名/整形のホットパス高速化
  4. 中央レート制御:Rate-Limit Broker 導入(ヘッドルーム20%死守)
  5. 安全装置:在庫上限・連敗/日次PnL・staleness・自動クールダウン
  6. A/B最適化:TTL・スプレッド・サイズ・毒度フィルタの 4 軸で回す

まとめ

“速さ”より“尾”と“構造”。
p99 を 250ms 以内に押し込み、レート制御を中央化し、WS ファーストと安全装置で事故を未然に潰す。個人でも十分届く設計で、速度の腕相撲から卒業しよう。

付録A:最小擬似コード(Broker & Bucket)

Rate-Limit Broker(Permit/Report)

# server.py(概念スケッチ)
def permit(ex, uid, ip, endpoint, cost, priority=0):
    k_global = (ex, scope_key(ex, uid, ip, endpoint))  # binance=IP/1m, bybit=UID/1s + IP/5s
    k_local  = (ex, endpoint)
    if ok(tokens[k_global], cost) and ok(tokens[k_local], cost) and not cooldown[(ex, uid, ip)]:
        consume(tokens[k_global], cost); consume(tokens[k_local], cost)
        return {"granted": True, "lease_id": uuid4().hex, "ttl_ms": 750}
    return {"granted": False, "wait_ms": next_refill_ms(k_global, k_local)}

def report(lease_id, http_status, headers):
    if http_status in (429, 403, 418):
        apply_cooldown(derive_account(headers))      # Bybitは全HTTP停止など
    learn_from_headers(headers)                      # used_weight, resets から自動補正

トークン/リーキーバケツ(Redis想定)

# refill は 1s/5s/1m 窓ごとに loop で実行
def refill(key, rate, limit, now):
    elapsed = now - last[key]
    tokens[key] = min(limit, tokens[key] + rate*elapsed)
    last[key] = now

付録B:メトリクス命名ミニガイド

  • レイテンシ(Histogram)
    • mm_total_latency_ms{symbol=...}
    • mm_cancel_latency_ms{symbol=...}
  • イベント(Counter)
    • mm_http_429_total{endpoint=...}
    • mm_http_403_total{endpoint=...}
    • mm_cancelled_total{reason=...}表記ブレ禁止
  • 状態(Gauge)
    • mm_used_weight_1m{ip=...}
    • mm_uid_rate_1s{uid=...}
    • mm_ip_rate_short{ip=...}
    • mm_cooldown_active{exchange=...,uid=...}

-Bot, CEX, DEX, 開発ログ