nautilus_trader 統合: アーキテクチャ¶
1. 配置原則¶
Phase 8 更新(python-helper-direct-api、2026-05): Rust 側 HTTP API(ポート 9876、
/api/replay/*//api/order/*//api/agent/*等)は全廃止し、src/replay_api.rs/src/api/order_api.rs/src/api/agent_api.rsを削除した。外部制御経路は Python helper(engine.replay_session.ReplaySession/engine.live_session.LiveSession) に集約。GUI ↔ engine 間の WebSocket IPC(ポート 19876)は維持する。
┌─────────────────────────────────────────────────────────┐
│ Rust (flowsurface 本体, iced) │
│ ├─ (旧 HTTP API port 9876 — Phase 8 で廃止) │
│ ├─ EventStore ← Klines/Trades 履歴の真実 │
│ ├─ exchange/ (暗号資産 adapter) ← データ取得用に役割を絞る │
│ └─ engine-client/ ← Python ワーカーへの IPC │
└────────────────┬────────────────────────────────────────┘
│ WebSocket IPC (port 19876, schema 3.x)
┌────────────────▼────────────────────────────────────────┐
│ Python (engine プロセス) │
│ │
│ 既存ワーカー(venue 直結) nautilus ワーカー │
│ ┌──────────────────────┐ ┌──────────────────────────┐ │
│ │ python/engine/ │ │ python/engine/nautilus/ │ │
│ │ exchanges/ │ │ ├─ engine_runner.py │ │
│ │ ・hyperliquid │ │ ├─ data_loader.py │ │
│ │ ・bybit │ │ ├─ jquants_loader.py ⭐ │ │
│ │ ・tachibana (P1) │ │ ├─ strategies/ │ │
│ └──────────────────────┘ │ ├─ clients/ │ │
│ │ │ ├─ tachibana_data ⭐│ │
│ │ │ └─ tachibana.py │ │
│ │ └─ narrative_hook.py │ │
│ └────────────┬─────────────┘ │
│ │ in-process │
│ ┌─────────────▼─────────────┐ │
│ │ nautilus_trader (PyPI) │ │
│ │ ・BacktestEngine (replay)│ │
│ │ ・LiveExecutionEngine │ │
│ │ ・LiveDataEngine ⭐ │ │
│ │ ・Strategy / OrderFactory│ │
│ │ ・BarAggregator ⭐ │ │
│ └───────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
⭐ = N1 / N2 で新設
責務分割¶
| 責務 | 所在 | 備考 |
|---|---|---|
| HTTP API のレスポンス組立 | 廃止 (Phase 8) | replay_api.rs / order_api.rs / agent_api.rs 削除済み。Python helper で代替 |
| 履歴データの正本(Klines) | Rust EventStore |
nautilus にコピー注入 |
| 過去歩み値・分足の正本(J-Quants) | S:\j-quants\ 直読み |
python/engine/nautilus/jquants_loader.py がストリーム読込 |
| バックテスト実行(replay) | Python nautilus.BacktestEngine |
Rust から「リプレイ開始」コマンドを受けて起動 |
| ライブ発注の意思決定 | Python Strategy |
ユーザー実装(N0/N1 は組み込みのみ) |
| ライブ発注の送信 | Python LiveExecutionClient |
venue ごとに 1 実装 |
| ライブ歩み値配信 | Python LiveDataClient(N2 で新設) |
立花 FD frame → TradeTick |
| 立花の認証・session 管理 | Python(既存 Phase 1 コード) | 重複実装しない |
| ナラティブの記録 | Python narrative_hook.py |
nautilus Strategy.on_event から Python 内 narrative store に直接書き込み(Phase 8 で /api/agent/narrative 経路は廃止) |
| keyring 永続化 | Rust data::config |
既存どおり |
| REPLAY pane の自動生成と identity 管理 | Rust UI(iced) | chart pane は (mode, instrument_id, pane_kind, granularity?)、order list / buying power pane は (mode, pane_kind) で identity を取り、ReplayDataLoaded IPC 受信を契機に生成判定を行う(Phase 8 以降 — それ以前は /api/replay/load HTTP 成功) |
| REPLAY 注文一覧 view | Rust UI(iced) | OrderListStore を venue で 2 view に分割。REPLAY view は venue="replay" のイベントのみ反映、バナー付き |
| REPLAY 買付余力 view | Rust UI(iced) | BuyingPowerStore を venue で 2 view に分割。REPLAY view は EngineEvent::ReplayBuyingPower のみ反映、CLMZanKaiKanougaku を一切参照しない |
| REPLAY portfolio snapshot | Python python/engine/nautilus/portfolio_view.py(新設) |
nautilus Portfolio から cash / equity / mark_to_market を 1 秒間隔で算出 |
Rust 直結(NativeBackend)は使わない: EngineClientBackend 一本に統一。
2. プロセス起動とハンドシェイク¶
既存 IPC は Command::Hello の schema_major / schema_minor 構成。現在の実装は python/engine/schemas.py の SCHEMA_MINOR を参照。本文書の記載は参考値で、最新値は常に schemas.py の SCHEMA_MINOR が正。
- Rust → Python:
Hello { schema_major: 3, schema_minor: <schemas.py 参照>, mode: "live" | "replay", capabilities: { nautilus: true } }//modeは N1.13 / D8 起動時固定 - Python → Rust:
Ready { schema_major: 3, schema_minor: <schemas.py 参照>, mode: "live" | "replay", capabilities: { nautilus: { backtest: true, live: false_until_n2 } } }//modeは N1.13 / D8 起動時固定 - Rust → Python:
SetVenueCredentials(既存) - Rust → Python:
Command::StartEngine { engine, ... }(§3 参照) engine: Backtest+Hello.mode="replay"→BacktestEngine起動 + J-Quants ロード(/api/replay/load→ §4)engine: Live+Hello.mode="live"→ N1 では既存 Phase 1 の立花 EVENT WS 閲覧経路のみ起動し、nautilusLiveExecutionEngine/LiveDataEngineは stub のまま。N2 から live engine 起動に切り替える
3. 新規 IPC メッセージ¶
engine-client/src/dto.rs に以下を追加(N1 実装分、schema 3.9 時点)。SubmitOrder / Order* 系は定義済み。本計画で追加したのは backtest engine ライフサイクル、replay データロード、speed 制御、overlay、REPLAY 買付余力:
pub enum Command {
StartEngine {
request_id: String,
engine: EngineKind, // Backtest | Live
strategy_id: String,
config: EngineStartConfig, // ticker, range, initial_cash, granularity
},
StopEngine { request_id: String, strategy_id: String },
LoadReplayData { // ⭐ N1 新設
request_id: String,
instrument_id: String, // "1301.TSE"
start_date: String, // "2024-01-01"
end_date: String, // "2024-01-31"
granularity: ReplayGranularity, // Trade | Minute | Daily
},
// ⭐ N1 新設: streaming ループ間の wall-clock pacing を変える(D4 / D7)
// Pause / Resume / Seek は N1 では追加しない(Q14 で再評価)
SetReplaySpeed { request_id: String, multiplier: u32 }, // 1 | 10 | 100
}
pub enum ReplayGranularity { Trade, Minute, Daily }
pub enum EngineEvent {
EngineStarted { strategy_id: String, account_id: String, ts_event_ms: i64 },
EngineStopped { strategy_id: String, final_equity: String, ts_event_ms: i64 },
ReplayDataLoaded { // ⭐ N1 新設
strategy_id: Option<String>, // None = 単独 LoadReplayData(戦略未起動)
bars_loaded: u64,
trades_loaded: u64,
ts_event_ms: i64,
},
PositionOpened { strategy_id, venue, instrument_id, position_id, side, opened_qty, avg_open_price, ts_event_ms },
PositionClosed { strategy_id, venue, instrument_id, position_id, realized_pnl, ts_event_ms },
// ⭐ N1 新設: OrderFilled 由来・narrative_hook が自動送出(D6)
ExecutionMarker {
strategy_id: String,
instrument_id: String,
side: OrderSide, // Buy | Sell
price: String, // 文字列精度規約
qty: Option<String>, // 約定数量(文字列精度規約)
ts_event_ms: i64,
},
// ⭐ N1 新設: Strategy.emit_signal(...) による明示送出(D6)
StrategySignal {
strategy_id: String,
instrument_id: String,
signal_kind: SignalKind, // EntryLong | EntryShort | Exit | Annotate
side: Option<OrderSide>,
price: Option<String>, // 注釈のみで価格を持たないケースあり
ts_event_ms: i64,
tag: Option<String>, // Annotate 時の任意ラベル
note: Option<String>,
},
// ⭐ N1 新設: REPLAY 買付余力(D9.6)
ReplayBuyingPower {
strategy_id: String,
cash: String, // 文字列精度規約
buying_power: String, // N1 は cash と同値(現物のみ)
equity: String, // cash + Σ position MTM
ts_event_ms: i64, // 仮想時刻
},
// ⭐ live strategy 約定後に push
LiveBuyingPower {
strategy_id: String,
cash: String, // 文字列精度規約
buying_power: String, // 立花 CLMZanKaiKanougaku 由来
equity: String, // cash + Σ position MTM
ts_event_ms: i64, // UTC ミリ秒
},
}
replay 中の市場データは既存 EngineEvent::Trades / EngineEvent::KlineUpdate を再利用する(D5)。新規 market data event は足さない。engine_runner.py の data feed 直前で「Rust 向けにも 1 件複製送出」する経路を 1 箇所追加するのみ。venue タグは "replay"。
精度保持規約(H2): 数量・価格・PnL は 文字列で運ぶ。f64 変換は Rust UI レンダラ層が最後に行う。
venue フィールド(H1): ポジション系イベントには venue を必須化。値は IPC スキーマ安定名("tachibana" / "replay")のみ。
BacktestEngine 内部 venue と外向け IPC venue の分離 (H-H): BacktestEngine 内部の venue は instrument_id 由来(例 "TSE")で扱い、IPC で送出する EngineStarted.account_id / Position* / Order* の venue タグには _IPC_VENUE_TAG = "replay" 定数を必ず使う。両者は別空間。engine_runner.py 冒頭の _IPC_VENUE_TAG を参照すること。
EngineEvent::EngineError の二役 (H-F): 同一 wire 形を (1) handshake 切断 frame と (2) StartEngine 例外通知 outbox event の両方が共有する。strategy_id == None で (1) 接続レベルエラー、strategy_id == Some(_) で (2) strategy 固有 outbox event を表す。受信側 (Rust) は strategy_id で分岐し、(1) は接続を切る・(2) は接続維持して該当 strategy state machine にだけ反映する。
clock 注入(H4 / Q3 決定): AdvanceClock Command は 実装しない。BacktestEngine.run(start, end) で自走(open-questions.md Q3)。
4. データフロー(replay モード)¶
ReplayDataLoaded IPC 受信 → Rust UI が Tick + Candlestick + 注文一覧 + 買付余力 の 4 種 pane を自動生成(identity 重複なら skip)
→ それぞれが対応する IPC(Trades / KlineUpdate / Order* / ReplayBuyingPower)を venue=replay で購読する
(chart pane の identity = (mode=replay, instrument_id, pane_kind, granularity?)、注文一覧 / 買付余力は (mode=replay, pane_kind)、D9 参照)
Phase 8 以降のロード起動経路は
ReplaySession.load(...)Python helper、または GUI のFile > Replay を開始...フォーム経由(旧POST /api/replay/loadHTTP は廃止)。
ReplaySession.load(instrument="1301.TSE", start, end, granularity="trade")
│ (helper 内部で WS attach mode または in-process spawn)
▼
engine_client.send(Command::LoadReplayData { ... })
▼
Python nautilus/jquants_loader.py
│ ストリーム読込: gzip.open("S:/j-quants/equities_trades_202401.csv.gz")
│ 銘柄フィルタ + 期間フィルタ
│ Code "13010" → InstrumentId("1301.TSE")(末尾 0 切り)
▼
TradeTick リスト → BacktestEngine.add_data(ticks)
▼
Strategy.on_trade_tick(tick) ←─ ★Strategy はここを実装する★
│ (必要なら BarAggregator 経由で on_bar も発火)
│
│ ユーザー判断: BacktestEngine.submit_order(...)
▼
nautilus SimulatedExchange
│ TradeTick の価格・サイズで約定判定(板なしなので last-trade-fill モデル)
▼
Strategy.on_event(OrderFilled)
│ narrative_hook.record(Outcome) ──→ Python 内 narrative store(旧 HTTP /api/agent/narrative は廃止)
▼
Event::OrderFilled → IPC → Rust UI / Python helper(`ReplaySession.events()`)
│
├─ OrderFilled → ExecutionMarker → iced execution layer
└─ Strategy.emit_signal → StrategySignal → iced signal layer
replay モードの約定判定: 板履歴がないため、SimulatedExchange の matching engine は 直近 TradeTick の last_price ベースで fill する。指値は last_price <= limit_price(買い)/ >= limit_price(売り)で fill する単純モデル。これは現実の板状況より楽観的だが、戦略の方向性検証には十分(spec.md §3.5.3 で利用者に明示)。
REPLAY 中は立花 CLMZanKaiKanougaku HTTP 呼び出しを order_router.py で skip する(D9.6 の誤参照防止コードガード)。
Phase 8 補足: Rust 側
/api/replay/*HTTP API は廃止された。ReplaySession.load()/run()/submit_order()等の Python helper が直接Command::LoadReplayData/Command::StartEngine/Command::SubmitOrderを engine に送る(in-process は inproc dispatcher、attach mode は WS client 経由)。
5. データフロー(live モード・立花)¶
立花 EVENT WebSocket (FD frame)
▼
python/engine/exchanges/tachibana_ws._FdFrameProcessor
│ trade dict + depth dict を合成
▼
python/engine/nautilus/clients/tachibana_data.py ⭐ N2 新設
│ trade dict → nautilus TradeTick に変換
│ LiveDataEngine.process(tick)
▼
Strategy.on_trade_tick(tick) ←─ ★replay と同一インタフェース★
│
│ ユーザー判断: LiveExecutionEngine.submit_order(...)
▼
TachibanaExecutionClient (= python/engine/nautilus/clients/tachibana.py)
│ tachibana_orders.submit_order(...) に委譲(重複実装しない)
│ POST CLMKabuNewOrder
▼
EVENT WebSocket (p_evt_cmd=EC)
▼
tachibana_event_bridge._parse_ec_frame → nautilus OrderFilled
▼
Strategy.on_event → narrative_hook
▼
Event::OrderFilled → IPC → Rust → UI 反映
6. live / replay 互換のための共通インタフェース ⭐ 2026-04-28 追記¶
ユーザー Strategy が on_trade_tick(tick) を実装すれば、以下のどちらの経路でも同じハンドラが呼ばれる:
class MyStrategy(Strategy):
def on_trade_tick(self, tick: TradeTick):
# tick.instrument_id, tick.price, tick.size, tick.ts_event は live/replay で同じ意味
...
def on_bar(self, bar: Bar):
# BarAggregator が tick から作るか、replay モードで J-Quants 直接投入
...
禁止メソッド(spec.md §3.5.2):
- on_order_book_* — replay で板を作らないため
- on_quote_tick — 同上
これらは N1.8 の lint で検出する。
6.1 再生コントロールと実行モデル¶
D4 の写像。N1 では実行モデルを 2 経路に分け、どちらでも仮想時刻 tick.ts_event の独立性を保つ。
- headless / 決定論性検証: 既存の
BacktestEngine.run(start, end)自走をそのまま使う(N0.6 / N1.9 の wall clock 非参照テストはこの経路で維持) - UI 駆動 viewer: streaming=True ループ(Tpre.1 spike 案 A)を採用し、bar/tick を 1 件ずつ
add_data([item])→run(streaming=True)→clear_data()で進める SetReplaySpeedの作用範囲: streaming ループ間の sleep のみを操作する。pacing 式は D7 の
MIN_TICK_DT_SEC = 0.001(同一マイクロ秒バーストでも UI 描画整合のため最低 1ms 刻む)SLEEP_CAP_SEC = 0.200(1 sleep の上限)- セッション境界(前場-後場 11:30〜12:30 / 引け後 / 営業日跨ぎ)は multiplier に依存せず
sleep=0で即時通過 - 仮想時刻
tick.ts_eventは J-Quants オリジナル値をそのまま流し、wall clock から独立で multiplier にも依存しない - Pause / Seek は本フェーズでは実装しない。streaming ループの suspend / 中間 tick の skip は決定論性テストの仮定や fill in-flight UX に影響するため、Q14(open-questions.md)で N2 以降に再評価する
7. 既存計画との衝突点と整理¶
| 衝突点 | 解消方針 |
|---|---|
| Phase 2「自作 Virtual Exchange Engine」 | 破棄。nautilus BacktestEngine で代替 |
| 立花 Phase 2 発注経路 | 書き直し。tachibana_nautilus.py 実装タスクに置換 |
| Rust 側発注 adapter(暗号資産) | 段階廃止。Phase N3 で nautilus 側に新規実装後、Rust の発注経路は削除 |
ナラティブの outcome 自動連携 |
そのまま。書き込み元が FillEvent から nautilus OrderFilled に変わる |
| N0 の EventStore 直読み Bar ローダ | 両立。日足長期テスト用に N0 ローダは残し、N1 の J-Quants tick ローダを並列追加 |
8. Python 単独モードへの含み¶
Rust(iced)を外す将来モードでは:
- nautilus 関連コードは
engine-clientIPC を介さず直接 Python から叩けるよう、engine_runner.pyに CLI / library 二系統のエントリを切る - 立花 Phase 1 の tkinter ログイン UI は subprocess 隔離経由で再利用(tachibana/architecture.md §7.3)
- J-Quants ローダは Rust に依存しないので Python 単独モードで完全に独立して動く