🗄️ 歴史的文書(アーカイブ) — この文書は過去の研究フェーズの記録であり、現在の結論・手法を反映していません。現在の研究状況は解説セクションを参照してください。
このシリーズは未完の下書き(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.0004LedgerState.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 段優先順位
--no-enforce-session-closeが渡された場合 → OFF(source=cli_disabled)--enforce-session-closeが渡された場合 → ON(source=cli_enabled)- 戦略が
requires_session_close=True→ ON(source=strategy_required) - デフォルト → 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 の statecompute_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 追加 (23ZScoreState+ 5RobustScaleState+ 2MeanWindowState)、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: boolsrc/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):
| Column | non-null | mean | std |
|---|---|---|---|
logret_1s_z_300s | 9,716 | 0.027 | 1.115 |
logret_5s_z_300s | 9,716 | -0.005 | 1.177 |
rv_5s_z_300s | 9,716 | 0.130 | 1.179 |
trade_vol_1s_z_300s | 9,716 | 0.121 | 1.248 |
logret_1s_z_1800s | 14,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.0がRiskManager.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.pyのrecv_ts_utc = exchange_ts_utc代入バグ(Parquet 書き出しパスのみ。ストリーム DB 書き出しパスは正常)- 影響: 過去 Parquet に保存された
recv_ts_utcは真の WS 受信時刻ではない - 対応: Phase 4c 検証で latency_arb は別の理由(GMO が ticker exchange_ts を trade ts で上書き)で REJECT 済み、緊急度は低い
- 影響: 過去 Parquet に保存された
- Maker-quote fill シミュレータ(§4.3 参照)
- Book-diff パイプライン(
queue_position_mm_joiningブロック中) HardRiskLimits.max_abs_position_ethデフォルト0.05が現行世代戦略(5 ETH)と齟齬(§6 参照)