この記事は、個人開発の高速ボットを“安全に勝たせる”ための設計原則をまとめた実戦ドキュメントです。対象は 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を壊すのは尾だから。だから“速さ”より尾を詰める設計を重視しています。
例えば、さっと調べてみただけでもAPIレートリミットの仕様は取引所毎に癖や仕様が全く違うんだな、と分かる。
・Binanceは重み制+短期BAN
・Bybitは重み+エンドポイント別制限+WS制限の三段構え
インフラの負荷分散やリアルタイム性が重要かどうかとかシンプルに悪用防止とか背景も色々ありそう。 https://t.co/dG5V3MYCmH— よだか(夜鷹/yodaka) (@yodakablog) August 9, 2025
TL;DR(要点だけ)
- 測る→詰める→守るの順:レイテンシ5段の p95/p99 を計測→ボトルネックを潰す→事故らないガードを常時ON。
- 最小SLO:
mm_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 p99 と newOrder 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の板ストリームは差分(diff depth)/部分板(partial depth)ともに 100ms・250ms・500ms の更新モードが用意されている。従って、2〜3 サイクル跨ぐとローカル見積りが古くなる。だから、250ms 以内に反応できれば「2〜3サイクル内追随」を満たしやすい。https://t.co/S8GgTRCfhS https://t.co/XJ2dzLTd1A
— よだか(夜鷹/yodaka) (@yodakablog) August 11, 2025
取引所別・レートリミットの実相(ドキュメント準拠の要点)
- 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で追うのが基本設計です。
- Binance:
newOrderRespType
は ACK/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–75ms、P95 はその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 市場データ):
- Diff / Partial Depth の更新速度(100/250/500ms)。SBE 版の 100ms 配信。
バイナンス開発者センター+2
GitHub
- Diff / Partial Depth の更新速度(100/250/500ms)。SBE 版の 100ms 配信。
- Binance(注文応答とユーザーデータ):
newOrderRespType=ACK/RESULT/FULL
と User Data Stream(executionReport/ORDER_TRADE_UPDATE)設計。
バイナンス開発者センター+3
- 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は初期スナップショット+例外補修のみに。
- 二段レート制御:
- グローバル窓(IP/UID×時間窓)
- エンドポイント別(重み/コスト差を反映)
常に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的制御)。
戦略別の“必要速度と勝ち筋(&根拠)”
- 板寄りMM:p99 ≤ 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- 公式数値を“固定値”と信じる → リリース毎に再確認&ブローカーで自動学習
導入ロードマップ(個人向け)
- 測る:5段 Histogram と SLO を出す
- WS ファースト化:REST は初期化+補修のみに
- ネット最適:地域最適VPS、HTTP/2常駐、署名/整形のホットパス高速化
- 中央レート制御:Rate-Limit Broker 導入(ヘッドルーム20%死守)
- 安全装置:在庫上限・連敗/日次PnL・staleness・自動クールダウン
- 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=...}