コンテンツにスキップ

Proposal: mid_fallback_fill_ratio の credit floor clamping (LT runtime lane)

Target agent: Codex (LT runtime lane owner)
Author: Claude (BT lane)
Date: 2026-04-10
Related BT-lane WORK_LOG: AEGIS/WORK_LOG/2026-04-10_mid_fallback_credit_floor_clamping.md
Related BT-lane commit: pricing.rs entry mid fallback clamping (see aegis_v3/aegis-bt-rs/src/engine/pricing.rs)


TL;DR

LT_RC の mid fallback entry は 2026-04-09〜04-10 に 46 件試行されたが、submit 0 件・fill 0 件 で事実上完全に機能していなかった。原因は mid_fallback_fill_ratio=0.85OrderManager の strategy-level credit floor (min_credit_ratio=0.35) の double-gate interaction。Claude 側で BT lane (pricing.rs) の entry path に clamping logic を導入して fix 済み + unit test 4 件追加で validate 済み。この proposal は同じ clamping ロジックを LT runtime (Python / paper_trading.py) に実装して BT と LT の挙動を揃える依頼


1. Starting state — 現状の LT lane

1.1 gating の二重構造 (これが根本原因)

LT 側では entry の credit floor が 2箇所 で check されている:

Gate 1 — _build_entry_broker_pricing() (aegis_v3/aegis/engine/paper_trading.py L4414-L4419)

broker_cr_threshold = self.pt_config.broker_min_credit_ratio  # デフォルト 0.15
if credit_ratio < broker_cr_threshold:
    pricing.rejection_reason = f"BROKER_CR_TOO_LOW:{credit_ratio:.3f}<{broker_cr_threshold:.3f}"
    return pricing

0.15 は permissive なので mid fallback ケースの post-CR 0.31 でも通過する。

Gate 2 — OrderManager.submit_entry() (aegis_v3/aegis/engine/order_manager.py L251-L261)

if to_open_close == "ToOpen":
    sw = (callback_data or {}).get("spread_width")
    cr = (callback_data or {}).get("min_credit_ratio", 0.35)
    dynamic_floor = sw * cr if sw and sw > 0 else self._credit_floor
    if limit_price < dynamic_floor:
        logger.warning(
            f"[OrderManager] {scenario_label} price ${limit_price:.4f} "
            f"below credit floor ${dynamic_floor:.4f} "
            f"(spread_width={sw}), rejected"
        )
        return False

ここでは scenario の strategy min_credit_ratio (= 0.35 for LT_RC) を floor に使う。これが killing gate。

1.2 _build_entry_broker_pricing() の mid fallback ブランチ (L4305-L4328)

elif self._is_spread_quote_mid_usable(spread_quote):
    used_mid_fallback = True
    bid_val = spread_quote.bid if spread_quote.bid is not None else 0.0
    effective_mid = spread_quote.mid if spread_quote.mid is not None else (bid_val + spread_quote.ask) / 2.0
    mid_ratio = self.pt_config.mid_fallback_fill_ratio  # = 0.85 固定
    total_credit = effective_mid * mid_ratio
    mid_fallback_context = {
        "bid": bid_val,
        "mid": effective_mid,
        "ratio": mid_ratio,
        "limit": total_credit,
    }
    ...

mid_ratio が固定 0.85 のため、synthetic_mid_cr 0.37 のケースで: - total_credit = 1.85 * 0.85 = 1.5725 - Gate 1 (broker 0.15): 0.3145 >= 0.15 → 通過 - Gate 2 (strategy 0.35): 1.5725 < 1.75REJECT

2. Observed damage (実測データ)

/volume1/aegis/logs/pt/pt_multi_scenario.log を 2026-04-09〜04-10 区間で集計:

2.1 live_spread_quotes.jsonl (2026-04-01〜04-09、5,393 records)

  • Market open 帯の mid fallback 該当率: 16.7% (662/3,957)
  • そのうち mid > 0 で clamp path に入る: 72.2% (478/662)
  • Fallback ケースの synthetic_mid_cr 中央値: 0.37 (CR gate 0.35 をぎりぎり通過する帯に集中)

