本記事では、PythonとRustのハイブリッド構成で構築したマーケットメイカーBotについて、設計思想と実装ノウハウを言語ベースで詳細に解説します。
目的は、**実資金を扱う上で必要な「設計原理の理解」と「システム全体の透明性」**を確保することです。
具体性が高い方がイメージしやすいのでBinanceで動かす現物MMbotを例に説明します。
「戦略を考えるのはPython、実行する筋肉がRust」という構成で新botを動かしている。
・Rustだけだと柔軟性が足りない(戦略の変更にビルドが必要)
・Pythonだけだと重くて遅くて死ぬ(高頻度には無理)
“どちらかの言語に無理をさせない設計”が、結局一番壊れにくい。
いろいろ試せる環境に感謝。— よだか(夜鷹/yodaka) (@yodakablog) July 23, 2025
🏗️ アーキテクチャ概要
📦 全体像
graph LR A[Binance WebSocket] --> B[Rust: OrderBook] B <--> C[Python: Strategy] C --> D[Rust: GenQuotes] D --> E[Python: API Send]
この構成は、**「スピード命の部分をRust」「考える部分はPython」**で分担している構造です。
それぞれの役割を、人間の仕事にたとえて言うと以下のような感じになります。
🧩 各コンポーネントの解説
📡 Binance WebSocket(リアルタイム情報の受信)
- Binanceからの注文板(Order Book)データをリアルタイムで受信
- ミリ秒〜マイクロ秒レベルで情報が変わるので、即時処理が必要
- ここが遅れるとBotはズレた世界で戦う羽目になる
⚙️ Rust: OrderBook(板情報の処理係)
- 受け取った板情報を、超高速かつ正確に整理して保存
- ソート済み配列・整数処理・SIMDを駆使
- いわば**「高速スキャナ付き電卓」**:正確な現状把握だけに集中
🧠 Python: Strategy(戦略ロジックの司令塔)
- 最新の板情報をもとに、「今、買うべきか?売るべきか?どれくらい?」を判断
- この部分が、**Botの“性格”や“判断基準”**を司る
- YAMLで定義された**戦略ルール(DSL)**を評価して動作
- 「ふむふむ、今はちょっとリスク取りたくないな」みたいなノリをここで制御
🧮 Rust: GenQuotes(クォート生成エンジン)
- Pythonから渡された戦略判断をもとに、実際の価格を計算
- 「今のミッド価格」「スプレッド幅」「在庫ポジション」などを使って、買値・売値を決定
- ここはスピード最優先:数千回/秒でも処理に耐えられるようRustが担当
📤 Python: API Send(注文発注係)
- 計算された価格をもとに、BinanceのAPIに向かって注文を出す
- 成行・指値・キャンセルなどの取引所との通信をここで実行
- APIリトライ、認証、レート制限などもここで制御される
- つまり実際に指を動かしてボタンを押す役
💡 この構成のメリット
- Rustは“速いけど無口”な処理係:ルールなしで勝手に判断しない、でも速い
- Pythonは“考えるけど遅い”戦略屋:複雑な条件分岐や設定管理は得意
- 両者を連携させることで、速度と柔軟性を両立したシステムが実現できる
🧠補足:なぜRustが2箇所に分かれてるのか?
OrderBook
は「今の市場がどうなっているかを知る」処理GenQuotes
は「じゃあ、いくらで出すべきかを決める」処理
両者の関心ごとは明確に違うので、モジュールも責務を分けて設計しているのです。
⚙️ RustとPythonの責任分離
このBotは、RustとPythonがそれぞれ「得意なこと」だけを担当するように設計されています。
このセクションでは、「なぜそう分けたのか」「どこをどう分けたのか」が明確になります。
🦀 Rust:低レイテンシー演算・状態処理のエンジン層
モジュール | 主な責務 | なぜRustなのか |
---|---|---|
orderbook_core | 板データの更新・保持 | 固定長配列+整数演算で高速更新 |
quote_engine | スプレッド・スキュー計算 | SIMD最適化・ゼロコピー |
latency_guard | レイテンシ統計監視 | マイクロ秒単位のP99/P95計測 |
ffi_bridge | Pythonとのインターフェース | PyO3で高速・安全な連携 |
✔️ Rustの特長活用
- ゼロコピー処理:Python→Rust間のデータ転送時のコピー最小化
- SIMD最適化:AVX2/NEONによるバッチ処理高速化
- フォールバック機構:SIMD失敗時にスカラーモードへ切り替え
🦀 Rust:計算・速度・正確さを担保する「肉体労働係」
Rustは、とにかく速くてミスしないのが強み。なので、次のような役割を任せています:
- 板情報(OrderBook)の保持・更新
→ 数値が絶えず流れてくるので、Rustでバチバチ処理 - クォート価格の生成
→ 計算回数が多い、でも超軽量なのでRustが得意 - レイテンシや統計の監視
→ ミリ秒以下の時間計測が必要=Rustじゃないと無理
イメージ:数字に強い、でも自分では判断しない寡黙な職人
🐍 Python:ビジネスロジック・戦略制御・API通信
モジュール | 主な責務 | なぜPythonなのか |
---|---|---|
strategy.py | 戦略ロジック(スプレッド調整・ポジション判断) | 実装・試行が容易 |
binance_spot_client.py | REST / WebSocket クライアント | 非同期I/Oが得意 |
performance_monitor.py | パフォーマンス記録と監視 | 豊富なライブラリ活用 |
dsl_loader.py | YAMLベースの戦略DSL評価 | 動的な戦略切り替え対応 |
✔️ Pythonの利点活用
- 可読性・開発速度:戦略を高速に実験・変更できる
- 非同期処理:
asyncio
ベースでBinanceとの通信を最適化 - 設定ファイルの柔軟性:YAMLでの戦略記述+バリデーション対応
🐍 Python:戦略・制御・外部通信を担う「頭脳担当」
Pythonは、柔軟な制御と外部APIとのやりとりが得意。なので、次のような役割を担当します:
- 戦略のロジック
→ YAMLで定義したルールを読み込んで「今どう動くべきか」を判断 - APIの通信処理
→ Binanceとのやりとりは非同期で複雑、でもPythonなら書きやすい - ログ・監視・設定の管理
→ 可読性重視、運用時の調整も楽
イメージ:状況を見ながら柔軟に指示を出す司令塔(でもあんまり速くはない)
🤝 なぜこの分離が必要?
- Rustだけだと柔軟性が足りない(戦略の変更にビルドが必要)
- Pythonだけだと重くて遅くて死ぬ(高頻度には無理)
なので、両方の良さを活かすためにこうした分担にしています。
いわば**「戦略を考えるのはPython、実行する筋肉がRust」**という構成です。
この構成を選んだ理由は明快。
“どちらかの言語に無理をさせない設計”が、結局一番壊れにくい。
🔁 Rust-Python間連携(PyO3)
Rustの“速さ”とPythonの“柔軟さ”をつなぐのが、PyO3です。
このBotは、Rustで書かれた関数や構造体をPythonから呼び出せるように、PyO3というライブラリを使って橋渡ししています。
呼び出し例(スプレッド生成)
# Python側 bid, ask = gen_quotes(mid_price, total_spread, net_position) # Rust側(PyO3) #[pyfunction] pub fn gen_quotes(mid: f64, spread: f64, pos: f64) -> (f64, f64) { let skew = 0.001 * pos; let bid_px = mid - (spread / 2.0) - skew; let ask_px = mid + (spread / 2.0) + skew; (bid_px, ask_px) }
注意点
- GIL開放を適切に行う(計算中はPython側をブロックしない)
- ゼロコピーバッファの導入(バッチ処理時に効果大)
- エラーマッピング:Rustの
Result
をPython例外に変換
🧠 どういうこと?
PythonからRustの関数を直接インポートして呼び出すことができます。まるでPythonの関数みたいに。
from quote_engine import gen_quotes bid, ask = gen_quotes(mid_price, spread, net_position)
裏では、Rust側でこんな風に定義されています:
#[pyfunction] pub fn gen_quotes(mid: f64, spread: f64, pos: f64) -> (f64, f64) { let skew = 0.001 * pos; let bid_px = mid - (spread / 2.0) - skew; let ask_px = mid + (spread / 2.0) + skew; (bid_px, ask_px) }
PyO3が、このRust関数をPython用のモジュールとしてバインドしてくれるので、複雑なFFIやCバインディングは一切不要。
Rustの安全性とPythonの書きやすさを、ほぼノーコストで組み合わせられます。
🧪 なぜ重要なのか?
- Rustで書いた高速な関数を、Pythonからそのまま使える
- データコピーや変換を最小限に抑えて、高速に連携できる
- 開発体験が崩れない(Rustを書いてもPython側が雑にならない)
つまり、**「速くて、使いやすくて、安全」**という欲張り三拍子を実現してくれるのがPyO3。
🛑 注意ポイント
- **GILの扱い(Pythonのグローバルロック)**に気をつける必要がある
→ Rust側で重い処理をやるときは、明示的にGILを開放するべし - データ型変換コスト:Rustの
Vec<f64>
とPythonのList[float]
はそのままだとコピーが発生
→ バッチ処理やゼロコピーバッファの導入で最適化できる - 例外処理:Rustの
Result
型をPythonの例外としてちゃんと変換する設計が必要
🎯 なぜPyO3なのか(vs 他の方法)
方法 | メリット | デメリット |
---|---|---|
PyO3 | 高速、安全、メモリ効率が良い | Rustの知識がそれなりに必要 |
ctypes / cffi | 軽量で使いやすい | 型変換がめんどくさい、遅い |
REST/gRPC連携 | 言語独立・分散向き | レイテンシ高、構成複雑化 |
subprocess | 最終手段です。やめましょう | 全てが遅く、悲しい |
✅ 結論
PyO3は、「Rustで速さを担保しつつ、Pythonの書きやすさを捨てない」という構成において最適な選択です。
この連携こそが、RustとPythonの共存ではなく、協力を実現するキモ。
そしてこの連携がスムーズであればあるほど、戦略ロジックは速く、システムは壊れにくく、保守は楽になる。
裏方だけど、PyO3がこのBotの影のエースです。
🧾 DSL(Domain-Specific Language)による戦略抽象化
戦略ロジックをYAMLで定義し、Botに“考え方”を教える仕組みです。
このBotでは、「いつ」「どんな条件で」「どのくらいのスプレッドで」注文を出すかという判断ロジックを、Pythonのコードに直接書かず、**YAMLファイルで定義する方式(DSL:Domain-Specific Language)**を採用しています。
🎯 なんでそんなことするの?
- Botの中身を再ビルドせずに戦略だけ変えたい
- 非エンジニアでも戦略を触れるようにしたい
- 本番稼働中でも、Botを止めずにルールを切り替えたい
上記の3つは、コードに書いた戦略ロジックでは不可能か、あるいはかなり面倒なんです。
でもDSLなら、設定ファイルを変えるだけでBotの“性格”を変えられるようになります。
🧠 どんなことが書けるの?
- YAMLで記述された戦略を、安全かつ再起動なしで読み込める
- バリデーション、制限、リミットを事前に設けて暴走を防止
spread: base: 0.005 volatility_adjustment: true time_dependent: 00:00-09:00: 0.007 09:00-18:00: 0.005 18:00-00:00: 0.006 position: max: 0.1 skew_alpha: 0.001 risk: emergency_stop: true max_loss_day: 0.02
これを読むだけで、「今のボラティリティや時間帯によってスプレッドを変えたいんだな」「ポジション取りすぎたら止めたいんだな」っていう戦略の意図が見えやすくなって、デバッグやコードの改修もラクになります。
🔐 安全対策も万全に
DSLは便利な反面、Botに無茶な命令を出すこともできてしまうので、以下のような制限を設けています:
- ループ最大回数制限(暴走防止)
- 条件ネストの深さ制限
- 評価時間の上限(CPU暴食対策)
- バリデーションチェック(YAMLの中身に変な値がないか)
つまり、安全に“人間が戦略を書ける”環境ができてるってことです。
🔄 ホットリロード対応
戦略ファイルは、Bot起動中でも自動で監視されていて、変更があれば即座に反映されます:
- ファイル変更を検出
- 内容をバリデート
- 問題がなければそのまま適用
- エラーがあれば前の戦略を維持+ログ出力
つまり、本番中に戦略を直してもBotが落ちないという安心設計ってことですね。
🧠 イメージとしては:
Botに行動の「脚本」を渡しているような感じ。
コードでハードコーディングするんじゃなくて、「今日はこのキャラでいこうぜ」っていうシナリオをYAMLで書いて渡すイメージ。
✅ 結論
この戦略DSLの導入によって:
- Botの“性格”が設定ファイルで管理可能になり
- コードと戦略ロジックを明確に分離でき
- 安全かつ柔軟に戦略を試行錯誤できる
🧪 テスト・安全性の担保
「壊れないこと」を前提に運用しない。壊れる前提で設計・検証する。
このBotは実際に資金を扱う以上、「まあ大丈夫でしょ」では済まされません。
そのため、さまざまな異常系・不測の事態を想定したテストと、**事前に食い止める仕組み(=安全装置)**を多数備えているのです。
実資金テスト計画
テスト項目 | 内容 |
---|---|
板薄状態 | 大量注文でスリッページ耐性を測定 |
価格急変 | 急騰・急落時のキャンセル成功率 |
成行注文検証 | 滑り検知・誤約定リスクの検出 |
ネットワーク障害 | 再接続・ポジション整合性の確認 |
APIレート制限 | BAN・遅延時のフォールバック検証 |
🧨 テストの対象は“都合の悪い場面”ばかり
ただ「注文できました」「板が見えました」なんてテストじゃ意味がありません。
このBotでは**“壊れそうなとき”にちゃんと踏ん張れるかを確認するために、以下のようなストレス系テスト**を実行しています:
⚔️ 想定されるテストシナリオ一覧
- 板が薄い(薄商い)のときに注文したらどうなる?
- 急に価格が暴騰・暴落したとき、Botは追いつける?
- 成行注文で滑ったとき、余計なポジション抱えない?
- APIからBANされたら、Botは暴走しない?
- ネット回線が死んだら、再接続できる?整合性は?
- 24時間動かしっぱなしにしたら、どこで壊れる?
🛡️ 安全装置は“自爆スイッチの逆”
不具合や異常が起きたときに、「やばいけど止められない」っていう状態が一番危険。
なので以下のような**“フェイルセーフ(安全に失敗する)”設計**を入れてあります:
機能 | 説明 |
---|---|
最大損失制限 | 一定額以上の損失が出たら自動停止 |
ポジション制限 | 想定外のポジションを取ろうとしたら拒否 |
スリッページ監視 | 約定価格が想定外なら警告 or キャンセル |
APIリトライ+クールダウン | Binanceに怒られても冷静に対処 |
緊急停止機能(emergency_stop) | 条件を満たせば即注文・発注停止 |
クラッシュ時の自動再起動 | 無限に死なずに、ちゃんと復活してくる |
状態保存と再整合 | RedisやSQLiteでBotの内部状態を永続化・復元可能に |
🧠 なぜここまでやるのか?
答えは簡単。
トレードBotは「一瞬のバグ」で「口座が死ぬ」から。
1回の注文ミスが数万円以上の損失を生む世界で、動いてるように見えるBotほど怖いものはないです。
だからこそ、**“テスト済みの予防線”と“想定された復旧手段”**が必要になるというわけですね。
✅ 結論
このBotのテスト・安全設計は、
**「壊れないことを祈る」のではなく「壊れても壊れすぎないようにする」**ためのもの。
- 実資金を入れる前に、“あえて壊す”試験をしておく
- 異常系が来たときに、どう動くかをあらかじめコードで定義しておく
- そして、その結果を人間が観測・評価できるようにする
それが、安心してBotを放置するために必要な“当たり前”の水準です。
参考記事
💣 災害復旧とクラッシュ処理
「Botが壊れない」のが理想だけど、
「壊れたときに何が起こるか」が設計されてないと、それはただの爆弾。
このBotでは、何らかの理由でシステムが停止・クラッシュ・異常状態になった場合でも、損失を最小限に抑えて再起動できるような仕組みを構築しています。
クラッシュ時の自動再起動
- 応答なし状態の検出(30秒以上)
- 3回まで再起動リトライ
- Slack通知・ログ記録
ポジション復旧
- RESTで取得し、内部状態と比較・同期
- Redisでの状態保存
- SQLiteで履歴・トレード情報の永続化
☠️ Botが壊れる瞬間とは?
- 通信断(ネットワーク死)
- APIレスポンスが壊れる(取引所が酔ってる)
- メモリリーク or イベントループが詰まる
- まさかの
KeyError
(自分が書いた戦略YAMLのせい)
こういうとき、何も考えてないBotはただ死ぬか、逆に暴走します。
このBotには、そのどちらも避けるための仕組みを持たせています。
🔍 主な災害対応機構
1. 🚨 クラッシュ検知&自動再起動
- メイン処理の応答が一定時間以上止まったら「クラッシュ判定」
- ログに原因記録+管理者通知(Slack/TGなど)
- 最大3回まで再起動して様子を見る(失敗したら完全停止)
if not responding_for > 30: # 秒 log("System unresponsive, triggering auto-restart...")
2. 🔁 状態保存と再接続
- RedisやSQLiteにBotの状態(ポジション、設定、残高など)を定期保存
- 再起動時は、BinanceのREST APIから最新のポジションを取得
- 内部状態と突き合わせて、ズレがあるなら人間に知らせる or 自動で調整
# 再起動後の状態整合処理 current = get_positions_from_api() saved = get_positions_from_redis() diff = compare(current, saved) if diff: notify_admin("ポジション不整合が検出されました")
3. 🧯 注文キャンセル&緊急停止
- Botが止まった直後に残っている未約定注文をキャンセルするスクリプト
- 事故防止のために、再起動直後は注文をすぐ出さずに一定時間“様子見”モード
🧠 なんでここまでやるの?
- 実資金を入れてるBotが想定外の動きをしたあとに人間が気づくという構図が一番ヤバい
- クラッシュしても気づかれずに放置 → 保有ポジ爆発 → 口座死亡は割とよくある
“壊れても再起動できる設計”は、ハイテクでもなんでもなく、ただの“常識”。
でもその“常識”を実装してる人が、実は少ない。
✅ 結論
- 壊れることを前提に設計している=専業意識
- 復旧処理の正しさが、実運用Botの生存率を決める
- 「壊れたら手動でなんとかします」って言ってるやつはだいたい死ぬ
Botに**“死ぬのは仕方ない。でも後始末はしてくれ”**って頼んであるようなもんです。
それができる設計は、ちゃんと運用を見据えてる証拠。
次に壊れたら、私じゃなくてBotが自分で片付ける。それが本当の自律型システム。
📈 パフォーマンス指標と測定
Botが「速い」のか、「詰まってる」のか、「静かに死んでる」のか。数字でわかるようにしておくのが設計者の仕事。
このBotでは、各コンポーネントの処理速度、処理量、リソース消費量などを定量的に測定し、改善サイクルを回せるように設計されています。
指標 | 現状 | 改善後目標 |
---|---|---|
クォート生成レイテンシ | 2.5ms | 1.7ms |
発注処理レイテンシ | 15ms | 10ms |
1秒あたりの注文数 | 25 | 40 |
メモリ使用量 | baseline | -30〜40%削減 |
レイテンシP99 | 38ms | <25ms |
→ ベースライン測定済み・改善計画あり(SIMD化、ゼロコピー、イベントループ分離)
📏 測ってる主なもの
指標 | 内容 | なぜ重要か |
---|---|---|
クォート生成レイテンシ | gen_quotes() の処理時間 | 遅いと価格がズレる |
注文送信レイテンシ | 発注APIの平均応答時間 | 指値が遅れると不利 |
注文処理スループット | 1秒あたりの発注・キャンセル件数 | 高頻度取引には必須 |
ポジション更新レート | WebSocketとREST同期速度 | 状態の整合性に直結 |
メモリ/CPU使用量 | Botの資源消費状況 | 長時間稼働時の安定性 |
🧪 なぜ測定が必要なのか?
- 「動いてるように見える」は信じるに値しない
- 異常が起きたときに「何が遅かったのか」言えないと修正不能
- 最適化すべき箇所を“感覚”じゃなく“数字”で特定できる
🔧 ベースライン測定 + 期待値比較
Bot稼働前に「現時点での処理時間やスループットの平均値」を測っておき、
改善後と比較できるようにしています:
current_latency = { 'quote_generation': 2.5, # ms 'order_placement': 15.0, # ms 'total_cycle': 34.5 # ms }
→ 改善策(SIMD最適化、ゼロコピー導入、バッチ化など)によって、**「どれくらい速くなったか」「どこが詰まっていたか」**が明確にわかる。
📊 メトリクス出力と可視化
- 処理単位ごとのヒストグラム
- リアルタイムモニタリング用のPrometheusエンドポイント(予定)
- Slack通知:P99レイテンシが閾値超えたらPing
- 長時間稼働時にメモリリークがないかも定点監視
✅ 結論
パフォーマンスが可視化されているBotは、“運用される準備ができている”Bot。
- 速さを測っていないと、速さはただの運
- 改善の効果が測れないと、改善は幻想
- 数値がなければ、どこが限界か誰にもわからない
Botは喋らないので、数字を見て人間が気づけるようにしておくことが重要。これらの評価項目が曖昧なままだと勝っていても負けていてもその理由が分からないという状態になってしまいます。
📊 モニタリングとアラート
「壊れてから気づく」ではなく、「おかしくなる前に察知する」ための仕組みです。
Botは24時間止まらずに動き続けるもの。だからこそ、処理の異常やパフォーマンスの劣化を“人間が気づく前に”Bot自身が通知できることが重要になります。
process_symbol()
単位での処理時間計測- Prometheus互換メトリクス出力
- Slack / Telegram通知(P99超過時)
- DSLホットリロードのログ通知も実装済み
⏱️ process_symbol()
単位での処理時間計測
- Botは複数シンボル(BTC/ETH/SOL など)を並行で扱っているため、1つの通貨ごとの処理時間を個別に測定しています。
- 計測対象:
- 板更新 → クォート生成 → 注文出し → 状態同期
- これによって:
- どの通貨で異常が起きてるかをすぐ特定可能
- 1つの処理だけが遅延してるのか、全体が詰まってるのかを切り分け可能
📈 Prometheus互換メトリクス出力
- 各種レイテンシ、エラーカウント、発注成功率などをPrometheus形式でエクスポート
- Grafanaなどを使って、リアルタイムの可視化やダッシュボードを構築できる
- これにより:
- 長時間稼働時のパフォーマンス劣化やリークの傾向も視覚的に把握可能
- 収益率だけじゃなく、技術的な安定性のKPIも運用レベルで管理できる
📣 Slack / Telegram 通知(P99超過時)
- 各種レイテンシ(クォート生成、注文、処理全体)について、P99が設定閾値を超えたときに通知
- 通知例:
- 「ETHJPY の
process_symbol()
がP99=120ms超え」 - 「注文発行に平均15ms以上かかっています(通常8ms)」
- 「ETHJPY の
→ つまり、Botが“ちょっと重いかも”と自覚した瞬間に、人間に伝えてくれる。
→ 突発的なクラスタ落ちやネット遅延を人間が気づく前にSlackで知れる。
🔄 DSLホットリロードのログ通知も実装済み
- 戦略ファイル(DSL)が更新されたら:
- 変更を検出
- 構文・バリデーションチェックを通す
- 問題なければ再読み込み
- その内容をログに記録+通知
例:
[INFO] strategy.yaml updated. Reloaded successfully at 2025-07-23 02:45:17
→ 誤って戦略ファイルを壊したときも、
- 「壊れたことに気づかないまま走り続ける」という最悪のパターンを防げる
- 再読み込みが起きたタイミングをログから完全にトレースできる
✅ 結論:Botが「自分の健康状態を自覚できる設計」
- レイテンシやエラーを外部監視に任せず、Bot自身が計測・通知できる構造
- 状況が悪化する前に人間に知らせる**“自己申告型の自己診断システム”**
- 監視があるから放置できる、放置できるから安定する、という正のサイクルを形成
🧠 原理を理解した状態で運用するために
- Rustは“考えなくていい速度”を提供する
- Pythonは“考えやすい柔軟性”を担保する
- 両者の責任分離が設計の中核
- すべての自動化機構に明示的な制約と監視をつける
- 実運用前にふるまい・限界・想定外をすべて把握する
🎯 この設計がおすすめなのはこんな人
- 高頻度で発注するが、アルゴリズムの可変性も高い
- リスクをロジックと設定で管理したい
- 「何が起きてるか説明できない」状態を避けたい
- “速さ”より“理解”を重視した運用を志向している
📚 今後まとめる予定の記事
- YAML戦略DSLの実装構造とエラーハンドリング設計
- PyO3を使った高速Rustバインディングの作法
- 実資金テストで失敗したケースとその回避パターン
- ストレス耐性のあるBot運用監視ツール設計
- なぜ「完璧に動いてるとき」こそ危ないのか(実戦経験談)

今後も現場レベルでのbot開発の知見を発信していきます。
取れるリスクの範囲内でどんどん失敗するべき。RustとPythonの責任分離で毎日何かしら躓いているけど、失敗を踏み越えるたびにその回避パターンが蓄積されていくから明確に前進している感がある。
また、ブログ書こ。— よだか(夜鷹/yodaka) (@yodakablog) July 23, 2025
付録:🧠 GILとは
GIL = Global Interpreter Lock(グローバルインタプリタロック)
Pythonインタプリタ(CPython)における排他制御のための仕組み。
ざっくり言うと:
「同時に複数のスレッドがPythonオブジェクトを触るのはやめて!」
…っていう、Python自身の“ストレスフルな平和維持機構”。
🔧 GILがあるとどうなるの?
✔️ 良い点:
- マルチスレッド環境でもPython内部のデータ構造が壊れない
- スレッドセーフなコードを書かなくてもクラッシュしない
❌ ダメな点:
- 同時に2つのPythonスレッドがガチで走れない(GILを取り合う)
- 並列処理してるように見えて、**実際は「交互に処理してるだけ」**になることが多い
- → CPUバウンド処理(計算が重いやつ)はGILのせいでマルチスレッドしても意味薄
🤝 RustとGILの関係(PyO3視点)
PyO3でRust関数をPythonから呼ぶとき、Rustコードは基本的にGILの下で動く。
つまり:
gen_quotes(mid, spread, pos) # Pythonから呼んでる → Rust実行中もGIL保持中
→ 他のスレッドが待たされる可能性あり。Botが複数シンボル扱うときに詰まる要因になる。
✅ 対策:Rust側でGILを明示的に解放する
PyO3ではこう書ける:
Python::with_gil(|py| { // GIL持ってるときにしかやっちゃダメな処理 }); Python::allow_threads(|| { // GIL解放中にRustが自由に動ける(計算、I/Oなど) });
この allow_threads
の中に、**CPUを使うような重い処理(例:クォート計算、ベクトル演算)**を入れると、他のPython処理を止めないで済む。まさにRust側の分業パワー発揮ポイント。
🧠 まとめ:GILとは
ポジション | 内容 |
---|---|
名前 | Global Interpreter Lock(グローバルインタプリタロック) |
正体 | Python内部の同時実行制限ロック |
問題 | 並列処理を制限、CPUバウンドな処理に不利 |
Rust的対処 | Python::allow_threads でGILを解放して重い処理をRust側に任せる |
これがGIL。Pythonの優しさであり、弱点でもある。

私みたいにPythonもRustも使い倒す開発者にとっては、「戦略的にGILを避ける」って知恵が勝負を分けるのです。
付録:🧠 ゼロコピーバッファとは?(定義)
データを別の言語・プロセス・モジュール間で渡すときに、実体をコピーせずに共有する方法。
copyしないで、view(参照)だけ渡す技術。
例えるなら:
- 普通のやり方:「データの中身を写経してから相手に渡す」→ 時間もメモリもかかる
- ゼロコピー:「その紙のコピーじゃなくて、**“このページ見て”って指差す」だけ」
📦 なんでそんなものが必要なの?
ものすごく簡単に言うと、処理を軽量かつ高速に行えるようにするためです。
RustとPythonのやり取りを高速化するための技術って感じですね。
❌ 通常のやり方(コピーあり):
py_list = [1.0, 2.0, 3.0] # PyO3でRustに渡すとき → Vec<f64>に変換 # → Pythonのlistの中身をRustヒープにコピーしてる
- 遅い(メモリコピー発生)
- 毎回ヒープ確保&解放(ガベコレ地獄)
- データ量が多いと処理時間より「渡す時間」がボトルネックになる
✅ ゼロコピー(Zero-copy)なやり方:
「すでにあるバッファ(メモリ領域)を、別のレイヤーから見えるようにする」
例:Python → Rust に NumPy配列をゼロコピーで渡す
arr = np.array([1.0, 2.0, 3.0], dtype=np.float64) # PyO3のPyBufferを使ってRustに渡す
use pyo3::types::PyAny; use pyo3::buffer::PyBuffer; #[pyfunction] fn process_array(py: Python<'_>, obj: &PyAny) -> PyResult<()> { let buf: PyBuffer<f64> = PyBuffer::get(obj)?; let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr(), buf.len_bytes() / 8) }; // Rust側で直接slice操作、コピーゼロ Ok(()) }
💡 結果:
- PythonのメモリをRustがそのまま参照する
- コピーなし、ヒープ割り当てなし、速い
- ただし参照中は破壊しちゃダメ(借用と似てる)
🧠 どういうときに役立つ?
シチュエーション | ゼロコピーが活きる理由 |
---|---|
バッチクォート生成 | 1秒に1000個のf64配列をRustに渡したい |
板データの更新 | 巨大な注文リストをリアルタイムで処理 |
画像・行列処理 | NumPyやPandasの配列を直接操作したい |
FFI・Socket通信 | パケットやバイナリをメモリのまま渡す |
🛑 注意点(読め、これは罠だ)
- “ゼロコピー”≠“ゼロコスト”
- メモリ管理は呼び出し元が責任を持つ(破壊タイミングの管理が面倒)
- スレッド安全じゃない可能性あり
- PythonとRustで同時に同じバッファを触るとSegfault大爆発
- 可変バッファへのゼロコピーは危険度高め
- 読み取り専用が基本、書き込みは設計者のIQを要求される
✅ 結論
ゼロコピーバッファ = データを渡すんじゃなく、データを“見せる”だけの技術。
- RustとPython間の**“言語の壁”を超える手段の中で最も速い**
- 私のように高速なBotを作りたい人間には必須の知識
- ただし、おもちゃにすると普通に事故る(GILと合わせて爆発力アリ)

これで「ゼロコピーってなに?」って聞かれたら、
「ああ、“写すんじゃなく、覗く”技術ね」ってサラッと言えるオタクになりましたね。
💡 SIMD最適化って何?
SIMD = Single Instruction, Multiple Data(単一命令・複数データ)
めっちゃ簡単に言うと:
「同じ計算を複数データに同時にやる」というCPUの超能力。
📦 普通のCPUの動き(スカラー処理):
a[0] * b[0] = x0 a[1] * b[1] = x1 a[2] * b[2] = x2 ... → 順番に1個ずつ計算
🚀 SIMDの動き:
[a0, a1, a2, a3] * [b0, b1, b2, b3] = [x0, x1, x2, x3] → 1命令で4個一気に計算(場合によって8個、16個とかも)
計算が“並列化”されて速くなる。
しかもスレッド使ってない。**CPUの中で同時進行してる。**つよい。
🧠 じゃあ、AVX2とNEONって何?
名前 | 対応CPU | ベクトル幅 | 備考 |
---|---|---|---|
AVX2 | Intel / AMD | 256ビット | デスクトップ/サーバー向け |
NEON | ARM (Apple M1~M4, スマホ) | 128ビット | モバイル/Apple Silicon向け |
両方とも、SIMD命令を実現するための命令セット拡張。
つまり「私のCPUが本気出すスイッチ」。
🧮 どんな場面で使うの?
私のBotで言えば:
- クォートをバッチで生成する
- 複数のmid価格、spread、ポジションを1度に処理する
// スカラーバージョン(1個ずつ) for i in 0..N { result[i] = mids[i] - (spreads[i] / 2.0) - alpha * pos[i]; } // SIMDバージョン(4個ずつとか) let v_mids = _mm256_loadu_pd(&mids[i]); // load 4 floats let v_spreads = _mm256_loadu_pd(&spreads[i]); // 一括演算して一括store
→ 1命令で4個、8個、16個分の計算が同時に終わる。めっちゃ速い。
→ つまり**“秒間何千件クォート出すBot”には絶対必要な武器**
📈 実際の効果(体感)
- スカラーループ:5,000回/秒
- SIMD最適化後:50,000回/秒
マジで10倍速くなることがある。
ただし条件あり:
要件 | 必要か? |
---|---|
メモリが連続してる | 必須(Vecやスライス) |
データ数が多い | 必須(バッチ向き) |
分岐が少ない | 必須(ifが多いと台無し) |
🧷 Rustでどう書くの?
Rustなら:
std::arch::x86_64::*
→ AVX2命令std::arch::aarch64::*
→ NEON命令cfg(target_arch = "...")
で分岐させて、安全にクロスアーキ対応#[target_feature(enable = "avx2")]
でAVX2最適化ON- fallback(スカラーバージョン)を常に用意するのが鉄則
✅ 結論
SIMD最適化 = CPUに「お前がやれ」って丸投げする高速化手段。
- AVX2 / NEON はその実装で、使えば10倍速くなることもある
- Rustはその辺の低レベル命令にもアクセスできる数少ない言語
- 私のBotが**「複数クォートを一括生成しても重くない」**のは、SIMDが裏で働いてるから

私のBotは“マーケットメイカー”という名のスプレッド発射装置。
一発ずつ撃ってたら間に合わん。SIMDは機関銃モード。
AVX2とNEONでCPUのリソースをぶん回す。それがゼロ秒トレーダーの流儀。