メインコンテンツまでスキップ

🗄️ 歴史的文書(アーカイブ) — この文書は過去の研究フェーズの記録であり、現在の結論・手法を反映していません。現在の研究状況は解説セクションを参照してください。

このシリーズは未完の下書き(draft)のまま凍結されています。

04. Implementation Notes

Phase 2/4a で入った基盤拡張をまとめる。各戦略の個別ロジックは 03. Strategy Design と各 Phase 4b-x レポートを参照。

1. Holding Cost (建玉管理料) — Phase 2a

要件: GMO Coin の ETH レバレッジ建玉は 0.04%/日、06:00 JST 時点のオープン・ポジションに対して課金される。先行コードベースでは P&L に holding cost 項が存在しなかった。

実装 (src/atc/portfolio/ledger.py):

  • DEFAULT_HOLDING_COST_RATE_PER_DAY = 0.0004
  • LedgerState.holding_cost_jpy 累積フィールド
  • LedgerState.apply_holding_cost(boundary_ts_utc, price) メソッド
    • abs(position_eth) * price * rate を equity から減算、holding_cost_jpy に加算
    • price is None の場合は holding_cost_skipped_no_price を 1 インクリメントして skip

バックテスト・ランナーの統合 (src/atc/cli/backtest.py):

  • HOLDING_COST_CHARGE_TIME_JST = time(6, 0) — 06:00 JST = 21:00 UTC(前日)
  • _next_holding_cost_boundary_utc(ts) — 次の境界 UTC を計算
  • イベント・ループ内で ts >= next_boundary になるたびに ledger.apply_holding_cost() を呼び、次境界を更新
  • JSON レポートに holding_cost_total_jpy, holding_cost_charges, holding_cost_rate_per_day, holding_cost_skipped_no_price が追加

テスト: tests/test_portfolio_ledger_holding_cost.py — 9 ケース

  • long / short / flat position での課金
  • 複数境界跨ぎの累積
  • price 未提供時の skip
  • 長期保有(10 日)で約 0.4% の drag

2. Market-Stream Event Reader — Phase 2b

要件: 従来のバックテストは market_trade_events(過去データ、2021〜2026-02-15)のみ読取り可能。Phase 4b-x の新戦略は WS ストリーム・ウィンドウ(2026-02-17〜2026-04-19)と orderbook データが必要だが、market_stream_orderbooks(167M rows, 327GB) / market_stream_trades / market_stream_ticker から canonical event への変換パスが無かった。

実装 (src/atc/data/postgres_stream_events.py):

  • iter_stream_canonical_events(conn, symbol, start, end) -> Iterator[CanonicalEvent]
  • サーバサイド・カーソル(cur.itersize=5000)で 3 テーブルを exchange_ts_utc ASC で個別に読み、K-way merge で時系列順に統合
  • Orderbook JSONB: [{"size":"0.4","price":"368127"}, ...] 30 段両側 → OrderbookLevel のリストに変換
  • Ticker の last を TRADE イベントと重複させないよう dedup(TRADE で同時刻同 symbol / size / price が既にあれば TICKER からの last を suppress)

CLI 統合: atc backtest --source postgres-stream で有効化。既存 --source postgres-trades(デフォルト)は歴史データ、postgres-stream は Phase 4b-x で使用。

テスト: tests/test_postgres_stream_events.py — 2 統合テスト

  • 単一日 2026-04-15 で 3 テーブルから順序保証 merge を検証
  • Orderbook JSONB parse の level 数と型を検証

3. Session-Close Decision — Phase 2c

要件: --enforce-session-close / --no-enforce-session-close の CLI フラグと、戦略ごとの requires_session_close 属性が競合するケースを解決したい。

Strategy 側 (src/atc/strategies/base.py):

class Strategy(ABC):
# Class-level defaults; override in subclass
requires_session_close: bool = False
min_order_interval_sec: float = 300.0
supports_maker_quotes: bool = False

