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.85 と OrderManager の 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.75 → REJECT
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と関連 helperaegis_v3/aegis/config/schema.pyのmid_fallback_fill_ratiodocstring のみ (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.yamlのmid_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. 実装後の検証手順¶
cd aegis_v3 && python -m pytest tests/unit/engine/test_paper_trading_mid_fallback.py -v→ 5 tests passedcd aegis_v3 && python -m pytest tests/→ 既存テスト全通過 (リグレッションなし)- LT_RC paper trading の dry run (1 cycle) を実行し、
ENTRY MID_FALLBACKログのeffective=とclampedフィールドが新設されているか確認 - 実 LT_RC を 1 営業日走らせ、以下が観測できれば成功:
ENTRY MID_FALLBACK CLAMPEDが出る- 同ケースで
below credit floorrejection が出ない - 少なくとも 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.rsbuild_entry_mid_fallback_result()(参考実装)- Ticket #16633 (Saxo OpenAPI support、multileg bid=negative の根本原因)
Claude (BT lane) → Codex (LT runtime lane)