立花注文機能: 実装計画¶
Phase 8(2026-05-03 完了)注記: 本計画の Phase O0〜O3 は完了済み。Phase 8 で Rust 側 HTTP API(
src/api/order_api.rs/src/replay_api.rs/src/api/agent_api.rs/src/api/mod.rs合計約 6,756 行)を全廃し、Python helper class(engine.replay_session.LiveSession/engine.replay_session.ReplaySession)を新設。本計画内の T0.5(Rust HTTP API/api/order/submit)/ T1.3(/api/order/modify等)/ T0.6(OrderGuardConfig)の成果物は Phase 8 で削除済み。E2E テスト(旧s80_*.sh/s81_*.sh等の bash + curl)は pytest +LiveSessionに移植済み。GUI 発注は元から HTTP を経由していなかったためAction::SubmitOrder→Command::SubmitOrderIPC 直送経路で無傷。
前提条件(着手ブロッカー): 立花 Phase 1(docs/specs/venues/tachibana/implementation-plan.md)の T2(認証実装)以降が完了していること。ログイン経路は tachibana_login_flow.py + tachibana_auth.py で構成される(tachibana_login.py は存在しない)。
現状確認(2026-04-25): python/engine/exchanges/ に既存の tachibana 系ファイル:
tachibana_auth.py— 認証・セッション管理(PNoCounterを含む)tachibana_codec.py— Shift-JIS / JSON エンコードtachibana_helpers.py— 共通ヘルパ(current_p_sd_date()等)tachibana_login_dialog.py— tkinter ログインダイアログtachibana_login_flow.py— ログインフロー本体tachibana_master.py— マスタデータtachibana_url.py— 仮想 URL 管理
未実装(本計画で新規作成):
tachibana_event.py— EVENT WebSocket 受信ループ + EC パーサ。Phase O2 の Tpre.5 / T2.1 で新規作成する(FD 受信+EC 受信の合流責務を持つ)。Phase 1(docs/specs/venues/tachibana/implementation-plan.md)には EVENT 受信ループは含まれていないため、本計画で初めて導入する
O-pre の Tpre タスクは Phase 1 の認証基盤が無くても型定義だけ進められるが、T0.3 以降は Phase 1 完了が必要。
マイルストーン一覧¶
Rust UI トラック(rust-ui-plan.md)を Python トラックと並行実施する。
| Python Phase | ゴール | 並行 Rust UI Phase | 期間目安 |
|---|---|---|---|
| O-pre | nautilus 互換型のスケルトン凍結 + EVENT EC 仕様の根拠確保(実装ゼロ・型と一次資料のみ) | U-pre(並行): パネルシェル・サイドバー 🖊 ボタン・フォーム UI 構造を先行実装。IPC 依存なし | 1〜2 日 |
| O0 | 現物・成行・買のみ単発発注がデモ環境で通る + WAL idempotent replay | U0(Tpre.2 完了直後): Order Entry IPC 配線。完了後すぐ UI で O0 手動テスト可 | 3〜4 日 |
| O1 | 訂正・取消・注文一覧 | U1: Order List パネル + 訂正取消 UI | 3〜4 日 |
| O2 | EVENT EC 約定通知の購読と UI 反映、重複検知 | U2: Toast 通知 + リアルタイム更新 | 2〜3 日 |
| O3 | 信用・逆指値・期日指定・余力 API 連携 | U3: Buying Power パネル + フォーム拡張 | 4〜5 日 |
全フェーズ共通の不変条件: spec.md §6 の nautilus 互換要件に違反する PR は merge 禁止。各 PR レビューチェックリストに「立花固有用語が HTTP API / IPC / Rust UI 層に漏れていないか」を必ず入れる。
Phase O-pre: nautilus 互換型のスケルトン凍結¶
ゴール: 公開 API 契約と IPC enum を nautilus に揃えた状態で凍結する。実装は空のまま、型だけ先に固める。
Tpre.1 nautilus_trader 1.211 の型定義を一次資料として参照¶
- ✅
nautilus_trader.model.identifiers.{ClientOrderId, VenueOrderId, InstrumentId, TradeId}のソースを読み、許容文字列・長さ制約をメモ ClientOrderId: 長さ 1〜36、ASCII printable のみ(spec.md §5 注釈に記載済み)- ✅
nautilus_trader.model.enums.{OrderSide, OrderType, TimeInForce, OrderStatus, TriggerType}の enum 値を全列挙 - IPC enum として Rust
dto.rsに凍結済み(SCREAMING_SNAKE_CASE) - ✅
nautilus_trader.model.orders.{Order, MarketOrder, LimitOrder, ...}の field 構成と命名を確認 NautilusOrderEnvelopeの field 構成としてtachibana_orders.pyに実装済み- ✅ 実体ライブラリは
pyproject.tomlに追加しない(nautilus 統合は N0 まで先送り) - Tips: Q0 を
案 A + Cで確定(open-questions.md に記録済み)。型はハードコード + dict テストで CI 保証
Tpre.2 IPC スキーマ確定 ✅(2026-04-26 完了)¶
前提: Q0 確定済み(open-questions.md Q0 記録済み)
- [x] ✅ engine-client/src/dto.rs に SubmitOrderRequest / OrderSide / OrderType / TimeInForce / OrderModifyChange / OrderListFilter / 全 OrderEvent::* を追加
- ファイル: engine-client/src/dto.rs
- [x] ✅ enum の serde rename_all = "SCREAMING_SNAKE_CASE" を強制(テスト: schema_v1_3_roundtrip.rs で pin)
- [x] ✅ 新規 enum variant を凍結: Command::SetSecondPassword / Command::ForgetSecondPassword / Event::SecondPasswordRequired
- [x] ✅ Command enum の #[derive(Debug)] を手実装に切り替え。value が [REDACTED] にマスクされることをテスト検証
- テスト: engine-client/tests/command_debug_redaction.rs(5 テスト全緑)
- [x] ✅ docs/specs/data-engine/schemas/commands.json と events.json を更新(schema 1.3+)
- 全発注コマンド(SetSecondPassword / ForgetSecondPassword / SubmitOrder / ModifyOrder / CancelOrder / CancelAllOrders / GetOrderList)と全発注イベント(SecondPasswordRequired / OrderSubmitted / OrderAccepted / OrderRejected / OrderPendingUpdate / OrderPendingCancel / OrderFilled / OrderCanceled / OrderExpired / OrderListUpdated)を追加
- [x] ✅ python/engine/schemas.py に対応 pydantic モデルを追加(SCHEMA_MINOR=3)
- [x] ✅ ラウンドトリップテスト: Rust serialize → JSON で shape 確認(schema_v1_3_roundtrip.rs)、Python pydantic serialize 確認(test_order_schema_v1_3.py)
- [x] ✅ 第二暗証番号 IPC 漏洩防止テスト(D2-M5): cargo test --test creds_no_second_password_on_wire — Command::SubmitOrder JSON に second_password が含まれないこと・deny_unknown_fields による注入拒否を 3 テストで検証
- [x] ✅ DTO deny_unknown_fields テスト(D3-1): engine-client/tests/dto_deny_unknown_fields.rs(7 テスト全緑)
Tpre.3 Python NautilusOrderEnvelope 雛形 ✅(2026-04-26 完了)¶
- ✅
python/engine/exchanges/tachibana_orders.py内にNautilusOrderEnvelope(pydantic) を定義 - テスト:
python/tests/test_nautilus_order_envelope.py(5 テスト全緑) - ✅ field 構成は nautilus
Orderと一致(client_order_id/instrument_id/ 全 field) - ✅ 内部 wire 型
TachibanaWireOrderRequestを別 class として定義(写像は_envelope_to_wire()に集約) - ✅
TAGS_REGISTRYを architecture.md §10.4 のキー一覧で初期化 - Tips:
_envelope_to_wire()/submit_order()等は Phase O0 T0.4 で実装するNotImplementedErrorstub として置いた
Tpre.4 Rust OrderSessionState の client_order_id 主キー化 ✅(2026-04-26 完了)¶
- ✅
engine-client/src/order_session_state.rsを新設。ClientOrderIdnewtype 規約を踏襲 - ✅
try_insert(client_order_id, request_key) -> PlaceOrderOutcomeのシグネチャを確定 - ✅
update_venue_order_id(client_order_id, venue_order_id)を追加 - テスト:
engine-client/tests/order_session_state.rs(7 テスト全緑)
Tpre.5 EVENT EC フレームの仕様根拠を確保(Q5、Phase O2 ブロッカ解消)+ tachibana_event.py 新規作成¶
理由: Phase O2 着手時に「マニュアル PDF が無く、サンプル frame も無い」状態で詰まるのを防ぐため、O-pre で根拠を確定する。Phase O0/O1 の作業と並行で進めて良いが、O2 着手 前に必ず完了させる。
新規作成: python/engine/exchanges/tachibana_event.py をここで新規作成する(EVENT WebSocket 受信ループ含む。FD 受信+EC 受信の合流責務)。Phase 1 計画 (docs/specs/venues/tachibana/implementation-plan.md) には EVENT 受信ループは含まれていないため、本計画で初めて導入する依存関係に注意。
- ✅ flowsurface に EC パーサが存在するか確認:
c:/Users/sasai/Documents/flowsurface/exchange/src/adapter/tachibana.rsでEC/OrderEcEvent/_parse_ec_frame相当を grep - 結果: 不在。
ECはSECSの部分文字列のみヒット。専用パーサ関数は flowsurface に存在しない(2026-04-28 確認) - samples で仕様代替済みのため実装に影響なし
- ✅ EC フレーム仕様は samples に実装済みで確認不要:
.claude/skills/tachibana/samples/e_api_event_receive_tel.py(行 534–568)およびe_api_websocket_receive_tel.pyに EC フレーム仕様(^A/^B/^Cデリミタ、p_evt_cmd 値一覧、EC=注文約定通知)が Python コメントで完全に記載されている。api_event_if_v4r7.pdf/api_event_if.xlsxの入手は不要。 - 最終手段(実 frame キャプチャ): デモ環境クレデンシャル未保有のためスキップ。samples による仕様参照で Phase O2 実装は完了済みのため、実 frame キャプチャは必須ではない。デモ環境接続が可能になった際に限り実施(受付・全部約定・部分約定・取消・失効・拒否を保存)。
- ✅ 結果を open-questions.md Q5 に追記し、Q5 を「解決」マークした(2026-04-28)
Tpre.6 受け入れ条件¶
- ✅
cargo check --workspace成功(全エラー 0) - ✅ Python pytest 既存スイート緑(schema 変更起因のリグレッション 0)
- ✅ enum ラウンドトリップ網羅テスト緑(
schema_v1_3_roundtrip.rs+test_order_schema_v1_3.py) - ✅ N2 シミュレーションテスト:
python/tests/test_nautilus_order_envelope.py::test_nautilus_market_order_dict_validates— ハードコード dict でNautilusOrderEnvelope.model_validate(...)成功確認 - ✅ Q0 決定済み:
open-questions.mdQ0 に「案 A + C / nautilus 1.211.x pin」を記録 - ✅ Tpre.5 EC 仕様根拠の所在が確定 —
.claude/skills/tachibana/samples/e_api_event_receive_tel.pyに仕様コメント実装済みを確認(2026-04-28) - ✅ 不変条件マッピング表の整備(D2-M1):
docs/specs/order/invariant-tests.mdを作成・骨格記入済み
これにより以降の Phase O0〜O3 は 「型は触らない、実装だけ足す」 モードで進められる。
Phase O0: 第二暗証番号 + 現物成行買い¶
T0.1 第二暗証番号: iced modal で取得(Q1 案 D)¶
- ✅ ~~data/src/config/tachibana.rs
TachibanaCredentials.second_passwordはNone固定のまま~~(N/A: architecture.md §5 によりdata/src/config/tachibana.rsは削除済み。Python 自律管理方式への移行で Rust 側の creds/session 保持コードを全廃) - ✅ .claude/skills/tachibana/SKILL.md S2 の
DEV_TACHIBANA_SECOND_PASSWORDコメントを「ログインでは収集しない(Phase O0 以降も)/ 発注時に iced modal で取得・メモリのみ保持」に書き換え - ✅ docs/specs/venues/tachibana/architecture.md §7.7 F-H5 を「Phase O0 でも解除しない: 発注時 iced modal 取得方式に変更」と注記
T0.2 iced modal: 第二暗証番号入力 ✅(2026-04-28 完了)¶
- ✅
src/modal/second_password.rs(実装済み、src/screen/dashboard/modal/second_password.rsではなくsrc/modal/配下に着地)— Rust iced 側 modal - ✅
Event::SecondPasswordRequired { request_id }受信で modal 表示 → 入力 →Command::SetSecondPassword { value }を送信(src/main.rsで配線済み) - ✅ modal キャンセル時は 未送信のまま HTTP 側で reject
- ✅ tkinter ログインダイアログには第二暗証番号フィールドを追加しない
T0.3 IPC スキーマ 1.3 — 発注最小セット¶
注: enum 列挙体・SubmitOrderRequest shape・新規 variant(Command::SetSecondPassword / Command::ForgetSecondPassword / Event::SecondPasswordRequired)は Tpre.2 で凍結済み。本タスクは「Tpre.2 で凍結済み variant の ディスパッチ実装」だけで、Rust DTO 定義・新規 variant 追加は一切行わない。
- ✅ Python
server.pyでCommand::SubmitOrderを受信したらtachibana_orders.submit_order(envelope, second_password)にルーティング - ✅
order_type=MARKET/order_side=BUY/time_in_force=DAY/tags=["cash_margin=cash"]/trigger_type=null/post_only=false/reduce_only=false以外は Phase O0 ではOrderRejected{reason_code="UNSUPPORTED_IN_PHASE_O0"}で reject(TriggerTypeは Phase O0/O1 で null 必須、O3 までLAST固定での実装は禁止) - ✅ dispatch 層で
trigger_type != nullをUNSUPPORTED_IN_PHASE_O0で reject(観測点:test_unsupported_phase_o0.py条件 (e)) - ✅
OrderSubmitted→OrderAcceptedの 2 段イベントを順番に発火(nautilus 流)。立花応答受領前にOrderSubmitted、sOrderNumber採番後にOrderAccepted - ✅
Command::SetSecondPassword/Command::ForgetSecondPasswordのディスパッチを Python 側で有効化(enum 定義は Tpre.2 で凍結済み)。Stringで受信し、Python 側でメモリのみ保持(_second_password: str | None) - ✅
Event::SecondPasswordRequired { request_id }の発火経路実装(enum 定義は Tpre.2 で凍結済み) - ✅
Commandenum のDebug手実装は Tpre.2 で実施済みのため、ディスパッチ実装時はマスクが自動的に適用されることを確認するだけでよい(再実装不要)
受け入れテスト追加:
- ✅ 第二暗証番号 idle forget テスト(D2-M3):
python/tests/test_tachibana_session_holder.pyに idle forget / lockout / on_submit_success / on_invalid / 組み合わせテスト 17 件実装済み(ファイル名は計画と異なるが受け入れ条件をカバー)。ForgetSecondPassword受信 →_second_passwordが None になること(python/tests/test_order_dispatch.py参照) - ✅
UNSUPPORTED_IN_PHASE_O0境界テスト(C1 / D3-2):uv run pytest python/tests/test_unsupported_phase_o0.py -v—pytest.mark.parametrizeで 7 条件 × 各 2〜4 値をカバー(24 テスト全緑)
Notes:
- TachibanaCredentialsWire.second_password は Phase 1 で assert!(... is_none()) 強制済み(From<&TachibanaCredentials> for TachibanaCredentialsWire impl 内、参照: data/src/config/tachibana.rs)。Order Phase で Command::SetSecondPassword 導入と同時に Phase 1 側の #[deprecated] 化を docs/specs/venues/tachibana/implementation-plan.md 側にも書き戻すこと(双方向リンク)
T0.4 Python 側 tachibana_orders.py の写像実装¶
注: 公開 class(NautilusOrderEnvelope / SubmitOrderResult)は Tpre.3 で凍結済み。本タスクは 写像と HTTP 送信の中身だけ書く。
- ✅ flowsurface
exchange/src/adapter/tachibana.rs:1307-1387のNewOrderRequest/NewOrderResponseを pydantic で wire 専用 class として 1:1 移植: - 命名:
TachibanaWireOrderRequest/TachibanaWireOrderResponse(Wireprefix で「立花固有・公開しない」を明示) - フィールド rename 名(
sZyoutoekiKazeiC等)一致 __repr__でsecond_passwordをマスク- テスト:
test_tachibana_order_mapping.py(13 テスト全緑) - ✅
submit_order(session, second_password, envelope: NautilusOrderEnvelope) -> SubmitOrderResult: - 内部で
_envelope_to_wire(envelope, session, second_password) -> TachibanaWireOrderRequestを呼ぶ。写像は architecture.md §10.1〜§10.4 に集約 - 立花未対応の
order_type/time_in_force組合せはUnsupportedOrderErrorを上に返す - テスト:
test_tachibana_submit_order.py(4 テスト全緑) - ✅
_compose_request_payload(wire: TachibanaWireOrderRequest, p_no_counter) -> dict: p_no=p_no_counter.next(),p_sd_date=current_p_sd_date(),sCLMID="CLMKabuNewOrder",sJsonOfmt="5"- 逆指値関連デフォルト(Phase O0 は固定値)
- テスト:
test_tachibana_compose_payload.py(8 テスト全緑) - ✅ HTTP 送信:
build_request_url(session.url_request, payload)→httpx.AsyncClient.get(url)→ Shift-JIS デコード →check_response() - ✅ 第二暗証番号 idle forget タイマー(C-R2-H2 / B3R3-1):
python/engine/exchanges/tachibana_auth.pyにTachibanaSessionHolderクラスを実装済み。monotonic clock(asyncio.get_running_loop().time()/time.monotonic()フォールバック)で idle 判定。reset trigger はSetSecondPassword受信時のみ(touch()は発注時)。テスト:python/tests/test_tachibana_session_holder.py(17 テスト全緑) - ✅ 第二暗証番号 lockout state(HIGH-R6-B1):
TachibanaSessionHolderにon_invalid()/is_locked_out()/on_submit_success()実装済み。p_errno=4→SecondPasswordInvalidError(tachibana_helpers.pyに追加)→on_invalid()呼び出しで counter += 1 → 閾値到達で lockout。SubmitOrder成功時にon_submit_success()で counter リセット - ✅ lockout 中の発注 reject(HIGH-R6-B1):
_do_submit_order/_do_modify_order/_do_cancel_order/_do_cancel_all_ordersでis_locked_out()チェック →OrderRejected{reason_code="SECOND_PASSWORD_LOCKED"}/Error{code="SECOND_PASSWORD_LOCKED"}を emit - ✅ WAL 復元 truncated 行スキップ(HIGH-R6-B1): WAL 復元(Python
read_wal_records)で末尾行に\nが無ければ truncated とみなしスキップ + WARN ログ(T0.7 で実装済み:python/engine/exchanges/tachibana_orders.py::read_wal_records) -
✅ ~~Phase 1 second_password ガード解除タスク(B3R3-4)~~(N/A:
data/src/config/tachibana.rsは architecture.md §5 により削除済み。TachibanaCredentials/TachibanaCredentialsWireとも廃止済みのため本タスク全体が不要) -
✅ ~~
with_second_passwordポジティブテスト(D4-3)~~(N/A: B3R3-4 と同じ理由で不要。data/src/config/tachibana.rs削除済み) -
✅
Submitted → Rejected即時遷移テスト(D3-3):uv run pytest python/tests/test_submitted_to_rejected_immediate.py—p_errno=2モック応答 →Event::OrderSubmitted→Event::OrderRejected{reason_code="SESSION_EXPIRED"}の順で 2 イベントが発火しOrderAcceptedを経由しないことを assert。_map_tachibana_state_to_nautilus()の単体テストも併設すること
受け入れテスト追加:
-
✅
VENUE_UNSUPPORTED写像テスト(D4-2):uv run pytest python/tests/test_venue_unsupported_mapping.py— Pythonsubmit_orderがUnsupportedOrderErrorを raise したケースで IPC 層がEvent::OrderRejected{reason_code="VENUE_UNSUPPORTED"}に写ることを assert。UNSUPPORTED_IN_PHASE_O0とは別経路であることを明示 -
✅ 第二暗証番号 lockout テスト(HIGH-R6-D1):
uv run pytest python/tests/test_second_password_lockout.py— SECOND_PASSWORD_INVALID 3 連投 → 4 回目が HTTP 423 +reason_code="SECOND_PASSWORD_LOCKED"で reject、1800 秒経過 (freezegun.freeze_time で +1800 秒進める) で解除されることを assert -
✅ 第二暗証番号マスク横断 grep テスト:
repr / str / model_dump_json() / model_dump()はtest_tachibana_order_mapping.pyに実装済み。logging.getLogger().info(obj)テストを追加済み(test_tachibana_order_mapping.py::test_second_password_not_in_log_output) - ✅ session 切れ即停止テスト(Rust):
test_submit_after_session_frozen_returns_503— session.freeze() 後のsubmit_orderが 503 SESSION_EXPIRED を返す(src/api/order_api.rs内#[cfg(test)])。Python dispatch 側はtest_submitted_to_rejected_immediate.py/test_cancel_all_exception_propagation.pyでカバー済み - ✅ Shift-JIS 受け入れテスト: Shift-JIS 応答(ひらがな含むエラー文)→
TachibanaError.messageが UTF-8 で正しく載ることを mock httpx で検証(test_tachibana_submit_order.py::test_submit_order_japanese_error_message_survives_shift_jis_roundtrip/test_submit_order_p_errno_japanese_message_survives_shift_jis_roundtrip— 2 テスト全緑) - 仮想 URL マスクテスト:
caplogでsubmit_order実行中のログを採取しp_no=文字列が出ていないことを assert(httpx モック依存度高・手動確認で代替) - ✅ URL masker 単体テスト(D2-L1):
uv run pytest python/tests/test_url_masker.py— マスクヘルパ単体をパラメタライズで検証 - ✅
PNoCounter.peek()非使用 CI grep(B2-L2):python/tests/test_p_no_counter_peek_guard.py— tachibana_*.py ソース内.peek()呼び出しを静的 grep で検証 - ✅
p_no単調増加 property test(D2-M4):python/tests/test_p_no_counter_monotonic.py—PNoCounter.next()の単調増加を hypothesis property test + 再起動シミュレーションで検証 - ✅
expire_time_ns変換テスト(C5):python/tests/test_expire_time_ns_conversion.py—_expire_ns_to_jst_yyyymmdd()の UTC→JST YYYYMMDD 変換を複数パターンで検証。注: CLMDateZyouhou マスタガード(Phase O3 では Phase O4 送り)は Phase O3 のNoteコメントに記載済み
レビュー反映 (2026-04-26, ラウンド 1)¶
解消した指摘: - C-1: _do_submit_order 例外時 OrderRejected 未発火 → try/except 追加、SessionExpiredError 時 second_password クリア (M-14 同時解消) - C-3: TachibanaWireOrderRequest model_dump/str で second_password 平文 → @field_serializer + str = repr - H-5: SubmitOrderResult.venue_order_id 非 Optional → Optional[str] に変更 - H-6: rustfmt 差分 → cargo fmt 適用 - M-1: SetSecondPassword 空文字列サイレントスキップ → if value is not None に修正 - M-2: _tachibana_session is None チェック欠落 → NOT_LOGGED_IN reject 追加 - M-3: test_unimplemented_streams FAIL → fetch_depth_snapshot を NotImplementedError 期待から外す - M-8: update_venue_order_id Some→Some 上書き → None のみ更新可に修正 - M-9: architecture.md の try_insert 3 引数記述を 2 引数に訂正 - M-10: OrderListFilter deny_unknown_fields 追加 - M-11: _REQUIRED_TAG_PREFIX → _REQUIRED_CASH_MARGIN_TAG リネーム - M-12: _envelope_to_wire else ブランチに Phase O3 実装予定コメント追加
レビュー反映 (2026-04-26, ラウンド 2)¶
解消した指摘:
- R2-CRITICAL: H-5 fix 副作用 — OrderAccepted.venue_order_id を Rust 側も Option
持ち越し(R2 以降):
- ✅ H-1: commands.json / events.json schema 1.3 反映 — Tpre.2 で完了済みと確認(2026-04-28)
- ✅ H-2: SecondPasswordRequired fire-and-forget + 再送 SubmitOrder ポリシー — architecture.md §2.1 に仕様追記済み(2026-04-28)
- ✅ H-3: NautilusOrderEnvelope extra="ignore" 二重解析 — server.py で model_validate(raw_order) → model_validate(order.model_dump()) に修正済み(2026-04-28)
- ✅ H-4: client_order_id pydantic バリデーション — SubmitOrderRequest.client_order_id に Field(min_length=1, max_length=36) + ASCII printable バリデーター追加済み(2026-04-28)
- ✅ C-2: ForgetSecondPassword 競合ポリシー — architecture.md §2.4 に仕様追記・server.py に _submit_order_inflight_count ガード実装・テスト 6 件追加(2026-04-28)
- ✅ M-6: Python enum str パススルー — SubmitOrderRequest.order_side / order_type / time_in_force を Literal[...] 型に変更・test_submit_order_enum_validation.py 10 件追加(2026-04-28)
- ✅ M-7: ClientOrderId try_new() コンストラクタ — try_new() 実装は既存(order_session_state.rs)。境界テスト 3 件(DEL char / space / single char)を engine-client/tests/order_session_state.rs に追加(2026-04-28)
レビュー反映 (2026-04-26, ラウンド 3 — サニティチェック)¶
R3 結果: MEDIUM 以上の新規指摘ゼロ。ループ終了。
LOW のみ残存(次フェーズ起票推奨):
- Phase O1 実装時の注意: venue_order_id=None のまま残った order レコードに CancelOrder を送る経路は、CancelOrder が venue_order_id: String(非 Optional)のため None を渡せない。Phase O1 CancelOrder 実装時に venue_order_id=None → HTTP 409 + reason_code="ORDER_STATUS_UNKNOWN" で early reject するガードを入れること
明示持ち越し(ユーザー承認済み理由付き): - ✅ H-1: commands.json / events.json schema 1.3 反映 → Tpre.2 完了済みと確認(2026-04-28) - ✅ H-2: SecondPasswordRequired 再送ポリシー → architecture.md §2.1 に仕様追記済み(2026-04-28) - ✅ H-3: NautilusOrderEnvelope extra="ignore" → server.py で order.model_dump() 経由に修正済み(2026-04-28) - ✅ H-4: client_order_id pydantic バリデーション → schemas.py に追加済み、テスト 12 ケース追加(2026-04-28) - ✅ C-2: ForgetSecondPassword 競合ポリシー → architecture.md §2.4 仕様追記・server.py ガード実装・テスト 6 件(2026-04-28 完了)
T0.5 Rust HTTP API /api/order/submit ✅¶
-
Cargo.tomlにxxhash-rustを追加(xxh3feature を有効化)。request_keyのxxh3_64で使用(architecture.md §4.1) -
src/api/order_api.rs新設 - 入力スキーマバリデーション(spec.md §5)
-
engine-client/src/order_session_state.rs実装済み(OrderSessionState/ClientOrderId/PlaceOrderOutcome) -
engine_client.send(SubmitOrder)→OrderAccepted/OrderRejectedを待機 タイムアウト:tokio::time::timeout(Duration::from_secs(30), ...)/ HTTP 504 +reason_code="INTERNAL_ERROR" - HTTP 応答: 201 Created(新規)/ 200 OK(idempotent replay)/ 409 / 400 / 403 / 502 / 504(タイムアウト)
-
src/main.rsでOrderApiStateを構築しreplay_api::spawn()に渡す
実装メモ:
- テストは src/api/order_api.rs 内 #[cfg(test)] モジュールに配置(binary crate は tests/ から private モジュールにアクセス不可のため)
- events は conn.subscribe_events() を conn.send(cmd) より先に呼ぶこと(レースコンディション回避)
- submit_timeout は #[cfg(test)] の .with_timeout() ビルダーで短縮可能(タイムアウトテスト用)
T0.6 安全装置 ✅(2026-04-27 完了)¶
- ✅ 起動 config に
tachibana.order.max_qty_per_order/max_yen_per_order/require_confirmation(OrderGuardConfigstruct としてsrc/api/order_api.rsに実装) - ✅ rate limit config 実装:
tachibana.order.rate_limit_window_secs=3/rate_limit_max_hits=2を config キーとして実装し、超過時は HTTP 429 +reason_code="RATE_LIMITED"を返す(RateLimitersliding-window 実装) - ✅ config 未設定時は
/api/order/submitを 503 で reject(明示 opt-in、誤発注防止)(OrderGuardConfig::default()でenabled: false) - ✅ Python
tachibana_urlで本番 URL 検出時、os.getenv("TACHIBANA_ALLOW_PROD") != "1"なら send をブロック is_production_url(url)/guard_prod_url(url)をtachibana_url.pyに追加submit_order()の HTTP 送信直前にguard_prod_url()を呼び出す- テスト:
python/tests/test_prod_url_guard.py(12 テスト全緑)
受け入れテスト追加:
- ✅ 誤発注ガード回帰テスト(必須):
- Rust:
max_qty_per_order超 → 400 reject、max_yen_per_order超 → 400 reject、未設定 → 503 を返す cargo test(src/api/order_api.rs内 7 テスト全緑) - 同一
client_order_idで N 並列リクエスト → 1 件のみ発注される連打耐性 integration test(既存 idempotency テストでカバー済み) - ✅ REPLAY skip テスト:
config.mode == REPLAYのとき/api/order/submitは 503 +reason_code="REPLAY_MODE_ACTIVE"で即 reject —test_submit_order_replay_mode_returns_503がsrc/api/order_api.rs内#[cfg(test)]に実装済み(line 1994) - ✅ rate limit 連打抑止テスト(D2-M2 / D3-4):
src/api/order_api.rs内 4 テスト — (a) N 件目までは通る(test_rate_limit_allows_up_to_max_hits) / (b) N+1 件目が HTTP 429 +reason_code="RATE_LIMITED"(test_rate_limit_rejects_on_n_plus_1) / (c)rate_limit_window_secs経過後 counter が reset され再度通る(test_rate_limit_resets_after_window、tokio::time::pause/advance使用) / (d)(instrument_id, side, qty, price)のいずれかが不一致なら別カウンタとして独立にカウントされる(test_rate_limit_different_key_independent_counter) - 仮想 URL マスクテスト:
caplogでsubmit_order実行中のログを採取しhttps://kabuka.e-shiten.jp以外のホスト名・p_no=文字列が出ていないことを assert(httpx モック依存度高・手動確認で代替。URL masker 単体テストtest_url_masker.pyは実装済み)
T0.7 監査ログ WAL + 起動時復元(重複発注防止)¶
- ✅
python/engine/exchanges/tachibana_orders.pyに_audit_log_submit()/_audit_log_accepted()/_audit_log_rejected()を追加(architecture.md §4.2) - ✅
wal_pathパラメータ経由で WAL ファイルに append: submit行は HTTP 送信 直前にf.write(line + "\n"); f.flush(); os.fsync(f.fileno())で書く(クラッシュ安全性)accepted/rejected行は応答受領後にf.write(line + "\n"); f.flush()で書く(fsync 不要)acceptedが OS バッファ残りのままクラッシュした場合、起動時復元はunknown状態(Phase O1 GetOrderList で補完可)— この許容を実装コメントに明記済み- 第二暗証番号は絶対に書かない(テスト:
test_audit_log_no_secret.pyで grep 検証) - ✅
read_wal_records(wal_path)で WAL 復元関数を実装 - 末尾行に
\nが無ければ truncated とみなしてスキップ + WARN ログを出す(C-R5-H1) - 非存在ファイルや空ファイルは空リストを返す
- テスト:
python/tests/test_wal_truncation.py(6 テスト全緑) - ✅ Rust 側
OrderSessionState::load_from_wal()の起動時復元: アプリ起動 → 当日分 WAL を読み戻し →client_order_id ↔ request_key ↔ venue_order_idの map を復元 submitのみでaccepted/rejected無し →unknown状態で復元(Phase O1 T1.5 でGetOrderListから確定)- 同一
client_order_idで再送 →IdempotentReplayを返す - テスト:
engine-client/tests/order_session_state_wal.rs(8 テスト全緑) - ✅
request_keyの canonicalization を architecture.md §4.1 の規則どおりに実装済み(src/api/order_api.rs)。テストで pin(tags順序入替・null vs 空文字 で同一 key になることを確認) - canonicalization テスト(D2-L2):
cargo test --test request_key_canonical— 5 テスト全緑(tags順序入替 /null↔""/ 重複排除 / 異なる qty / OrderSessionState end-to-end) - ✅ WAL 冪等再送テスト: 同一論理リクエスト(tags 順序違い)の 2 連投 → 1 件 Created (201) + 1 件 IdempotentReplay (200) を Rust integration test で確認 —
test_idempotent_replay_with_different_tags_order_returns_200をsrc/api/order_api.rs内#[cfg(test)]に実装済み - ✅ WAL 第二暗証番号漏洩 grep テスト(D2-H2):
uv run pytest python/tests/test_audit_log_no_secret.py(5 テスト全緑) - WAL
.jsonl全行を grep してsecond_password/sSecondPassword等の禁止キー名が含まれないことを確認 - C-L4 制御文字エスケープが効いて
\n/\t/\x01-\x03が生のまま出力されないことを確認 - ✅ WAL truncation 復元テスト(HIGH-R6-D2)(Rust 側):
cargo test -p flowsurface-engine-client --test order_session_state_wal— WAL 末尾行が\n欠落の状態でOrderSessionState::load_from_wal()を実行し、当該行が skip +log::warn!が出ることを assert(8 テスト全緑)
T0.8 テスト ✅(2026-04-26 完了)¶
- ✅ Python pytest-httpx で flowsurface テスト群を移植(入力は
NautilusOrderEnvelope経由に置換): submit_order_returns_error_on_wrong_password_response(flowsurface tachibana.rs:4168)→python/tests/test_tachibana_error_responses.pysubmit_order_returns_error_on_market_closed_response(同 4215) → 同上submit_order_returns_error_on_invalid_issue_code_response(同 4256) → 同上(3 テスト全緑)- ✅ Python:
_envelope_to_wireの写像テーブルテスト — architecture.md §10.1〜§10.4 の各行に 1 ケースずつ(python/tests/test_tachibana_order_mapping.py28 テスト全緑、2026-04-28 完了) - ✅ Python:
_compose_request_payloadのフィールド存在 /sCLMID/sJsonOfmt/ 逆指値デフォルト(test_tachibana_compose_payload.py8 テスト全緑) - ✅ nautilus 互換性テスト: nautilus を import しない状態で、
nautilus_trader.model.orders.MarketOrder.create(...)互換の dict をNautilusOrderEnvelope.model_validate(...)で読み込み可能(field 名・enum 文字列一致を検証)—python/tests/test_nautilus_compatibility.py(6 テスト全緑) - ✅ Rust:
OrderSessionStateのCreated/IdempotentReplay/Conflict3 ケース(flowsurface 同名テストの移植)—engine-client/tests/order_session_state.rs(既存 8 テスト) - ✅ Rust:
/api/order/submitのスキーマバリデーション(不正 client_order_id、quantity=0、instrument_id 形式違反)—src/api/order_api.rs内#[cfg(test)]に追加(5 テスト全緑) - ✅ Python:
TACHIBANA_ALLOW_PROD=1ガードのテスト(test_prod_url_guard.py12 テスト全緑)—monkeypatch.delenv("TACHIBANA_ALLOW_PROD", raising=False)で本番 URL ブロック確認済み - ✅ 発注 E2E
s80_order_submit_demo.sh(2026-04-28 完了・PASS):curl POST /api/order/submitで「現物・成行・買 100 株」が通りsOrderNumberが返ること。C-M2(sSecondPasswordログ非露出)も PASS。DEV_TACHIBANA_SECOND_PASSWORD環境変数で第二暗証番号をヘッドレス注入(Q11 解決)。 - ログイン E2E は
tests/e2e/tachibana_demo_login.shとして既存・実行可能 - ✅ クラッシュリカバリ E2E
s80_order_crash_recovery_demo.sh(2026-04-28 完了・PASS): POST /api/order/submitを送信 → HTTP 201 +venue_order_id- エンジンプロセスを
taskkill /F /Tで強制終了 - 再起動 → 同一
client_order_idで再送 → HTTP 200(IdempotentReplay)+ 同一venue_order_id(重複発注なし) - C-M2(
sSecondPasswordログ非露出)PASS
Exit 条件: デモ環境で curl /api/order/submit → sOrderNumber が返る。監査ログに第二暗証番号が出ていないことを確認。クラッシュリカバリ E2E が手動で通ること。
Phase O1: 訂正(modify)・取消・一覧¶
T1.1 Python modify・取消・一覧 ✅¶
-
tachibana_orders.modify_order/cancel_order/cancel_all_orders/fetch_order_list - 関数名は nautilus 抽象 に統一(
modify_order/cancel_order/cancel_all_orders/fetch_order_list。fetch_order_listは nautilus 名そのまま)。内部で立花CLMKabuCorrectOrder等を呼ぶ - flowsurface の
CorrectOrderRequest/CancelOrderRequest/OrderListRequestを pydantic で移植(型名はTachibanaWireModifyRequest等にリネーム — T0.4 のWireprefix 規約に統一) - レスポンス型
ModifyOrderResult/CancelOrderResult/CancelAllResult/OrderRecordWire実装
T1.2 IPC 拡張 ✅¶
-
Command::ModifyOrder/CancelOrder/CancelAllOrders/GetOrderList(schema 1.3 で既実装) -
Event::OrderListUpdated/OrderPendingUpdate/OrderPendingCancel(schema 1.3 で既実装) - SCHEMA_MINOR: 3 → 4(Rust engine-client/src/lib.rs + Python engine/schemas.py)
T1.3 Rust HTTP ✅¶
-
/api/order/modify/api/order/cancel/api/order/cancel-all/api/order/list -
cancel-allは確認モーダル必須(HTTP 層では JSON body にconfirm: trueを必須とする。query param ではない。spec.md §4 に準拠) -
/api/order/cancelの Rust 実装ではOrderSessionState.get_venue_order_id(client_order_id)で lookup し、venue_order_idを Pythoncancel_order(...)に渡すこと(architecture.md §2.3)。venue_order_id = None(unknown)は 404 reject -
cancel-allのconfirmフィールド検証はテーブルテスト化(body 欠落 /confirm: false/confirm: "true"(文字列)すべて 400 reject)
T1.4 UI: 注文一覧パネル ✅(2026-04-28 完了)¶
- ✅
src/screen/dashboard/panel/orders.rs(新設 — scaffold) - ✅ 当日注文を表示・選択 → 訂正 / 取消ボタン(dashboard pane ルーターへの統合済み:
Content::OrderList/Event::OrderListMsg/Effect::OrderListAction) - ✅ 確認モーダル(成行発注時:
Action::RequestConfirm→ConfirmOrderEntrySubmit。取消時:Action::CancelOrder→ConfirmDialog→Message::ConfirmCancelOrder→ IPC) - ✅ 発注フォーム・確認モーダル・取消確認モーダルはすべて iced 側で実装(tkinter はログイン専用)
- Tips: 取消確認ダイアログは
src/main.rsのAction::CancelOrderハンドラでConfirmDialogを表示し、Message::ConfirmCancelOrderで IPC を送信する設計。Menu::Orderビュー arm にもconfirm_dialogオーバーレイを追加済み
T1.5 起動時の台帳復元 ✅(一部)¶
-
OrderSessionState::update_venue_order_id_from_list()追加(GetOrderList 応答からの venue_order_id 補完) - Python server.py に
_do_get_order_list/_do_modify_order/_do_cancel_order/_do_cancel_all_ordersdispatch handlers 追加 - ✅
client_order_id不明の注文は HTTP/api/order/modify/api/order/cancel入力としてvenue_order_idも受理できるようにする(spec.md §5、2026-04-28 完了)。IPCCommand::ModifyOrderにvenue_order_id: Option<String>追加(SCHEMA_MINOR 0→1)
T1.6 テスト ✅¶
- Python: modify・取消・一覧の正常系・session 切れ(
test_tachibana_modify_cancel_order.py13 tests) - Rust:
/api/order/cancel-allのconfirm必須チェック(body 欠落・false・文字列 "true"・true) - Rust:
test_cancel_with_unknown_venue_order_id_returns_404 - Rust:
schema_v1_4_roundtrip.rs— ModifyOrder / CancelOrder / OrderListUpdated roundtrip (12 tests) - ✅ session 切れ即停止テスト (2026-04-28 完了):
p_errno=2→reason_code=SESSION_EXPIRED→state.session.freeze()→ HTTP 502 + 次回呼び出しは 503 SESSION_EXPIRED の detection path をtest_modify_session_expired_detection_freezes_session/test_cancel_session_expired_detection_freezes_sessionで検証(src/api/order_api.rs内 B-1c セクション) - ✅ 訂正・取消 E2E
s81_order_modify_cancel_demo.sh(2026-04-28 完了・PASS): デモ環境で「指値発注 → 訂正(数量変更)→ 取消」が curl から完結。C-M2 PASS。値幅制限(Toyota 7203 は ±900 円 / 日)に注意しORDER_LIMIT_PRICE=2800をデフォルト設定。
Exit 条件: デモ環境で「指値発注 → 訂正 → 取消」が UI から完結。
Phase O2: EVENT EC 約定通知¶
T2.1 EC パーサ + tachibana_event.py 実装本体 ✅(2026-04-26 完了)¶
- ✅
tachibana_event.pyを Tpre.5 で新規作成済み(EVENT WebSocket 受信ループ含む。FD 受信+EC 受信の合流責務)。本タスクではその上に EC パース実装を載せる。Phase 1 計画 (docs/specs/venues/tachibana/implementation-plan.md) との依存関係: Phase 1 の認証セッション(tachibana_auth.py)が前提 - ✅
tachibana_event.py._parse_ec_frame(items) -> OrderEcEvent - ✅ 主要項目(architecture.md §6)の写像(p_NO/p_EDA/p_NT/p_DH/p_DSU/p_ZSU/p_OD → IPC フィールド)
- ✅ EVENT URL sanitize(C-R2-L1):
build_event_url内_check_no_control_charsで制御文字を reject(既実装) - ✅ EVENT URL sanitize 受け入れテスト(D4-4):
python/tests/test_event_url_sanitize.py— 15 テスト全緑 - ✅ マニュアル現物確認 → samples で代替: EC フィールド仕様は
.claude/skills/tachibana/samples/e_api_event_receive_tel.py(行 534–568)で確認済み。PDF 不要。
T2.2 IPC イベント拡張 ✅(2026-04-26 完了)¶
- ✅
Event::OrderFilled/OrderCanceled/OrderExpired— schema 1.3 で骨格定義済み。SCHEMA_MINOR 4→5 に bump - ✅
OrderPartiallyFilledは持たない: nautilus 流にOrderFilledのleaves_qtyで部分/全部を判定する。詳細は architecture.md §3 末尾
T2.3 重複検知 ✅(2026-04-26 完了)¶
- ✅
tachibana_event.pyのTachibanaEventClientに_seen: set[tuple[str, str]]を実装(EC 重複検知キーは(venue_order_id, trade_id)に統一。nautilus 用語) - ✅ 当日リセット:
reset_seen_trades()メソッド実装済み(夜間閉局検知時に呼び出す)
T2.4 Rust UI 反映 ✅(2026-04-28 完了)¶
- ✅ notification toast:
Message::OrderToast(Toast)variant を追加し、map_engine_event_to_tachibana()でOrderFilled/OrderCanceled/OrderExpiredを toast に変換(既存通知機構を使用) - ✅ 注文一覧パネルの行更新:
EngineEvent::OrderListUpdated→Message::OrderListUpdated→Dashboard::distribute_order_list()→ 全OrderListペインにset_orders()で配信(src/screen/dashboard.rs) - Tips:
distribute_order_listは全ウィンドウ(メイン+ポップアウト)のOrderListペインを横断して更新する。テスト:orders::tests::cancel_clicked_known_order_returns_cancel_action_with_venue_id/set_orders_replaces_previous_list(2 件追加) - 将来の改善(未実装): EC イベント(
OrderFilled/OrderCanceled/OrderExpired)受信時に自動でGetOrderListを発行する自動リフレッシュ機能。map_engine_event_to_tachibana()をOption<Message>→Vec<Message>に変更しMessage::OrderListNeedsRefreshを追加する方針。詳細:docs/specs/order/fix-order-list-auto-refresh-2026-04-28.md
T2.5 テスト ✅(2026-04-28 追加完了)¶
- ✅ Python: 実 frame サンプル(合成)でパース → 期待 IPC イベント —
python/tests/test_ec_parser.py(11 テスト全緑) - ✅ Python:
(venue_order_id, trade_id)キーの重複検知 —python/tests/test_ec_dedup.py(7 テスト全緑) - ✅ server.py EC dispatch テスト(2026-04-28 追加):
python/tests/test_ec_server_dispatch.py(8 テスト全緑)— NT=2/3/4/1 / 未知 venue_order_id / FD 無視 / 累積 qty / OrderList 逆引き補完 を検証 - ✅ EC state-machine テスト(D2-L3): 拒否 / 失効 / 部分→全部 の遷移順序を assert —
python/tests/test_ec_state_machine.py(10 テスト全緑) - ✅ Rust schema_v1_5_roundtrip テスト:
engine-client/tests/schema_v1_5_roundtrip.rs(8 テスト全緑) - EC 重複検知 E2E: 再接続を fault-injection で発生させ、同一 EC frame を 2 度受信しても
Event::OrderFilledが 1 度しか発火しないこと(単体テストはtest_ec_dedup.pyでカバー済み) - 約定 toast 目視 E2E: 発注 → 約定通知が UI に出ること(GUI ありで目視確認。デモ口座で実際に約定が発生することが条件)
Phase O2 実装完了(2026-04-28): server.py への EVENT WebSocket 配線を追加。TachibanaEventClient.receive_loop が VenueReady 後に起動し、EC → OrderFilled/OrderCanceled/OrderExpired IPC イベントへの変換が稼働。959 テスト全緑。
Exit 条件: デモ環境で発注 → 約定通知が UI に出る。再接続時の再送が UI を二重表示させないことを確認。
Phase O3: 信用・逆指値・余力 ✅(2026-04-26 完了)¶
T3.1 NewOrderRequest 拡張 ✅¶
- ✅
cash_margin = 2/4/6/8(信用新規・返済の制度・一般)—sGenkinShinyouKubunマッピング完成 - ✅
gyakusasi_order_type/gyakusasi_zyouken/gyakusasi_price— STOP_MARKET / STOP_LIMIT 対応 - ✅
expire_day = YYYYMMDD—expire_time_ns → JST YYYYMMDD変換(_expire_ns_to_jst_yyyymmdd()) - ✅ 信用返済の建玉個別指定(
tatebi_type=1+aCLMKabuHensaiData)—tategyoku_idtag 解析 - ✅ B-M4 修正(2026-04-28):
_ACCOUNT_TYPE_MAPをマニュアル確定値に修正。旧タグ名(specific_with_withholding等)を廃止しspecific/general/nisa/nisa_growthに統一。src/api/order_api.rsの旧タグ名も全置換済み(docs/specs/order/fix-account-type-map.md)
T3.2 余力・建玉 API ✅¶
- ✅
tachibana_orders.fetch_buying_power(CLMZanKaiKanougaku) +BuyingPowerResultdataclass - ✅
tachibana_orders.fetch_credit_buying_power(CLMZanShinkiKanoIjiritu) +CreditBuyingPowerResult - ✅
tachibana_orders.fetch_sellable_qty(CLMZanUriKanousuu) +SellableQtyResult - ✅
tachibana_orders.fetch_positions(CLMGenbutuKabuList/CLMShinyouTategyokuList) +PositionRecord - ✅
InsufficientFundsError例外(reason_code="INSUFFICIENT_FUNDS",shortfallfield)
T3.3 発注前ガード ✅¶
- ✅
InsufficientFundsError→OrderRejected{reason_code="INSUFFICIENT_FUNDS"}Python dispatch - ✅ Rust
reason_code_to_status("INSUFFICIENT_FUNDS")→ HTTP 403(src/api/order_api.rs) - ✅ Phase O3 解禁: LIMIT / SELL / STOP_MARKET / STOP_LIMIT / GTD —
check_phase_o0_order()更新 - ✅ Phase O3 引き続き非対応: MARKET_IF_TOUCHED / LIMIT_IF_TOUCHED / GTC / IOC / FOK
T3.4 UI ✅¶
- ✅
src/screen/dashboard/panel/buying_power.rs新設(BuyingPowerPanelscaffold) - ✅
CashMarginSelectionenum(to_tag()/label())、StopOrderForm、GtdFormstub - ✅
src/screen/dashboard/panel.rsにpub mod buying_power;追加 - ✅ 後日バグ修正(2026-04-28): サイドバーから BuyingPower ペインを新規登録した場合、VenueReady 後でも
GetBuyingPowerが自動発行されなかった問題を修正。OpenOrderPanel(ContentKind::BuyingPower)ハンドラにtachibana_state.is_ready()+buying_power_request_id.is_none()ガード付きの auto-fetch を追加。VenueReady / BuyingPowerAction / OpenOrderPanel の 3 経路をbuying_power_request_id記録で対称化し、EngineConnected時のリセットも追加。詳細:docs/specs/order/fix-buying-power-auto-fetch-on-add-2026-04-28.md。
T3.5 テスト ✅¶
- ✅
python/tests/test_tachibana_credit_orders.py(16 テスト)— 信用 cash_margin マッピング・逆指値・GTD・建玉 - ✅
python/tests/test_tachibana_buying_power.py(6 テスト)— 余力 API パース・InsufficientFundsError - ✅
python/tests/test_unsupported_phase_o0.py更新(31 テスト)— Phase O3 解禁パターン追加 - ✅
python/tests/test_order_dispatch.py更新(11 テスト)— LIMIT/SELL ガード通過テスト更新 - ✅ Rust
test_insufficient_funds_returns_403(src/api/order_api.rs)— HTTP 403 マッピング確認
Exit 条件: 信用新規買い・逆指値・期日指定がデモで完結。余力不足が UI で正しく表示。
完了確認(2026-04-26): cargo test --workspace 全緑・cargo clippy -- -D warnings クリーン・uv run pytest python/tests/ -v 714 テスト全緑
横断タスク¶
- ✅
.claude/skills/tachibana/SKILL.mdの Phase 1 制約記述を Phase O0 解禁時に更新(T0.1 内)— 第二暗証番号は「ログイン時には収集しない / Phase O0 以降は iced modal で取得・メモリのみ保持」に更新済み - ✅ docs/specs/venues/tachibana/spec.md §2.2「発注は Phase 2+」記述を「docs/specs/order/ で管理」に書き換え完了
- ✅ docs/plan/README.md の Phase ロードマップに Order Phase O0–O3 を追記完了
- ~~docs/specs/backtest/spec.md §2.3 Phase N2 に「
tachibana_orders.pyをLiveExecutionClient内で再利用」を明記~~ N/A: 変更不要。既に方針一致しており spec.md に追記しても冗長。N2 着手時に確認済みとみなす。 - ✅ nautilus 互換境界 lint テスト:
python/tests/test_nautilus_boundary_lint.py—dto.rs/schemas.py/src/Rust UI 層に立花固有禁止語(sCLMID/p_sd_date/Zyoutoeki/p_no/p_eda_no等)が含まれないことを grep で確認 - ✅ CI:
cargo test --workspace(Q-CI-1):.github/workflows/rust-tests.ymlを新設。dtolnay/rust-toolchain@stable+Swatinem/rust-cache@v2+cargo test --workspaceをpull_request+push: branches: [main]で実行(2026-04-28 完了) - ✅ 不変条件マッピング doc 整合性テスト(D3-5):
uv run pytest python/tests/test_invariant_tests_doc.py(5 テスト全緑)—invariant-tests.mdの ✅ 行に紐付く test ファイル・関数名が実在することを CI で保証 - 注文入力ペインの link_group 同期(後続 Phase): 計画書
order-entry-link-group-plan.md参照。注文入力にも銘柄概念があるため、チャート/板と同じ番号を割り当てると銘柄が同期する仕様。Phase O0 範囲では[-]ボタンの非表示分岐から OrderList/BuyingPower を除外する前提整備(State::from_config正規化 +switch_tickers_in_group防御層)を 2026-05-01 に完了済み。実装本体は別フェーズ予定
下流計画への影響¶
本計画の完了は、以下の nautilus_trader 計画フェーズのブロッカーを解除する。
| 完了フェーズ | 解除されるブロッカー | 参照先 |
|---|---|---|
| O0(現物成行買い)完了 | nautilus N1(リプレイ API 差し替え + REPLAY 仮想注文)着手可能。N1 で order_router.py が本計画の tachibana_orders.submit_order を live 経路として呼ぶ |
nautilus_trader/implementation-plan.md Phase N1 |
| O0〜O2(約定通知)完了 | nautilus N2(LiveExecutionClient デモ)着手可能。N2 は本計画の tachibana_orders.py / tachibana_event._parse_ec_frame / 監査ログ WAL がすべて稼働している前提 |
nautilus_trader/implementation-plan.md Phase N2 |
nautilus 互換不変条件: 本計画で書く
submit_order/modify_order/cancel_orderの関数シグネチャ・型・戻り値は変更しない(nautilus N2 でのLiveExecutionClient委譲先として再利用するため)。spec.md §6 の nautilus 互換要件違反は merge 禁止。IPC schema 連鎖: 本計画の Tpre.2 で schema 1.2 → 1.3 に bump する(tachibana T0.2 の schema 1.2 確定が前提)。nautilus N1.1(schema 1.3 → 1.4)は本計画の schema 1.3 ラウンドトリップテストが緑になるまで着手しないこと。連鎖の全体像は docs/plan/README.md §実装トラック詳細 を参照。
nautilus N2 移行時に行う作業(参考・本計画スコープ外)¶
architecture.md §10.6 の通り、本計画完了時点で型互換が完全に取れていれば、N2 で行うのは:
pyproject.tomlにnautilus_traderを追加python/engine/nautilus/clients/tachibana.pyを新設(LiveExecutionClient継承)し中身はtachibana_orders.submit_order(...)を呼ぶだけ_envelope_to_wireをNautilusOrderEnvelopeの代わりに本物のnautilus_trader.model.orders.Orderを受けるよう型注釈だけ書き換え(field アクセス互換のため動作変更なし)
本計画のコードは削除しない。HTTP API /api/order/* も nautilus 経路と並行して残す(手動発注・curl 経路の維持)。
レビュー反省録¶
R1 修正バッチ(2026-04-27)¶
背景: レビュー R1 で指摘された Group A(Rust 型/ロジック)・B(Python)・C(IPC スキーマ)・D(テスト追加)の 4 グループ計 28 項目を TDD(RED→GREEN→REFACTOR)で修正。
実施内容:
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| A-1 | update_venue_order_id_from_list テスト追加 |
engine-client/tests/order_session_state.rs |
✅ |
| A-2 | ClientOrderId::try_new 境界値テスト + pub(crate) |
engine-client/src/order_session_state.rs |
✅ |
| A-3 | WAL restore で request_key==0 スキップ |
engine-client/src/order_session_state.rs |
✅ |
| A-4 | update_venue_order_id 戻り値チェック + warn |
src/api/order_api.rs |
✅ |
| A-5 | RateLimiter::record_and_check 初回計測点 None ガード |
src/api/order_api.rs |
✅ |
| A-6 | trim_end_matches → strip_suffix ダブルサフィックス防止 |
src/api/order_api.rs |
✅ |
| A-7 | /api/test/* を #[cfg(debug_assertions)] でガード |
src/replay_api.rs |
✅ |
| A-8 | frozen state + SessionFrozen variant |
engine-client/src/order_session_state.rs |
✅ |
| A-9 | WAL 復元を main.rs で起動時に実行 | src/main.rs |
✅ |
| A-10 | inject_hit_at dead code 削除 |
src/api/order_api.rs |
✅ |
| A-11 | serde_json::to_string(...).unwrap_or_default() → unwrap_or_else |
src/api/order_api.rs |
✅ |
| B-1 | OrderAccepted.venue_order_id を Optional に |
python/engine/schemas.py |
✅ |
| B-3 | wal_path を _do_submit_order に渡す |
python/engine/server.py |
✅ |
| B-4 | _sanitize_for_wal の \n/\t 通過バグ修正 |
python/engine/exchanges/tachibana_orders.py |
✅ |
| B-5 | except OSError: を except Exception: 前に追加 |
python/engine/server.py |
✅ |
| B-6 | order_side="BUY" ハードコード修正 → sBaibaiKubun から読む |
python/engine/exchanges/tachibana_orders.py |
✅ |
| B-7 | receive_loop に再接続ループ追加 |
python/engine/exchanges/tachibana_event.py |
✅ |
| C-1 | OrderListFilter / SetSecondPassword に extra="forbid" |
python/engine/schemas.py |
✅ |
| C-2 | OrderRecordWire に #[serde(deny_unknown_fields)] |
engine-client/src/dto.rs |
✅ |
| D-3 | close_strategy=funari → sCondition="6" |
python/engine/exchanges/tachibana_orders.py |
✅ |
テスト追加数: Rust +9、Python +14(合計 +23)
4 コマンド検証結果:
- cargo fmt --check → OK
- cargo clippy -- -D warnings → OK
- cargo test --workspace → 全緑(0 失敗)
- uv run pytest python/tests/ -v → 728 passed(0 失敗)
R2 修正バッチ(2026-04-27)¶
背景: R1 完了後のブランチ全体レビュー(3 エージェント並列: silent-failure-hunter / rust-reviewer / general-purpose)。CRITICAL+HIGH+MEDIUM 計 16 件を修正。
解消した指摘:
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| R2-C1 | on_submit_success() 未呼び出し → invalid_count リセットされず誤 lockout |
python/engine/server.py |
✅ |
| R2-H1 | touch() が get_password() 前で idle 判定が常に False → 全 4 ハンドラで順序修正 |
python/engine/server.py |
✅ |
| R2-H2 | freeze() 未配線 → SESSION_EXPIRED 受信時に state.session.lock().await.freeze() を追加 |
src/api/order_api.rs |
✅ |
| R2-H3 | parse_order_side/type/tif ワイルドカード _ → 誤フォールバック → unreachable!() に変更 |
src/api/order_api.rs |
✅ |
| R2-H4 | expect("validated") → panic リスク → match + HTTP 500 に変換 |
src/api/order_api.rs |
✅ |
| R2-H5 | let _ = stream.write_all(...) 書き込み失敗の無言破棄 → if let Err(e) + log::debug! |
src/api/order_api.rs |
✅ |
| R2-H6 | receive_loop が接続成功のたびに retry_count リセット → 30 秒安定判定後のみリセット | python/engine/exchanges/tachibana_event.py |
✅ |
| R2-M1 | modify/cancel で on_invalid() 未呼び出し → SecondPasswordInvalidError サブクラス追加 + 配線 |
tachibana_helpers.py / server.py |
✅ |
| R2-M3 | RecvError::Lagged ログ欠如 → log::warn! 追加(4 ヶ所) |
src/api/order_api.rs |
✅ |
| R2-M4 | _do_get_order_list session=None 無言返却 → log.warning 追加 |
python/engine/server.py |
✅ |
| R2-M5 | _now() deprecated asyncio.get_event_loop() → get_running_loop() に変更 |
python/engine/exchanges/tachibana_auth.py |
✅ |
| R2-M7 | backoff 上限なし → min(backoff, 60.0) キャップ追加 |
python/engine/exchanges/tachibana_event.py |
✅ |
| R2-M8 | テスト組み合わせ不足 → test_clear_does_not_remove_lockout + test_idle_expired_while_locked_out 追加 |
test_tachibana_session_holder.py |
✅ |
テスト追加数: Rust +0、Python +3(合計 +3)
4 コマンド検証結果:
- cargo fmt --check → OK
- cargo clippy -- -D warnings → OK(警告 0 件)
- cargo test -p flowsurface-engine-client → 全緑
- uv run pytest python/tests/ -q → 744 passed(0 失敗)
R3 サニティチェック(2026-04-27)¶
R3 结果: MEDIUM 以上の新規指摘 2 件を追加発見 → 即修正。
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| R3-H1 | _do_cancel_all_orders に SecondPasswordInvalidError catch 漏れ → on_invalid() 未呼び出し |
python/engine/server.py |
✅ |
| R3-M1 | parse_order_* の other => + warn+default → validate() 保証不変条件なのに誤値許容 → unreachable!() に変更済み |
src/api/order_api.rs |
✅(R2-H3 統合) |
LOW のみ残存(次フェーズ以降):
- receive_loop の retry_count = 0 + break パターン(break 後に参照されない dead code、実害なし)
- update_venue_order_id_from_list 複数 unknown エントリ非決定性(BTreeMap 移行は Phase O2 以降 / WAL に挿入時刻追加時に対応)
ループ収束確認:
- cargo fmt --check → OK
- cargo clippy -- -D warnings → OK(警告 0 件)
- cargo test -p flowsurface-engine-client → 全緑
- uv run pytest python/tests/ -q → 744 passed(0 失敗)
MEDIUM 以上ゼロ。ループ終了。
レビュー反映 (2026-04-27, ラウンド 4 — フルスタック完了後レビュー)¶
背景: O0〜O3 全フェーズ完了後のブランチ全体レビュー(5 エージェント並列: rust-reviewer / silent-failure-hunter / type-design-analyzer / ws-compatibility-auditor / general-purpose)。CRITICAL+HIGH+MEDIUM 計 17 件を R1〜R2 で修正、R3 サニティチェックで収束確認。
R1 修正バッチ(2026-04-27):
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| C-2 | cancel_all 内ループで SecondPasswordInvalidError/SessionExpiredError を個別 except + raise | tachibana_orders.py |
✅ |
| H-A | modify_order / cancel_order に is_frozen() チェック追加 | src/api/order_api.rs |
✅ |
| H-B | parse_trigger_type 未知値を validate() で 400 reject + unreachable!() | src/api/order_api.rs |
✅ |
| H-C | OrderModifyChange に extra="forbid" 追加 | python/engine/schemas.py |
✅ |
| H-D | OrderRecordWire (Python) に extra="forbid" 追加 | python/engine/schemas.py |
✅ |
| H-F | receive_loop 正常終了後に reconnect_fn で再接続 | tachibana_event.py |
✅ |
| H-G | on_submit_success() を modify/cancel/cancel_all 成功パスに追加 | python/engine/server.py |
✅ |
| H-H | SESSION_EXPIRED 検出を split_once exact match に変更 | src/api/order_api.rs |
✅ |
| H-I | touch() を get_password() より前に移動(全 4 ハンドラ) | python/engine/server.py |
✅ |
| M-1 | HTTP wire 型 6 構造体に #[serde(deny_unknown_fields)] 追加 | src/api/order_api.rs |
✅ |
| M-2 | ForgetSecondPassword に extra="forbid" 追加 | python/engine/schemas.py |
✅ |
| M-3 | TriggerType SCREAMING_SNAKE_CASE roundtrip テスト追加 | schema_v1_3_roundtrip.rs |
✅ |
| M-5 | p_OD パース失敗時に ts_event_ms=0 → 現在時刻に変更 | tachibana_event.py |
✅ |
| M-6 | WAL_ERROR reason_code → INTERNAL_ERROR に統一 | python/engine/server.py |
✅ |
| H-E | SubmitOrderRequest IPC に request_key: u64 追加、Python WAL に書き込み(SCHEMA_MINOR 5→6) | dto.rs / order_api.rs / schemas.py / tachibana_orders.py / server.py |
✅ |
| C-1 | cancel_all fire-and-forget 設計注記を order_api.rs に追加 | src/api/order_api.rs |
✅ |
R2 修正バッチ(2026-04-27):
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| R4-M-A | cancel_all の failed_count>0 を log.warning + PARTIAL_CANCEL_FAILURE Error で通知 | python/engine/server.py |
✅ |
| R4-M-B | reconnect_fn 失敗後に stale ws を再イテレーションしない(retry_count 二重インクリメント解消) | tachibana_event.py |
✅ |
テスト追加数: Rust +12、Python +27(合計 +39)
4 コマンド検証結果(R3 サニティ後):
- cargo fmt --check → OK
- cargo clippy --workspace -- -D warnings → OK(警告 0 件)
- cargo test --workspace → 全緑
- uv run pytest python/tests/ -q → 775 passed, 2 skipped(0 失敗)
LOW のみ残存(次フェーズ以降):
- receive_loop の外側/内側終了条件が > vs >= で非対称(余分な 1 回 reconnect 試行、データロスなし)
- フレーム処理エラーで logger.error のスタックトレース欠落(exc_info=True 推奨)
- compute_request_key が 0 を返した場合の WAL スキップ(xxh3_64 で 0 は確率的に極めて低い)
- modify/cancel 操作が WAL に記録されない(WAL は submit 冪等性専用の設計、意図通り)
明示持ち越し(設計決定済み): - C-1: cancel_all SESSION_EXPIRED の即時 freeze 不可 → 設計注記で許容(案 A 確定) - H-E の IPC 経由 request_key は実装済み。Python 側の xxh3_64 独立計算(案 β)は不採用。
MEDIUM 以上ゼロ。ループ終了。
レビュー反映 (2026-04-28, 引継ぎ実装完了後レビュー)¶
背景: T1.6 session 切れ即停止テスト実装(Rust src/api/order_api.rs B-1c セクション)後のレビュー(rust-reviewer + silent-failure-hunter 並列)。
解消した指摘:
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| F1 (HIGH) | spawn_mock_engine_rejects の JoinHandle を返却・テスト末尾で await(MISSES.md 既録パターン) |
src/api/order_api.rs |
✅ |
| F2 (MEDIUM) | try_insert / update_venue_order_id 戻り値を assert でセットアップ検証 |
src/api/order_api.rs |
✅ |
| F3 (MEDIUM) | コメント "Read SubmitOrder" → "Read incoming command" | src/api/order_api.rs |
✅ |
| F4 (MEDIUM) | is_frozen() 前置 assert で 2 回目呼び出し順序保証を明示 | src/api/order_api.rs |
✅ |
| F5 (MEDIUM) | modify_order / cancel_order の SESSION_EXPIRED 検知時 reason_code を "INTERNAL_ERROR" → "SESSION_EXPIRED" に変更 |
src/api/order_api.rs |
✅ |
設計決定:
- 初回 SESSION_EXPIRED 検知(HTTP 502)の reason_code は "SESSION_EXPIRED" に統一。spec.md §6 は is_frozen() ガード(503)のみを定義しており 502 の reason_code は未規定だったが、クライアント識別性向上のため揃えた
- spawn_mock_engine_rejects は汎用ヘルパーのため既存呼び出し側(test_insufficient_funds_returns_403)は let _mock_handle = でバインドのみ(await なし)。新規テストのみ末尾で await
テスト追加数: Rust +2
最終コマンド確認:
- cargo fmt --check → OK
- cargo clippy --workspace -- -D warnings → OK(警告 0 件)
- cargo test --workspace → 全緑
- uv run pytest python/tests/ -q → 881 passed, 2 skipped
MEDIUM 以上ゼロ。ループ終了。
残課題実装完了(2026-04-28)¶
背景: open-questions.md / invariant-tests.md に「明示的に次フェーズ送り」と記録されていた設計課題(C-2 / Q-CI-1 / M-6 / M-7 / A-H2 / C-H1 / C-H2 / C-H3)を完了。
実施内容:
| ID | 内容 | ファイル | 状態 |
|---|---|---|---|
| C-2 | ForgetSecondPassword 競合ポリシー: _submit_order_inflight_count カウンタ導入 + 即時クリア + ログ分岐 + テスト 6 件 |
server.py / architecture.md §2.4 |
✅ |
| Q-CI-1 | cargo test --workspace CI ジョブ新設(dtolnay/rust-toolchain@stable + Swatinem/rust-cache@v2) |
.github/workflows/rust-tests.yml |
✅ |
| M-6 | SubmitOrderRequest.order_side / order_type / time_in_force を str → Literal[...] 型に変更 + テスト 10 件 |
python/engine/schemas.py / python/tests/test_submit_order_enum_validation.py |
✅ |
| M-7 | ClientOrderId::try_new() 境界値テスト 3 件追加(DEL char / 単文字 / スペース) |
engine-client/tests/order_session_state.rs |
✅ |
| A-H2 | reason_code SCREAMING_SNAKE_CASE 不変条件テスト新設: AST で server.py / tachibana_orders.py の全 reason_code を抽出し spec.md §5.2 canonical セットと照合(5 テスト) |
python/tests/test_invariant_reason_code.py |
✅ |
| C-H1 | mask_virtual_url(s) -> str 実装(_VIRTUAL_URL_RE = re.compile(r"https?://\S*e-shiten\.jp\S*"))+ 単体テスト 7 件 |
python/engine/exchanges/tachibana_codec.py / python/tests/test_url_masker.py |
✅ |
| C-H2 | func_replace_urlecnode 29 文字テーブル境界値テスト 11 件 + build_request_url が標準エンコーダを使っていないことの monkey-patch テスト 2 件 |
python/tests/test_url_encode_pipeline.py |
✅ |
| C-H3 | (venue_order_id, trade_id) 重複検知テストは test_ec_dedup.py(7 テスト)に実装済みを確認。invariant-tests.md を ✅ に更新 |
docs/specs/order/invariant-tests.md |
✅ |
不変条件テーブル更新: invariant-tests.md の A-H2 / C-H1 / C-H2 / C-H3 をすべて ✅(テストファイル・関数名記入済み)に更新。test_invariant_tests_doc.py(CI ガード)が全 6 テスト緑を確認済み。
テスト追加数: Python +41(947 passed, 2 skipped)
最終コマンド確認:
- uv run pytest python/tests/ -q → 947 passed, 2 skipped(0 失敗)