はじめに
個人でアルゴリズム取引の研究をしているうちに、既存のバックテスト環境にいくつか不満が溜まっていきました。
- クラウド型は戦略コードをサーバーに送る必要がある。個人で研究しているアルファを預けるのは抵抗がある
- ローカル型は分析UIが弱い。PnLが集計値でしか見えず、「どのトレードが効いたか」まで追えない
- AI Agent 時代なのに、AI がバックテスターを扱いやすい形になっていない。集計指標を眺めるだけでは、AI も改善仮説を立てづらい
この3点を同時に解決したくて、バックテスターを自作しました。名前は Hawk-Backtester です。
本記事では、このサービスで採用した技術スタックと設計判断について、「なぜその構成にしたか」を中心に書いていきます。プロダクト宣伝というより、同じような課題を持つ開発者への設計記録のつもりです。
サービスURL: https://app.hawk-backtester.com
要件整理 — 何を満たす必要があったか
設計に入る前に、満たしたい要件を言語化しました。
- 戦略コードがサーバーに送信されないこと(プライバシー)
- 任意の Python ライブラリ・外部 API・GPU を使えること(自由度)
- ブラウザで動くこと(手軽さ)
- シミュレーション速度が "遅くて使う気が失せない" レベルを超えていること(実用性)
- 取引1本ごとのライフサイクルが追跡できること(分析粒度)
- AI Agent が自律的にループを回せる API があること(拡張性)
この6点、普通に考えると矛盾します。
- 「コードをサーバーに送らない」と「ブラウザで動く」は一見両立しない
- 「任意のライブラリを使える」と「ブラウザ内で動く」は、普通の意味では両立しない
この矛盾をどう解くかが、このプロジェクトの設計の核でした。
アーキテクチャ — 責務の分け方
結論から書くと、以下の構成に落ち着きました。
┌─────────────────────────────────────────────┐
│ ユーザーのローカル環境 │
│ │
│ ┌──────────────────────┐ │
│ │ Python 戦略コード │ │
│ │ (hawk-bt ライブラリ) │ │
│ │ - 任意のライブラリ │ │
│ │ - 外部API/GPU自由 │ │
│ └──────────┬───────────┘ │
│ │ WebSocket (127.0.0.1) │
│ ┌──────────┴───────────┐ │
│ │ ブラウザ (Chrome) │ │
│ │ ┌─────────────────┐ │ │
│ │ │ WASM Simulator │ │ │
│ │ │ (Rust 製) │ │ │
│ │ │ - 注文執行 │ │ │
│ │ │ - 資金管理 │ │ │
│ │ │ - Ticket 管理 │ │ │
│ │ └─────────────────┘ │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────┘
※ サーバー側はユーザーアカウント管理・
ヒストリカルデータ配信・結果保存のみ
戦略コード本体は一切受け取らない
ポイントは 「シミュレータ本体をブラウザ内 WASM で動かし、戦略コードはローカル Python プロセスに置いて WebSocket で繋ぐ」 という構成です。
この構成にしたことで:
- 戦略コードは ローカル Python プロセスから一歩も外に出ない → プライバシー要件クリア
- Python 側は普通の Python なので 任意のライブラリ・GPU・外部API 自由 → 自由度要件クリア
- シミュレータは Rust 製で WASM 化されてブラウザ内で動く → ネイティブに近い速度
- UI はただの Web アプリ → 手軽さ要件クリア
ブラウザを「ただのビューア」ではなく「ローカル計算リソース」として使う、というのが発想の転換点でした。
なぜ Rust + WASM なのか
シミュレータの実装言語として、最初は Python で書くことも検討しました。NumPy + Numba で十分速いのでは?というやつです。しかし却下しました。理由は以下です。
1. 分離性の問題
シミュレータと戦略コードを 別プロセスに分離したかった。これはプライバシーというより、責務の分離の話です。
- シミュレータ:決定論的・副作用なし・純粋計算
- 戦略コード:外部API叩いたり、NNを推論したり、何でもあり
この2つを同じプロセスに同居させると、デバッグも遅い戦略による足引っ張りもヤバい。別言語・別プロセスにしたかった。
2. ブラウザで動かしたかった
「ユーザーがローカルに環境構築する手間なしに、UIを触って即座に結果を見せたい」という体験を作るには、シミュレータがブラウザ内で動く必要がありました。
- Python を WASM 化する選択肢(Pyodide)はある
- が、NumPy/Pandas 依存の Python コードを Pyodide で動かすと、初期ロードが重すぎる
- Rust ならバイナリサイズが小さく、起動も高速
3. 並列性能
同時に複数チケット(ポジション)を管理しながら、ステップごとに状態遷移する処理は、Rust の所有権モデルと相性が良い。データ競合がコンパイル時に潰せる。
4. 将来の拡張性
当初のシミュレータは単純な注文執行と資金管理でしたが、後から:
- モンテカルロシミュレーション
- パラメータスイープの並列実行
- 複数資産のポートフォリオシミュレーション
を追加する見込みがあり、Rust の方が確実に速度の天井が高いと判断しました。
結果として、WASM 化されたシミュレータのサイズは数MB程度に収まり、ブラウザを開いてから数秒でバックテストできる状態になっています。
なぜ WebSocket なのか
ブラウザ内 WASM とローカル Python プロセスを繋ぐ手段はいくつかあります。
- HTTP(REST / Server-Sent Events)
- WebSocket
- WebRTC DataChannel
最終的に WebSocket に決めました。
HTTPを選ばなかった理由
バックテストは「シミュレータが1ステップ進む → 戦略に現在の状態を渡す → 戦略が注文を返す → シミュレータが次のステップへ」という 高頻度の双方向通信 が必要です。
1バックテストで数千〜数万ステップ、そのたびに HTTP リクエスト往復していたら、ヘッダーオーバーヘッドだけで膨大になります。
WebRTC を選ばなかった理由
WebRTC DataChannel は確かに速いのですが、セットアップが複雑すぎる。ローカル127.0.0.1 接続で使うにはオーバーキル。
WebSocket の利点
- 双方向・常時接続
- オーバーヘッドが小さい
- 実装がシンプル
- Python 側も Rust 側もライブラリが枯れている
唯一の懸念は「ブラウザから127.0.0.1への WebSocket 接続は mixed content で弾かれないか」でしたが、これは許可されています(同一オリジン扱いではないが、localhost は特例として許可されている)。
戦略記述のインターフェース — hawk-bt ライブラリ
ユーザーが Python で戦略を書くための SDK として、hawk-bt という PyPI パッケージを公開しています。
pip install hawk-bt
使い方のイメージは以下のような形です(詳細は公式ドキュメント参照)。
from hawk_bt import Strategy, connect
class MyStrategy(Strategy):
def on_step(self, ctx):
# ctx には現時刻の価格・自分のポジション・残高などが入る
if ctx.close > ctx.ma_20 and ctx.position_size == 0:
return ctx.buy(size=10, sl=ctx.close * 0.98)
elif ctx.close < ctx.ma_20 and ctx.position_size > 0:
return ctx.close_position()
# ブラウザからの接続を待ち受け
connect(MyStrategy(), host="127.0.0.1", port=8787)
この時点では 何の計算も実行されません。ブラウザUI側で「Connect to Local Strategy」ボタンが押されると、WebSocket がつながってシミュレーションが開始されます。
なぜこの API 形状にしたか
backtrader や vectorbt のように、バックテストフレームワーク側にイベントループを持たせる設計もありえました。が、敢えて ユーザー側で connect() を呼んで待ち受ける形 にしました。
理由は、「AI Agent から使うとき」を想定したからです。後述しますが、AI Agent は hawk_auto_tune.py のようなスクリプトを自分で編集・実行する必要があります。エントリーポイントが普通の Python スクリプトだと、AI Agent が扱いやすい。
Ticket 粒度のデータ保持 — 分析のための設計
シミュレータ側で重視したのが、取引1本ごとの Ticket データを全部保持する という設計です。
一般的なバックテスターだと、結果として出てくるのは集計値です:
- Total Return
- Max Drawdown
- Sharpe Ratio
- Win Rate
これらは重要ですが、これだけだと 「なぜ勝ったか / なぜ負けたか」が見えない。
Hawk-Backtester では、個々の注文(Ticket と呼んでいます)について以下の情報を全て保持しています:
- Open time / Close time
- Entry price / Exit price
- Size
- Long / Short
- Exit reason(TP / SL / Strategy Close / Force Close)
- 保有期間中の MFE(Maximum Favorable Excursion)
- 保有期間中の MAE(Maximum Adverse Excursion)
- 各時点での PnL への寄与
これを UI 側で 4つの分析軸に展開しています:
1. Time Series 分析
時系列でエクイティカーブ、ドローダウン、露出、同時保有ポジション数などを表示。「どの時期に稼いで、どの時期に溶かしたか」が見える。
2. Scatter Plot 分析
Holding Time vs PnL、MAE vs MFE、Size vs PnL、Entry Price vs PnL の散布図。「保有時間が長いと勝つのか負けるのか」「参入価格帯で勝率に偏りがあるか」が見える。
3. Exit Analysis
終了理由別の内訳(TP 到達 / SL 到達 / 戦略による手仕舞い / 強制決済)と、それぞれの PnL 合計。「TP と SL のどちらが効いてるか」が一目で分かる。
4. Ticket Lifecycle
個々のチケットを時間軸にマッピング。Concurrent Slot 表示で、同時保有ポジションの重なり方も可視化。マウスオーバーで個別チケットの詳細が出る。
この粒度でデータを残しておくと、次に書く AI Agent 連携 が生きてきます。
AI Agent モードの設計 — 研究ループを丸投げする
個人的に一番こだわったのがここです。
問題意識
バックテストによる戦略改善ってめちゃくちゃ時間がかかります。
仮説を立てる
→ コードを書く
→ バックテストを回す
→ 結果を眺める
→ 改善案を考える
→ コードを書き直す
→ バックテストを回す
→ ...
このループ、AIに回させたい。
既存の課題
AI Agent にこのループを任せるには、以下が必要です:
- AI Agent が戦略コードを編集できる(Claude Code、Cursor などのエディタ統合 AI で可能)
- AI Agent がバックテストを実行できる(CLI 経由なら可能)
- AI Agent が「結果」を構造化データとして受け取れる
- AI Agent が「結果」を他のセッションに保存して参照できる
既存のバックテスターは 1, 2 はできますが、3 と 4 が弱い。AI に「バックテスト結果どうだった?」と聞いても、CSV を見に行かせる必要があります。
Agent API の設計
Hawk-Backtester には Agent API を用意して、以下を可能にしました:
- バックテスト結果を
result_id付きで保存 - AI Agent が
curlや SDK 経由でそのresult_idにアクセス - 過去のイテレーションの結果を参照して、次の仮説を立てる
実例を次の「使ってみた」編で詳しく見ますが、Claude Code から以下のようなことができます:
# AI Agent がバックテストを実行し、結果を保存
python3 hawk_auto_tune.py # 内部で hawk-bt 経由で実行
curl -X POST https://app.hawk-backtester.com/api/.../save \
-H "Authorization: Bearer xxx" \
-d '{"result_id": "..."}'
AI Agent が「iter6 は totalReturn 66.59% だった、iter7 の Donchian(55) は 57.91% に落ちた、iter6 の方が良かったので戻す」みたいな判断を 自律的にできる ようになります。
つまずいたポイント
WASM のメモリ管理
Rust から WASM にコンパイルしたとき、ブラウザのメモリ上限を考えた設計が必要 でした。シミュレーションが進むにつれ Ticket データが積み上がっていくので、長期間・高頻度のデータだとメモリが膨れます。
対策:
- 固定サイズの Ring Buffer で直近のデータを保持
- Ticket の詳細データは必要時にバッチで UI に送る
- High Precision Mode を オプション化
WebSocket の切断ハンドリング
長時間のバックテストで、ユーザーのローカル Python プロセスがクラッシュしたり、ブラウザがスリープしたりすると、WebSocket が切れる。再接続時に状態を復元するロジックが必要でした。
最終的には「切れたらそのバックテストはやり直し」という割り切りをしました。状態同期を完璧にするより、再実行が速い方が UX として良い、という判断です。
ヒストリカルデータの配布
これは今も悩み中です。ユーザーが自分でデータを Upload する方式にしていますが、「ちょっと触ってみたい」人にはハードルが高い。仮想データ(統計モデルで生成したダミーデータ)を生成する機能を追加して緩和しています。
使っている技術まとめ
| レイヤ | 技術 |
|---|---|
| Frontend | TypeScript |
| Simulator | Rust → WASM |
| Strategy SDK | Python (hawk-bt on PyPI) |
| 通信 | WebSocket (ローカル 127.0.0.1) |
| Backend | サーバーは薄めの認証・データ配信・結果保存のみ |
| AI Agent 連携 | REST API (Agent API) |
終わりに
「クラウドの手軽さと、ローカルの自由度・秘匿性を両立する」というテーマでバックテスターを作りました。
結果として、以下のような設計になりました:
- UIはブラウザ
- シミュレータはブラウザ内 WASM
- 戦略コードはローカル Python
- 通信は WebSocket
- AI Agent からループを回せる API 付き
次の記事では、この Hawk-Backtester に対して Claude Code を AI Agent として接続して、戦略の自律改善ループを実際に回してみた 記録を書きます。iter0 から iter8 まで8回のイテレーションで、winRate 39% → 41.6%、totalReturn 0.85% → 66.59% に到達した具体的な経緯を、スクショと共に載せる予定です。
同じような課題を持つ方、バックテスト環境の設計に興味がある方、フィードバックもらえるとすごく嬉しいです。
本記事は個人開発プロダクトの技術紹介です。