diff --git a/application/signal_snapshot.py b/application/signal_snapshot.py index a9f5179..a7979c3 100644 --- a/application/signal_snapshot.py +++ b/application/signal_snapshot.py @@ -30,6 +30,17 @@ "trend_rsi14_dynamic_threshold", "trend_rsi14_effective_threshold", "trend_bb_upper", + "blend_gate_volatility_delever_symbol", + "blend_gate_volatility_delever_window", + "blend_gate_volatility_delever_threshold_mode", + "blend_gate_volatility_delever_threshold", + "blend_gate_volatility_delever_dynamic_threshold", + "blend_gate_volatility_delever_dynamic_sample_count", + "blend_gate_volatility_delever_dynamic_lookback", + "blend_gate_volatility_delever_dynamic_percentile", + "blend_gate_volatility_delever_dynamic_min_periods", + "blend_gate_volatility_delever_dynamic_floor", + "blend_gate_volatility_delever_dynamic_cap", "blend_gate_volatility_delever_metric", "blend_gate_volatility_delever_triggered", ) diff --git a/decision_mapper.py b/decision_mapper.py index 214b3ff..d80ff46 100644 --- a/decision_mapper.py +++ b/decision_mapper.py @@ -54,6 +54,25 @@ "dual_drive_volatility_delever_vetoed", "dual_drive_volatility_delever_redirect_symbol", ) +_SOXL_RISK_CONTROL_EXECUTION_FIELDS = ( + "blend_gate_volatility_delever_enabled", + "blend_gate_volatility_delever_symbol", + "blend_gate_volatility_delever_window", + "blend_gate_volatility_delever_threshold_mode", + "blend_gate_volatility_delever_threshold", + "blend_gate_volatility_delever_dynamic_threshold", + "blend_gate_volatility_delever_dynamic_sample_count", + "blend_gate_volatility_delever_dynamic_lookback", + "blend_gate_volatility_delever_dynamic_percentile", + "blend_gate_volatility_delever_dynamic_min_periods", + "blend_gate_volatility_delever_dynamic_floor", + "blend_gate_volatility_delever_dynamic_cap", + "blend_gate_volatility_delever_metric", + "blend_gate_volatility_delever_triggered", + "blend_gate_volatility_delever_retention_ratio", + "blend_gate_volatility_delever_redirect_symbol", + "blend_gate_volatility_delever_removed_ratio", +) def _build_portfolio_inputs( @@ -176,6 +195,27 @@ def _attach_tqqq_risk_control_execution_fields( execution[field] = value +def _attach_soxl_risk_control_execution_fields( + plan: dict[str, Any], + *, + decision: StrategyDecision, + runtime_metadata: Mapping[str, Any] | None, +) -> None: + if _resolve_canonical_profile(str(plan.get("strategy_profile") or "")) != "soxl_soxx_trend_income": + return + execution = plan.get("execution") + if not isinstance(execution, dict): + return + diagnostics = {**dict(runtime_metadata or {}), **dict(decision.diagnostics)} + annotations = diagnostics.get("execution_annotations") + if isinstance(annotations, Mapping): + diagnostics = {**diagnostics, **dict(annotations)} + for field in _SOXL_RISK_CONTROL_EXECUTION_FIELDS: + value = diagnostics.get(field) + if value not in (None, ""): + execution[field] = value + + def _apply_reserved_cash_policy( annotations: ValueTargetExecutionAnnotations, *, @@ -610,4 +650,9 @@ def map_strategy_decision_to_plan( decision=normalized_decision, runtime_metadata=runtime_metadata, ) + _attach_soxl_risk_control_execution_fields( + plan, + decision=normalized_decision, + runtime_metadata=runtime_metadata, + ) return plan diff --git a/requirements.txt b/requirements.txt index 87552ad..ebeb662 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@e0f760255232b62481444a8c1d6637546ba2c07e -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@5fe430699e532ee444e6c2370b34da3dc8b01b06 -hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@8d539aeef707b3594af4073f4cd4c3b13140b73f +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@023641c88506c732624a7329e48b51b9dbbe3c2a +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@7d35772d1125b534d0bcca557cb6dbaf28914719 +hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@2e0075004239e7ede7ba256763a3441d4ec4ca73 pandas requests pytz diff --git a/tests/test_decision_mapper.py b/tests/test_decision_mapper.py index 8a70b57..ba321fe 100644 --- a/tests/test_decision_mapper.py +++ b/tests/test_decision_mapper.py @@ -72,6 +72,17 @@ def test_prefers_normalized_execution_annotations_when_present(self): "active_risk_asset": "SOXL", "investable_cash": 9000.0, "current_min_trade": 100.0, + "blend_gate_volatility_delever_threshold_mode": "rolling_percentile", + "blend_gate_volatility_delever_threshold": 0.60, + "blend_gate_volatility_delever_dynamic_threshold": 0.60, + "blend_gate_volatility_delever_dynamic_sample_count": 252, + "blend_gate_volatility_delever_dynamic_lookback": 252, + "blend_gate_volatility_delever_dynamic_percentile": 0.95, + "blend_gate_volatility_delever_dynamic_min_periods": 126, + "blend_gate_volatility_delever_dynamic_floor": 0.50, + "blend_gate_volatility_delever_dynamic_cap": 0.75, + "blend_gate_volatility_delever_metric": 0.61, + "blend_gate_volatility_delever_triggered": True, } }, ) @@ -94,6 +105,13 @@ def test_prefers_normalized_execution_annotations_when_present(self): self.assertEqual(plan["execution"]["signal_display"], "signal") self.assertEqual(plan["execution"]["dashboard_text"], "strategy dashboard") self.assertEqual(plan["execution"]["investable_cash"], 9000.0) + self.assertEqual(plan["execution"]["blend_gate_volatility_delever_threshold_mode"], "rolling_percentile") + self.assertEqual(plan["execution"]["blend_gate_volatility_delever_threshold"], 0.60) + self.assertEqual(plan["execution"]["blend_gate_volatility_delever_dynamic_threshold"], 0.60) + self.assertEqual(plan["execution"]["blend_gate_volatility_delever_dynamic_sample_count"], 252) + self.assertEqual(plan["execution"]["blend_gate_volatility_delever_dynamic_percentile"], 0.95) + self.assertEqual(plan["execution"]["blend_gate_volatility_delever_metric"], 0.61) + self.assertIs(plan["execution"]["blend_gate_volatility_delever_triggered"], True) def test_maps_hybrid_decision_from_snapshot_source(self): decision = StrategyDecision( diff --git a/tests/test_signal_snapshot.py b/tests/test_signal_snapshot.py index 691620a..4a839a8 100644 --- a/tests/test_signal_snapshot.py +++ b/tests/test_signal_snapshot.py @@ -90,6 +90,39 @@ def test_uses_price_as_of_as_snapshot_date_fallback(self): self.assertEqual(snapshot["price_as_of"], "2026-06-01") self.assertEqual(snapshot["universe_as_of"], "2026-05-14") + def test_includes_soxl_dynamic_volatility_fields(self): + snapshot = build_signal_snapshot( + platform="longbridge", + strategy_profile="soxl_soxx_trend_income", + execution={ + "blend_gate_volatility_delever_threshold_mode": "rolling_percentile", + "blend_gate_volatility_delever_threshold": 0.60, + "blend_gate_volatility_delever_dynamic_threshold": 0.60, + "blend_gate_volatility_delever_dynamic_sample_count": 252, + "blend_gate_volatility_delever_dynamic_percentile": 0.95, + "blend_gate_volatility_delever_metric": 0.61, + "blend_gate_volatility_delever_triggered": True, + }, + ) + + self.assertEqual( + snapshot["indicators"]["blend_gate_volatility_delever_threshold_mode"], + "rolling_percentile", + ) + self.assertEqual( + snapshot["indicators"]["blend_gate_volatility_delever_dynamic_threshold"], + 0.60, + ) + self.assertEqual( + snapshot["indicators"][ + "blend_gate_volatility_delever_dynamic_sample_count" + ], + 252, + ) + self.assertIs( + snapshot["indicators"]["blend_gate_volatility_delever_triggered"], True + ) + if __name__ == "__main__": unittest.main()