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

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

Phase 1.5-v2 — mm_zero_fee_inside_spread は逆選択 100% が損失原因

本ページは Phase 1.5-v2 の診断レポート research_reports/phase1_5_v2_mm_zero_fee_adverse_selection.md を整理したもの。 Fix-1 で fill simulator を wire up した結果、mm_zero_fee_inside_spread-¥22.6M(62 日)でリーダーボード最下位に転落した(leaderboard-v2.md rank 15)。本診断はその損失が (A) 純粋な逆選択に 100% 帰属 し、シミュレータ・バグ (B) も設計外の戦略ロジック・バグ (C) も存在しないことを示す。

Verdict

(A) 真正な逆選択 — 100% 帰属。 (B) シミュレータ・バグ無し。(C) 設計外の戦略ロジック・バグ無し — ゼロ・オフセットで top-of-book にクォートするという設計選択が、各 fill 後に続く informed-flow ドリフトをカバーするに足る half-spread を捕捉できない、という構造的問題のみ。

Breakdown:

  • Simulator は正しい(下記 §1 logical audit)
  • 戦略はトレンド市場に re-quote し、resting quote は trade flow の informed leg に picked off される
  • 実現 30 秒 forward PnL per ETH = -58 JPY、平均 half-spread = +98 JPY — 市場は 30 秒で 1.6 bps 逆行し、spread capture を完全に飲み込んで 58 JPY の損失を上乗せ

Evidence

1 日診断ラン

Input: mm_zero_fee_inside_spread を業務日 2026-03-18、--source s3-streaminitial_position_eth=10.0, max_abs_position_eth=10.0 で実行。

fills:                    1,571   (62 日換算 ~97k — 実測 95,945 と整合)
strategy_total_pnl_jpy: -¥340,721 (62 日換算 -¥21M — 実測 -¥22M)
alpha_total_pnl_jpy: -¥135,616
turnover: 7,870 ETH
maker_quote_submissions: 124,302
maker_quote_cancellations:122,731
fill ratio: 7.48% of trades が自クォートを fill

Artifacts: /tmp/mm_diag/backtest.json, /tmp/mm_diag/markers.jsonl

Per-fill forward-return 分布

Signed forward return = (trade_mid_at_+h − fill_px) × sign(delta_qty) — fresh fill 保有者が horizon h で支払う / 受け取る JPY-per-ETH MtM。

horizonBUY avgSELL avgcombined per-ETHcombined bps
5 s-75 JPY-62 JPY-69 JPY-1.90 bps
30 s-76 JPY-41 JPY-58 JPY-1.61 bps
60 s-80 JPY-30 JPY-55 JPY-1.52 bps
300 s-131 JPY+39 JPY-46 JPY-1.28 bps
  • BUY fill: 買った後 30s で 2 bps、5 分で 3.6 bps 市場下落。2-3 bps エッジを持つ seller に picked off される
  • SELL fill: 売った後 30s で ~1 bps 市場上昇(5 分前後で mean-reversion が効くが 30s ダメージは既に確定)
  • 1s-60s の全 horizon で両サイド逆方向に negative — unambiguous な逆選択シグネチャ

全 1571 fills での +30s MtM 推定: -¥456k。実現 1 日 PnL(hold-and-close 含): -¥340k。実現損失が 30s MtM より小さいのは一部 fill が EoD までに部分回復するため、方向性は明確。

Simulator バグを排除する sanity check

  1. Fill 価格 ≈ 同時刻 trade 価格(± 1-2 bps, cross-side)。 per-fill (trade_px_at_fill − fill_price) / fill_price: BUY 平均 -2.1 bps(aggressor が自 bid より安く払う — 正)、SELL 平均 +1.8 bps(aggressor が自 ask より高く払う — 正)。1571 fills 全体 max |delta| は 12 bps(小規模 market sweep が book 2 tick 分食い込み、物理的に妥当)。非現実的経済なし。

  2. Fill size = quote size ちょうど(毎回 5.0 ETH)。 turnover 7,870 / fills 1,571 = exactly 5.0。これはシミュレータ簡略化(queue-position / partial fill なし)だが、maker に over-fill する bullish な単純化。現実なら fill は小さく遅くなるため、絶対損失は比例的に縮小する。符号・一件あたり量には影響しない。

  3. Latency gate 機能中。 1531/1571 fills が直前 submit に matched。Quote-age-at-fill: p5 = 0.34s, median = 0.98s, max = 96s — 全て 0.3s order_latency_sec を十分超過。submit event 自体を食ってはいない(code line 474)。

  4. Fill 時に quote drop。 line 512 で resting_quotessurvivors に置換(fill 済 quote は append されない)。実測: 11 distinct (side, price) ペアで > 1 fill、max は 1 ペアで 4 fills — これは simulator の二重 fill ではなく同 (side, price) タプルの正統な re-submit(prior fill 除去後に新 submit 扱い)。

  5. 同サイド連続 fill は max 3。 一方向に叩かれ続けていれば BUY-BUY-BUY-... の long run が出るが、実測 run は mean 1.31, max 3。Fill 分布は ~均衡: 784 BUY vs 787 SELL。Imbalance-lean パラメータが book バランスに機能している → 方向性ある simulator リークは棄却。

  6. Fill rate ≠ 1 fill/分。 1571 fills / 24h = 1 fill per 55s(1 fill/分ではない)。62 日スケーリングは適切に動作、trade-rate limited(全 trade の 7.5% が自 quote に hit、100% ではない)。

Spread capture 数学

Average spread at fill: 5.42 bps。Half-spread at mean fill price 361,146 = +97.9 JPY per ETH(理論 maker capture)。

Average 30s adverse drift = 1.61 bps = -58 JPY per ETH(実現)。