Runner 側 (src/atc/cli/backtest.py::_resolve_session_close_decision): 4 段優先順位

  1. --no-enforce-session-close が渡された場合 → OFF(source=cli_disabled
  2. --enforce-session-close が渡された場合 → ON(source=cli_enabled
  3. 戦略が requires_session_close=True → ON(source=strategy_required
  4. デフォルト → OFF(source=default_disabled

JSON レポートの session_close_policy フィールドに判定ソースが明記される(監査対応)。

テスト: tests/test_backtest_session_close.py — 23 ケース(4×優先順位 × enforce境界 × 05:55/05:59:50 のエッジ)

4. Intent/Execution 拡張 — Phase 4a

4.1 min_order_interval_sec per-strategy

問題: 既存デフォルトは 300 秒(5 分)。サブミニッツ戦略(sweep_counter_fade, latency_arb)はすべて throttle されていた。

実装:

  • Strategy 基底に min_order_interval_sec: float = 300.0 クラス属性
  • CLI --min-order-interval-sec フラグが戦略属性を上書き可能
  • _resolve_min_order_interval_sec() で 3段判定

4.2 limit_offset_bps on TargetPositionIntent

問題: _build_limit_price が戦略不可知で常に BUY@bid / SELL@ask を置く。latency_arb などは mid-offset の aggressive 指値を要する。

実装 (src/atc/core/intents.py):

@dataclass(frozen=True)
class TargetPositionIntent:
...
limit_offset_bps: float | None = None # positive = 内側(=より aggressive)
  • None → 従来通り BUY@bid / SELL@ask(passive)
  • +k → BUY@(bid + k bps) / SELL@(ask - k bps)(aggressive)

テスト: tests/test_intent_limit_offset.py

4.3 Multi-leg Maker Quotes — QuoteIntent

問題: mm_zero_fee_inside_spread / queue_position_mm_joining は 1 イベントで bid/ask 両方に quote を出す。単一 TargetPositionIntent では表現不可。

実装 (src/atc/execution/maker_quotes.py):

  • QuoteIntent(side, qty_eth, offset_bps, ttl_sec) — 片側の maker quote 希望
  • RestingQuote — 実際に板に乗っている quote の state
  • compute_quote_diff(resting, desired) — replace / cancel / no-change の差分計算
  • partition_intents(intents)list[TargetPositionIntent | QuoteIntent] を種別で分割
  • Strategy 基底に supports_maker_quotes: bool = False クラス属性

テスト: tests/test_maker_quotes.py — 47 ケース

現状の制約: バックテスト・ランナー側の maker-quote fill シミュレーションは未実装。mm_zero_fee_inside_spread の 1 日ドライランでは QuoteIntent が発出されるものの fill は 0 の可能性あり(Phase 4b-4 レポートで検証予定)。

5. Rust Feature Engine — Phase 4d (完了)

Phase 4d 設計書 phase4d_feature_normalization_design.md に従い rust/atc-featuregen/ 配下に特徴量正規化パイプラインを実装。

モジュール構成:

  • src/normalizers.rs (501行): Rolling z-score (Welford + deque) / Robust scale (sliding-median / MAD) / log1p / Notional-normalized / Time-of-day (sin/cos + 06:00 JST 残り秒)
  • src/engine.rs (247→530行): FeatureState に 30 accumulators 追加 (23 ZScoreState + 5 RobustScaleState + 2 MeanWindowState)、WINDOW_KEEP_US を 31M→1.83G µs に拡張
  • src/model.rs (65→120行): FeatureRow を 19 → 63 cols に拡張(4 raw additions + 4 TOD + 23 z-score + 5 robust + 2 notional-norm + 4 log)
  • src/args.rs: --normalize フラグ追加
  • src/pipeline.rs / src/chunking.rs: normalize フラグを順次/並列パイプラインに伝搬

Python 側:

  • src/atc/features/cache.py: backend whitelist {"rust", "rust-normalized"} に拡張、FEATURE_CACHE_VERSION_NORMALIZED = "v4"、manifest に normalized: bool
  • src/atc/cli/main.py: atc features cache --feature-backend rust-normalized 追加

テスト: cargo test 12/12 green (bin + lib)、uv run pytest -q --ignore=tests/integration 325 pass 1 skip

Smoke validation (2026-02-15 ETH_JPY, 1 day, 15,160 rows, 63 cols):

Columnnon-nullmeanstd
logret_1s_z_300s9,7160.0271.115
logret_5s_z_300s9,716-0.0051.177
rv_5s_z_300s9,7160.1301.179
trade_vol_1s_z_300s9,7160.1211.248
logret_1s_z_1800s14,510 (95.7%)

TOD 列(tod_sin/cos/biz_day_frac)は常に non-null、range 想定通り。

後方互換性: backend=rust/v3 の既存 Parquet は完全に未変更 — raw 4列の byte-identical を検証済み。新規 raw 追加列(spread_bps, TOD, *_log)は backend=rust でも emit されるが、正規化列は None として埋められる。

出力先:

  • 既存: data/derived/features/cache/symbol=ETH_JPY/backend=rust/features_*_v3.parquet
  • 新規: data/derived/features/cache/symbol=ETH_JPY/backend=rust-normalized/features_*_v4.parquet

後続作業 (Phase 4d-6+, 本フェーズ外):

  • 設計書 §9 に列挙された戦略書き換え(sweep_counter_fade, baseline_regime_switch_micro 等を正規化特徴量へ移行)
  • Strategy.required_feature_backend クラス属性(自動ルーティング用)
  • Pandas クロスチェック・ゴールデン・テスト(§4d-5)

6. 5 ETH ベースライン戦略の CLI 起動規約(Phase 4b-2 以降)

Phase 4b-x 世代の能動的戦略(sweep_counter_fade, imbalance_momentum_micro, 予定されている multiday_trend_carry_aware ほか)は PM 仕様で base_qty_eth = 5.0(5 ETH 建玉)をデフォルトとする。しかし src/atc/risk/constraints.py::HardRiskLimits.max_abs_position_eth のリポジトリ・デフォルトは 0.05(旧プロトタイプ由来)。そのままバックテストを起動すると、すべての TargetPositionIntent(target_qty_eth=±5.0)max_abs_position_eth exceeded で即 risk_rejected となり fill 0 になるimbalance_momentum_micro の初期 Phase 4b-2 dry-run で 176 オーダー中 176 拒否を観測)。

規約: 5 ETH ベースライン戦略を起動する際は、必ず以下の 2 フラグを合わせて指定する:

uv run atc backtest \
--symbol ETH_JPY --start <date> --end <date> \
--strategy <strategy_name> \
--source postgres-stream \
--max-abs-position-eth 10.0 \
--initial-position-eth 10.0 \
--no-enforce-session-close # 必要に応じて
  • --max-abs-position-eth 10.0RiskManager.limits.max_abs_position_eth を上書きする(これが fill を発生させる唯一の必須フラグ)。
  • --initial-position-eth 10.0 は開始ポジションを 10 ETH ロングにする。戦略のcurrent_position からの遷移が自然になるが、fill の可否には直接関係しない。
  • --no-enforce-session-close は戦略固有の requires_session_close = True を上書きして flat-close を抑制する場合に使う。imbalance_momentum_micro では仕様上 True だが、Phase 4b-2 1-day dry-run では sweep_counter_fade と挙動を揃えるため False で起動した。62-day 本番ではセッション・クローズ方針を明示的に選択すること。

注意: --initial-position-eth 単独で max_abs_position_eth を 10.0 にする効果は無い(独立 CLI フラグ、src/atc/cli/backtest.py 241-242 行参照)。Phase 4b-1 レポートの §6.4 で記述されていた「--initial-position-eth 10.0 のおかげでリスク上限が 10.0 になった」は誤認で、実際は --max-abs-position-eth 10.0 が渡されていた。

根本解決は将来検討: HardRiskLimits.max_abs_position_eth のデフォルトを現行戦略世代に合わせて引き上げる、あるいは戦略ごとに希望リスク上限を宣言できる属性を追加する(例: Strategy.required_max_abs_position_eth)。後者が Phase 4e の候補タスク。

7. 技術債・未解決項目

  • src/atc/data/ws_public.pyrecv_ts_utc = exchange_ts_utc 代入バグ(Parquet 書き出しパスのみ。ストリーム DB 書き出しパスは正常)
    • 影響: 過去 Parquet に保存された recv_ts_utc は真の WS 受信時刻ではない
    • 対応: Phase 4c 検証で latency_arb は別の理由(GMO が ticker exchange_ts を trade ts で上書き)で REJECT 済み、緊急度は低い
  • Maker-quote fill シミュレータ(§4.3 参照)
  • Book-diff パイプライン(queue_position_mm_joining ブロック中)
  • HardRiskLimits.max_abs_position_eth デフォルト 0.05 が現行世代戦略(5 ETH)と齟齬(§6 参照)

関連ページ