2.2 LT_RC entry outcomes (2026-04-09〜04-10)

結果 件数
LT_RC ENTRY MID_FALLBACK 試行 46
Credit floor (Gate 2) rejected 33 (72%)
Risk gate rejected (post-reprice max_loss blow-up) 12 (26%)
Order submitted 0
Entry filled 0

2日間で 1 件もエントリできていない。mid fallback feature は完全に dead-letter 化していた。

3. Target state — 期待する最終状態

3.1 設計原則

min_credit_ratio (strategy) と mid_fallback_fill_ratio (tactical discount) を直交させる。fill_ratio は約定確率を上げる tactical discount であり、strategy edge を侵食してはならない。

3.2 Python 側の clamping logic (_build_entry_broker_pricing に追加)

elif self._is_spread_quote_mid_usable(spread_quote):
    used_mid_fallback = True
    bid_val = spread_quote.bid if spread_quote.bid is not None else 0.0
    effective_mid = spread_quote.mid if spread_quote.mid is not None else (bid_val + spread_quote.ask) / 2.0
    desired_ratio = self.pt_config.mid_fallback_fill_ratio
    width = spread.spread_width

    # Why: fill_ratio の tactical discount が credit floor (strategy min_cr) を
    # 割ると OrderManager で reject されるため、synthetic_mid_cr が gate を
    # 通過するケースに限り effective_ratio を floor ちょうどまで自動で引き上げる。
    # synthetic_mid_cr < min_cr のケースはそのまま進めて Gate 1 / Gate 2 で
    # 自然に reject させる。(2026-04-10 Claude: BT lane の対応と同じ)
    effective_cr_floor = self._resolve_effective_credit_floor(order)  # 後述
    if width > 0 and effective_mid > 0 and effective_cr_floor > 0:
        synthetic_cr = effective_mid / width
        if synthetic_cr >= effective_cr_floor:
            min_ratio_to_clear = effective_cr_floor / synthetic_cr
            effective_ratio = min(max(desired_ratio, min_ratio_to_clear), 1.0)
        else:
            effective_ratio = desired_ratio
    else:
        effective_ratio = desired_ratio

    total_credit = effective_mid * effective_ratio
    mid_fallback_context = {
        "bid": bid_val,
        "mid": effective_mid,
        "desired_ratio": desired_ratio,
        "effective_ratio": effective_ratio,
        "clamped": abs(effective_ratio - desired_ratio) > 1e-9,
        "limit": total_credit,
    }
    uic_info = f" UICs={spread_quote.leg_uics}" if spread_quote.leg_uics else ""
    clamp_tag = " CLAMPED" if mid_fallback_context["clamped"] else ""
    logger.info(
        f"ENTRY MID_FALLBACK{clamp_tag}: {order.spread.underlying} "
        f"bid={bid_val:.4f} ask={spread_quote.ask:.4f} mid={effective_mid:.4f} "
        f"desired={desired_ratio:.4f} effective={effective_ratio:.4f} "
        f"credit={total_credit:.4f} type={spread_quote.price_type_bid}"
        f"{uic_info}"
    )

3.3 _resolve_effective_credit_floor() の新設

Gate 2 (OrderManager) で使われる strategy min_credit_ratio_build_entry_broker_pricing 内で参照できるようにする。現状は scenario config から order.callback_data 経由で渡っている。実装案:

def _resolve_effective_credit_floor(self, order: OpenOrder) -> float:
    """Return the binding credit floor for mid fallback clamping.

    Why: OrderManager applies scenario.strategy.min_credit_ratio as the final
    rejection gate. To avoid feeding a limit that gets rejected downstream,
    we must clamp against the same threshold here, not against the more
    permissive pt_config.broker_min_credit_ratio (which is 0.15 by default).
    Returns max(strategy_min_cr, broker_min_credit_ratio) so whichever is
    stricter wins.
    """
    strategy_min_cr = 0.0
    if hasattr(self, "scenario") and self.scenario is not None:
        strategy = getattr(self.scenario, "strategy", None)
        if strategy is not None:
            strategy_min_cr = float(getattr(strategy, "min_credit_ratio", 0.0) or 0.0)
    broker_min_cr = float(getattr(self.pt_config, "broker_min_credit_ratio", 0.0) or 0.0)
    return max(strategy_min_cr, broker_min_cr)