Net per-fill expected PnL = +98 − 98(closing half-spread) − 58(drift) ≈ -58 JPY per ETH per round trip(最終的に mid で close 前提)。1571 fills × 5 ETH = 7,855 ETH round-trip exposure → ~-¥456k/日 MtM 損失実測損失と正確に一致。

Fix-1 simulator loop audit(src/atc/cli/backtest.py:457-512

Line-by-line 再レビュー:

  • L463-467: TRADE + TradePayload での gate が正
  • L471-476: per-quote latency gate は quote.submitted_at_utc(event ts でなく strategy 自身のタイムスタンプ)を使用 — 正
  • L477-482: cross 条件は L1321-1324 _should_fill_limit_order と mirror。BUY(trade_price ≤ quote.price)/ SELL で符号正
  • L483-499: booking は apply_limit_fill(..., limit_price=quote.price, ...)SimulatedFill.price = limit_price を構築。Maker 慣例: resting quote の価格で fill。Ledger に単一 apply_fill 呼出し。ポジションは shared ExecutionManagerexecution.current_position_eth 経由で更新 → ledger / execution / strategy state が consistent
  • L512: resting_quotes = survivors で filled quote は除去され二重 fill 不可

Simulator バグなし。

What's NOT the problem

  • TTL 経由の stale quote ではない: quote age at fill は p5 0.34s, median 0.98s, p95 3.94s — 5s TTL を大きく下回る。Fill の大半は quote が "current" な状態で発生 → 逆選択は stale quote 経由でなくリアルタイムに発生
  • キャップでの在庫滞留ではない: 0.6% の fill のみ |pos|=10、fill 後の mean |pos| = 2.57。在庫軸の inventory skew + imbalance-lean は機能している
  • 即時再発火ではない: fill → 次 same-side fill の median = 71s(sub-second でない)。Fill 間は quiescent、逆選択ダメージは各独立 fill で 1500+ trades/日 累積
  • min_order_interval_sec throttle ではない: src/atc/cli/backtest.py:795-811 審査、この throttle は legacy pending_order path のみに適用、maker-quote submit path(L664-689)には未適用 — maker strategy が fast requote cadence を必要とする設計意図に整合(戦略の min_order_interval_sec = 0.5 hint と consistent)。バグではない

What IS the problem — 戦略設計レベル

戦略は "inside spread" を名乗りながら、QUOTE_OFFSET_BPS_FROM_INSIDE = 0.0 でちょうど bid/ask に quote する。結果:

  1. Zero offset では既存キューの上に積む。Simulator は trade cross 時に即時 fill 仮定だが、現実は queue position 依存で遅い。つまり simulator 結果は maker capture 率の 上限推定 — live では fill は減るが 30 秒逆行率は変わらない。Per-fill bps 損失は fill 数に比例縮小、fill 数も同様に縮小、bps-per-fill ダメージは不変。

  2. Zero taker fees は逆選択議論を変えない。Counterparty のエッジは情報源、手数料構造ではない。30 秒後の下落を知る seller は喜んで自 bid を叩き、30s 2 bps をポケット、自分はそれを失う。

  3. Adverse-selection guard(ADVERSE_VOL_500MS_THRESH = 0.8 ETH/500ms)は緩すぎる。2026-03-18 の ETH_JPY tape 平均 0.24 trades/sec では、単発 0.9 ETH trade が 500ms に入れば pause 発動するが、大半の informed flow は <0.5 ETH の小口 burst で、個々では guard を trip せず集合的に価格ドリフトを発生させる。

ミティゲーション案(追求する場合 — ただし Bottom line 参照)

  1. Quote 配置を広げる: QUOTE_OFFSET_BPS_FROM_INSIDE を 1-2 bps に。Half-spread capture と fill-selection 品質のトレードオフ。3 bps でパラメータ sweep。
  2. Adverse-vol guard を締める: ADVERSE_VOL_500MS_THRESH を 0.8 → 0.3 ETH/500ms、または multi-trade "N trades in T seconds" gate(純 count + 方向一致、cumulative size だけでない)。
  3. Orderbook imbalance でクォートを方向性スキュー: ob_imbalance_top5 > 0 のときのみ BUY quote、< 0 のときのみ SELL quote。現状は両サイド無条件、imbalance-lean は価格微調整のみ。
  4. 戦略を廃止: zero-fee 下の maker-capture 仮説は single-venue で informed flow に対して成立しない、という第一原理的結論(market-making edge = spread capture − adverse selection、single-venue small book で queue-information 無しの adverse selection は ~1.5 bps/fill でほとんどのレジームで half-spread を超過)。所見を文書化して次に進む方が高 EV の可能性。

Bottom line

-¥22.6M の損失は完全に real。 Fix-1 simulator は正常動作。mm_zero_fee_inside_spread は informed flow に対して 30s あたり per-ETH 1.6 bps で敗北、95,945 fills × 5 ETH/fill × 62 日で観測損失を正確に生成。

live では損失はむしろ拡大し得る:

  • 実 fill 経済には partial fill / queue penalty があり fill 数は減る(損失も減る)が、queue 後方 maker は逆選択 trade で worse price で fill される(本シミュレータは未モデル
  • GMO Coin 実 queue-priority ルールは小口 quoter に不利

アーティファクト

  • /tmp/mm_diag/backtest.json — 1 日再ラン結果
  • /tmp/mm_diag/markers.jsonl — 248,610 order markers(1,571 fills + timestamp / side / price / size / reason tag)
  • /tmp/mm_diag/run_diagnostic.py, analyze_fills.py, analyze_lifecycle.py, post_fill_trajectory.py, check_extremes.py — reproducible 分析スクリプト

関連ページ