diff --git a/scripts/run_soxl_core_only_p3_isolated.py b/scripts/run_soxl_core_only_p3_isolated.py index aa60aa3..fa69b68 100644 --- a/scripts/run_soxl_core_only_p3_isolated.py +++ b/scripts/run_soxl_core_only_p3_isolated.py @@ -67,6 +67,10 @@ "market_regime_control_enabled", "market_regime_control_applied", ) +# Candidate wrappers may explicitly extend this finite set only when their +# frozen P2 config has a separately bound redirect asset. The default keeps +# the legacy v3/v4 SOXX-only contract byte-for-byte compatible. +ALLOWED_VOLATILITY_DELEVER_REDIRECT_SYMBOLS = frozenset({"SOXX", None}) class SoxlCoreOnlyP3IsolatedRunnerError(ValueError): @@ -309,7 +313,8 @@ def _summarize_source_decision(decision: object, *, as_of: datetime) -> dict[str or summary["base_blend_tier"] not in {"full", "mid", "defensive"} or summary["active_risk_asset"] not in {"SOXL", "SOXX", "SOXX+SOXL"} or not isinstance(summary["blend_gate_volatility_delever_triggered"], bool) - or summary["blend_gate_volatility_delever_redirect_symbol"] not in {"SOXX", None} + or summary["blend_gate_volatility_delever_redirect_symbol"] + not in ALLOWED_VOLATILITY_DELEVER_REDIRECT_SYMBOLS or summary["market_regime_control_enabled"] is not False or summary["market_regime_control_applied"] is not False ): diff --git a/scripts/run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py b/scripts/run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py index c4d29ff..8d2b6ea 100644 --- a/scripts/run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py +++ b/scripts/run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py @@ -31,6 +31,7 @@ def _core(): module.P2_UES_UV_LOCK_SHA256 = P2_UES_UV_LOCK_SHA256 module.P2_CANDIDATE_ID = P2_CANDIDATE_ID module.P2_CONFIG_SHA256 = P2_CONFIG_SHA256 + module.ALLOWED_VOLATILITY_DELEVER_REDIRECT_SYMBOLS = frozenset({"SOXX", "BOXX", None}) module.__file__ = str(Path(__file__).resolve()) return module diff --git a/tests/test_run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py b/tests/test_run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py index 63c19dd..a016a1d 100644 --- a/tests/test_run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py +++ b/tests/test_run_soxl_core_only_v5_longterm_drawdown_p3_isolated.py @@ -2,7 +2,9 @@ import importlib.util import json +from datetime import UTC, datetime from pathlib import Path +from types import SimpleNamespace import pytest @@ -36,3 +38,35 @@ def test_v5_runner_installs_its_own_identity_and_rejects_v4_candidate() -> None: assert module.validate_p2_candidate(json.loads(V5_CANDIDATE.read_text(encoding="utf-8")))["candidate_id"] == CANDIDATE_ID with pytest.raises(Exception, match="identity mismatch"): module.validate_p2_candidate(json.loads(V4_CANDIDATE.read_text(encoding="utf-8"))) + + +def test_v5_runner_accepts_its_frozen_boxx_delever_redirect_without_relaxing_v4() -> None: + module = _module() + v5_core = module._core() + v4_script = Path(__file__).parents[1] / "scripts" / "run_soxl_core_only_free_split_close_p3_isolated.py" + spec = importlib.util.spec_from_file_location("run_soxl_core_only_free_split_close_p3_isolated", v4_script) + assert spec and spec.loader + v4_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(v4_module) + v4_core = v4_module._core() + decision = SimpleNamespace( + positions=tuple( + SimpleNamespace(symbol=symbol, target_value=weight) + for symbol, weight in (("SOXL", 0.0), ("SOXX", 25_000.0), ("BOXX", 75_000.0)) + ), + diagnostics={ + "blend_tier": "full", + "base_blend_tier": "full", + "active_risk_asset": "SOXX", + "blend_gate_volatility_delever_triggered": True, + "blend_gate_volatility_delever_redirect_symbol": "BOXX", + "market_regime_control_enabled": False, + "market_regime_control_applied": False, + }, + ) + + result = v5_core._summarize_source_decision(decision, as_of=datetime(2026, 1, 2, tzinfo=UTC)) + + assert result["diagnostics"]["blend_gate_volatility_delever_redirect_symbol"] == "BOXX" + with pytest.raises(v4_core.SoxlCoreOnlyP3IsolatedRunnerError): + v4_core._summarize_source_decision(decision, as_of=datetime(2026, 1, 2, tzinfo=UTC))