🗄️ 歴史的文書(アーカイブ) — この文書は過去の研究フェーズの記録であり、現在の結論・手法を反映していません。現在の研究状況は解説セクションを参照してください。
Phase 4b-4 / Phase 1.5 — mm_zero_fee_inside_spread Silence Diagnosis
本ページは Phase 4b-4 / Phase 1.5 の診断レポート research_reports/phase4b4_mm_zero_fee_silence_diagnosis.md を整理したもの。
リーダーボード上位(α −¥565k、strat_pnl +¥25,967)の mm_zero_fee_inside_spread が、なぜ 62 日間で 1 フィルしかしなかったのかを特定する。結論は戦略ではなくシミュレータのバグ。元リーダーボードは leaderboard.md を参照。
- 期間: 2026-02-17 → 2026-04-19(62 日, ETH_JPY, run tag
phase1_5_s3) - ヘッドライン: 戦略は沈黙していない — クォートを猛烈に発行している。沈黙しているのはexecutor / fill simulator、これが resting maker quote と後続 trade をマッチさせていない。
1. レポート一覧
data/derived/reports/backtest_ETH_JPY_2026-02-17_2026-04-19_mm_zero_fee_inside_spread_phase1_5_s3.json より:
| Field | Value |
|---|---|
supports_maker_quotes | true |
intents | 20,568,443(12,033,988 events から) |
maker_quote_submissions | 5,902,523 |
maker_quote_cancellations | 5,902,523 |
orders | 1 |
fills | 1 |
forced_close_orders | 1 |
forced_close_fills | 1 |
strategy_total_pnl_jpy | +25,967(期末近くでアンワインドされた初期 10 ETH のみによる) |
数値的: ~5.9M クォート submitted、~5.9M cancelled、ゼロ filled。唯一の orders/fills エントリは初日(Feb 17)の 06:00 JST forced-close market fallback — start_position_eth=10.0 をアンワインドする。以降 61 日フラットで走行 → +¥25,967 の「PnL」は実質初期ポジションの exit P&L。
Daily log(data/derived/reports/phase1_5_logs_s3/mm_zero_fee_inside_spread.log)もこれを反映: day 1 が pnl=+25,967 fills=1、以降のすべての日が pnl=+0 fills=0。warning なし、no-quote reason なし — 戦略のガードすらリミッティング要因ではない。
2. 根本原因: Phase 4a maker-quote path にフィル・シミュレータが存在しない
src/atc/cli/backtest.py には 2 つの並行エクゼキューション・パスがある。
Legacy single-leg path(baseline_inventory_mm, momentum 等で使用)
TargetPositionIntent→pending_order: PendingLimitOrderをビルド- 全後続
TRADEイベントで(active_after_utc後):_should_fill_limit_order(pending_order, trade_price)がフィル判定 (src/atc/cli/backtest.py:395-430,:1223) - クロス成功 →
execution.apply_limit_fill(...)→ ledger fill、fills += 1
Phase 4a two-sided maker-quote path(mm_zero_fee_inside_spread で使用)
QuoteIntentlist →compute_quote_diff(...)がto_cancel / to_submitdiff 生成 (src/atc/cli/backtest.py:546-607)to_submitエントリは in-memoryresting_quotes: list[RestingQuote]に追加to_cancelエントリはリストから drop されstatus="maker_quote_cancelled"マーカ記録resting_quotesに対してそれ以外何もされない。rg resting_quotes src/によれば submit/cancel ブロックと line 269 の宣言の型アノテーションにしか出現しない。いかなる fill path にも consume されていない。
Legacy path と比較すれば: _should_fill_limit_order(pending_order, ...)(line 402)が全 trade event で呼ばれる。resting quote list に対する類似ループは存在しない。したがって戦略が emit する全 QuoteIntent は write-only: submit → 次イベントで cancel(REQUOTE_MIN_MOVE_BPS を越えて price/spread が動くため)、ad infinitum。同じコードパスは supports_maker_quotes = True を設定するあらゆる戦略を沈黙させる。
resting quote のためのフィル・シミュレータが存在しないことの確認:
$ rg 'resting_quotes|RestingQuote' src/
src/atc/execution/maker_quotes.py:23 class RestingQuote:
src/atc/execution/maker_quotes.py:48 resting: list[RestingQuote],
src/atc/execution/maker_quotes.py:52 ) -> tuple[list[RestingQuote], list[QuoteIntent]]:
src/atc/execution/maker_quotes.py:67 to_cancel: list[RestingQuote] = []
src/atc/cli/backtest.py:20 from atc.execution.maker_quotes import RestingQuote, compute_quote_diff, partition_intents
src/atc/cli/backtest.py:269 resting_quotes: list[RestingQuote] = []
src/atc/cli/backtest.py:549 if quote_intents or resting_quotes:
src/atc/cli/backtest.py:551 resting=resting_quotes,
src/atc/cli/backtest.py:560 resting_quotes = [
src/atc/cli/backtest.py:562 for r in resting_quotes
src/atc/cli/backtest.py:583 resting_quotes.append(
src/atc/cli/backtest.py:584 RestingQuote(
tests/test_maker_quotes.py は diffing、stale cancellation、TTL expiry、legacy-bypass をカバーするが、resting quote の price を trade が cross して fill がブックされるテストは無い。これは機能が半実装であることと整合する: quote bookkeeping は完成済み、fill simulation は決して wire up されなかった。
3. なぜ baseline_inventory_mm は 11,905 フィルするのか
baseline_inventory_mm は supports_maker_quotes を設定していない;legacy(レポートで supports_maker_quotes=False)。TargetPositionIntent を返す → pending_order / _should_fill_limit_order を通過(働く fill path)。legacy limit-price builder が target position を設定することで概念的に inside-spread をクォートし、これは正しくシミュレートされる。
したがって違いは パラメータではなく(MIN_SPREAD_BPS=2, MAX_SPREAD_BPS=80, IMBALANCE_LEAN_THRESH=0.25 など全て妥当)、adverse-selection / RV / session-close ガードでもなく(スロットリングはするが fill をゼロアウトしない)、mm_zero_fee_inside_spread が Phase 4a two-sided パスに opt-in し、そのシミュレータが欠けているという点。
4. 戦略自身のロジックのサニティチェック
src/atc/strategies/baselines/mm_zero_fee_inside_spread.py のパラメータ:
MIN_SPREAD_BPS=2.0,MAX_SPREAD_BPS=80.0— 非常に広いレンジ;ETH_JPY は典型的にこの中。QUOTE_QTY_ETH=5.0,INVENTORY_MAX_ETH=10.0— 妥当。ADVERSE_VOL_500MS_THRESH=0.8 ETH/500ms,RV_GUARD_5S=5 bps— 標準ガード;observable pause は引き起こすが 62 日 100% 沈黙ではない。QUOTE_TTL_SEC=5.0,REQUOTE_MIN_MOVE_BPS=1.0— 頻繁な cancellation/requote ペアを生成(submissions == cancellations == 5.9Mと整合)。
QUOTE_OFFSET_BPS_FROM_INSIDE=0.0 の下では戦略は best bid / best ask ちょうどにクォート (bid_px = bid - 0 - skew_px, ask_px = ask + 0 - skew_px)。legacy path のようにシミュレータがこれらを crossing trade にマッチしていれば fill は頻繁。戦略ロジックは本質的に正しく、downstream ランナーが intent をドロップしている。
5. 修正案
修正対象は src/atc/cli/backtest.py であって戦略ではない。戦略やそのパラメータを変更してはならない。 2 つのオプション。
Option A(推奨): backtest runner に resting-quote fill ループを追加
Legacy pending_order シミュレーションを resting_quotes リスト用にミラー。各 EventType.TRADE イベントで resting_quotes を iterate:
BUYクォート(価格P_b):trade_price <= P_bならP_bでフィル(passive maker fill)。adversely-selected variant はtrade_priceを使うこともできるが、canonical maker convention は resting price でのフィル。SELLクォート(価格P_s):trade_price >= P_sならP_sでフィル。- フィル時:
execution.apply_limit_fill(...)を signedsize_ethで呼ぶ、filled_orders増加、ledger.apply_fill(...)、execution.current_position_eth更新、resting_quotesからクォート除去(status="filled_maker_quote"OrderMarkerも append)。 - 既存の
active_after_utc相当のレイテンシゲートを尊重 — 新しく submit されたクォートはその submission を引き起こしたまさにその trade でフィルすべきでない。RestingQuoteにfillable_after_utc = submitted_at_utc + order_latency_secを保存し、fill チェックをevent.exchange_ts_utc >= fillable_after_utcでゲート。 ttl_secを従来通り尊重。INVENTORY_MAX_ETHスタイルの per-position cap は既存 risk layer 経由で尊重。
Concrete insertion point: pending_order fill ブロックの直前(src/atc/cli/backtest.py:395)に、event.event_type == EventType.TRADE で resting_quotes を iterate する sibling ループを追加。execution.apply_limit_fill(...) を再利用して PnL accounting が legacy path と正確に一致するようにする。
最小スケルトン(レビュー目的のみ — コミット禁止):
if (
event.event_type == EventType.TRADE
and isinstance(event.payload, TradePayload)
and resting_quotes
):
trade_price = float(event.payload.price)
survivors: list[RestingQuote] = []
for r in resting_quotes:
fillable_after = r.submitted_at_utc + timedelta(seconds=order_latency_sec)
if event.exchange_ts_utc < fillable_after:
survivors.append(r)
continue
crosses = (r.side == "BUY" and trade_price <= r.price) or (
r.side == "SELL" and trade_price >= r.price
)
if not crosses:
survivors.append(r)
continue
delta = r.size_eth if r.side == "BUY" else -r.size_eth
exec_result = execution.apply_limit_fill(
delta_qty_eth=delta,
ts=event.exchange_ts_utc,
limit_price=r.price,
reason=r.reason,
)
if exec_result.fill is not None:
filled_orders += 1
turnover += abs(exec_result.fill.qty_eth)
ledger.apply_fill(exec_result.fill)
state.current_position_eth = execution.current_position_eth
order_markers.append(
OrderMarker(
ts=event.exchange_ts_utc,
side=r.side,
delta_qty_eth=delta,
target_qty_eth=execution.current_position_eth,
reference_price=r.price,
status="filled_maker_quote",
fill_price=exec_result.fill.price,
reason=r.reason,
)
)
# Fill consumes the quote.
else:
survivors.append(r)
resting_quotes = survivors
対応するテストを tests/test_maker_quotes.py に追加 — trade が resting bid を cross して fill counters / ledger positions が更新されるランナーレベル・テスト。
Option B(フォールバック): mm_zero_fee_inside_spread を legacy path に変換
src/atc/strategies/baselines/mm_zero_fee_inside_spread.py:47 で supports_maker_quotes = False を設定し、quote したい側の単一 TargetPositionIntent を emit するように on_event を書き換え(例: インベントリがターゲットより下のとき bid、上のとき ask)。真の両建て挙動は失うが、現シミュレータの下で実 fill を delivery する。Option A が out of scope の場合のみ推奨。
6. パラメータ / ロジック / シミュレータのどのミスマッチ?
シミュレータ・ミスマッチ。 以下で確認:
- 5.9M submission → 戦略の on_event が quote-emission path に regularly 到達(ガードがブロックしていない)。
- 62 日 ETH_JPY テープで inside-spread クォートしても 0 fill。
rg resting_quotes src/→ リストがいかなる fill path でも read されていない。- Legacy-path 戦略(例
baseline_inventory_mm)は同じデータ上、非常に似た quoting intent で fill を得ている — 唯一の構造的差異はsupports_maker_quotes。
7. 推奨アクション
src/atc/cli/backtest.pyに Option A を実装(~40 LOC)。tests/test_maker_quotes.pyに明示的テスト(crossing trade 後に resting quote に対して少なくとも 1 フィルがブックされることを assert)でゲート。mm_zero_fee_inside_spreadの 62 日 Phase 1.5 バックテストを再ラン。- その後でのみ、戦略自身のパラメータチューニングを検討。
src/atc/strategies/baselines/mm_zero_fee_inside_spread.py への変更は不要 — その quoting ロジックは内部的に一貫している。
アーティファクト
- Report:
data/derived/reports/backtest_ETH_JPY_2026-02-17_2026-04-19_mm_zero_fee_inside_spread_phase1_5_s3.json - Log:
data/derived/reports/phase1_5_logs_s3/mm_zero_fee_inside_spread.log
関連ページ
- Leaderboard (62-day, s3-stream) —
mm_zero_fee_inside_spreadがランク 1(誤認的に)になった元のリーダーボード - Regime Decomposition (rally / chop / down) — 「フラットが rally で負け down で勝つ」という現象の再確認
- Benchmark Audit — 実質フラット戦略がベンチマークに見かけ上近いことのコスト会計的非対称性