: self.scenario への参照方法は既存コードの慣習に合わせて調整してください (他の箇所で self.config.strategy.min_credit_ratio 風にアクセスしている箇所があればそれに揃える)。実装の詳細は Codex の判断に委ねます。

3.4 Exit path (_build_exit_broker_pricing) は触らない

_build_exit_broker_pricing の mid fallback branch は 変更しない。理由: - Exit は credit floor gate の対象外 (OrderManager は entry でのみ floor 適用) - Exit で clamp を上に掛けると PnL を削る (低い limit = 多く profit を残す) - 実測ログでも exit mid fallback で below credit floor rejection は出ていない

BT lane でも同様の分離で、build_mid_fallback_result (exit 経由) はそのまま、build_entry_mid_fallback_result (entry 経由) で clamping を実装した。

3.5 schema.py の docstring 更新 (任意)

mid_fallback_fill_ratio: float = Field(
    0.85, ge=0.0, le=1.0,
    description=(
        "Desired fill ratio for mid-based pricing when combo bid is negative/zero. "
        "Effective ratio is clamped upward when necessary so post-discount CR "
        "does not fall below the binding credit floor "
        "(max of strategy.min_credit_ratio and pt_config.broker_min_credit_ratio). "
        "0.85 = desired 15% discount from mid when floor is not binding."
    ),
)

4. Allowed vs Forbidden changes

✅ Allowed

  • aegis_v3/aegis/engine/paper_trading.py_build_entry_broker_pricing と関連 helper
  • aegis_v3/aegis/config/schema.pymid_fallback_fill_ratio docstring のみ (default 値は変更しない)
  • aegis_v3/tests/** 以下に新規 pytest を追加
  • AEGIS/WORK_LOG/2026-04-10_codex_mid_fallback_cr_clamping.md に作業ログを追記

❌ Forbidden

  • aegis_v3/aegis-bt-rs/** は触らない (Claude lane、実装済み)
  • aegis_v3/configs/scenarios/lt_rc.yamlmid_fallback_fill_ratio: 0.85 を変更しない (clamping が自動で効くので値変更は不要)
  • aegis_v3/aegis/engine/order_manager.py の credit floor ロジックを変更しない (OrderManager の gate は保険として維持)
  • _build_exit_broker_pricing() は一切変更しない
  • pt_config.broker_min_credit_ratio の default (0.15) を変更しない

⚠️ Review-first (human confirmation 必須)

  • _build_entry_broker_pricing() 内で self.scenario にアクセスする方法 — 既存コードの慣習と違うならその旨報告し、共有された慣習に合わせる
  • callback_data 経由で strategy min_cr を取得する代替案を採用する場合、その選択理由を明記
  • 既存の _build_entry_broker_pricing 呼び出し元 (L2195 など) に signature 変更が必要な場合は変更前に停止

5. 受け入れテスト (pytest、最低限)

5.1 Entry path tests (tests/unit/engine/test_paper_trading_mid_fallback.py 等)

Test 1: clamping 発動

Given: mid=1.85, width=5, strategy.min_credit_ratio=0.35, mid_fallback_fill_ratio=0.85
When: _build_entry_broker_pricing is called with bid<=0 and mid>0
Then: effective_ratio ≈ 0.9459 (= 0.35 / 0.37)
      total_credit ≈ 1.75 (= 0.35 * width)
      credit_ratio ≈ 0.35 exactly
      pricing.rejection_reason is None
      mid_fallback_context["clamped"] is True

Test 2: clamping 不要

Given: mid=2.50, width=5, strategy.min_credit_ratio=0.35, mid_fallback_fill_ratio=0.85
When: _build_entry_broker_pricing is called with bid<=0 and mid>0
Then: effective_ratio == 0.85 (unchanged)
      total_credit ≈ 2.125
      credit_ratio ≈ 0.425
      pricing.rejection_reason is None
      mid_fallback_context["clamped"] is False

Test 3: strategy gate 失敗 (clamping せず reject)

Given: mid=1.50, width=5, strategy.min_credit_ratio=0.35, mid_fallback_fill_ratio=0.85
When: _build_entry_broker_pricing is called with bid<=0 and mid>0
       (synthetic_cr = 0.30 < 0.35)
Then: effective_ratio == 0.85 (not clamped)
      pricing.rejection_reason == "BROKER_CR_TOO_LOW:..." OR reaches OrderManager
      which rejects with 'below credit floor'

Test 4: broker_min_credit_ratio > strategy_min_credit_ratio のエッジケース

Given: strategy.min_credit_ratio=0.15, pt_config.broker_min_credit_ratio=0.20
When: _resolve_effective_credit_floor() is called
Then: returns 0.20 (broker 側が strict なのでそちらが binding)

5.2 Exit path regression

Test 5: exit path unchanged

Given: mid=1.85, width=5, strategy.min_credit_ratio=0.35, mid_fallback_fill_ratio=0.85
When: _build_exit_broker_pricing is called with bid<=0 and mid>0
Then: total_credit == 1.5725 (= 0.85 * 1.85, no clamping applied)
      (verification against BT pricing.rs build_mid_fallback_result)

5.3 Integration smoke

pytest aegis_v3/tests/integration/test_paper_trading_scan_cycle.py::test_lt_rc_mid_fallback_entry_accepted
(新設、mock Saxo adapter + lt_rc scenario config で実 scan cycle を回し、以前なら credit floor で弾かれたケースが submit まで到達することを確認)

6. 実装後の検証手順

  1. cd aegis_v3 && python -m pytest tests/unit/engine/test_paper_trading_mid_fallback.py -v → 5 tests passed
  2. cd aegis_v3 && python -m pytest tests/ → 既存テスト全通過 (リグレッションなし)
  3. LT_RC paper trading の dry run (1 cycle) を実行し、ENTRY MID_FALLBACK ログの effective=clamped フィールドが新設されているか確認
  4. 実 LT_RC を 1 営業日走らせ、以下が観測できれば成功:
  5. ENTRY MID_FALLBACK CLAMPED が出る
  6. 同ケースで below credit floor rejection が出ない
  7. 少なくとも 1 件以上の mid fallback entry が submit まで到達する

7. Stop conditions (Codex に対する)

以下の場合は作業を停止し、Claude (BT lane) にコメント:

  • _build_entry_broker_pricing() の既存 signature を変更しないと実装が通らない場合
  • self.scenario 相当のアクセスパスが paper_trading.py 内で存在しない場合 (別解が必要)
  • 既存の pytest テストに 1 件でもリグレッションが出た場合
  • Rust BT (pricing.rs) との parity 挙動が乖離することが判明した場合
  • live_runtime-*.md で別の方針が決まっている場合

8. 期待インパクト

  • LT_RC の mid fallback entry 成功率: 0% → 予想 60-80% (synthetic_cr >= 0.35 の 478 件のうち大半が通るようになる)
  • Credit floor rejection は strategy gate 失敗の真のシグナルに戻る (tactical discount の副作用ではなくなる)
  • Exit path の挙動は完全に不変 (PnL リグレッションなし)

9. リスク

  • self.scenario アクセスの慣習差でコンパイル/型エラー → 実装前に既存コードの参照パターンを grep で確認
  • strategy_min_cr が callback_data 経由で来ていて self.scenario には無い可能性 → その場合は callback_data 経由で解決
  • broker_min_credit_ratio > strategy_min_credit_ratio の config を書くユーザーがいた場合に max() の方向が正しいか → テストで担保

10. 関連ドキュメント

  • AEGIS/WORK_LOG/2026-04-08_mid_fallback_pricing.md (mid fallback 初期実装)
  • AEGIS/WORK_LOG/2026-04-10_mid_fallback_credit_floor_clamping.md (BT lane の分析と fix)
  • aegis_v3/aegis-bt-rs/src/engine/pricing.rs build_entry_mid_fallback_result() (参考実装)
  • Ticket #16633 (Saxo OpenAPI support、multileg bid=negative の根本原因)

Claude (BT lane) → Codex (LT runtime lane)