Skip to content

Commit ed07ba9

Browse files
committed
Tune macro volatility confirmation defaults
1 parent 965a01f commit ed07ba9

4 files changed

Lines changed: 90 additions & 7 deletions

File tree

src/quant_strategy_plugins/macro_risk_governor_plugin.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ def build_macro_risk_governor_signal(
245245
benchmark_drawdown_watch: float = -0.10,
246246
benchmark_drawdown_crisis: float = -0.18,
247247
realized_vol_window: int = 10,
248-
realized_vol_threshold: float = 0.35,
248+
realized_vol_threshold: float = 0.30,
249+
realized_vol_requires_confirmation: bool = True,
249250
vix_watch_level: float = 28.0,
250251
vix_crisis_level: float = 35.0,
251252
vix_spike_lookback_days: int = 5,
@@ -261,7 +262,7 @@ def build_macro_risk_governor_signal(
261262
delever_score_threshold: float = 5.0,
262263
crisis_score_threshold: float = 7.0,
263264
delever_leverage_scalar: float = 0.0,
264-
delever_risk_asset_scalar: float = 1.0,
265+
delever_risk_asset_scalar: float = 0.0,
265266
crisis_leverage_scalar: float = 0.0,
266267
crisis_risk_asset_scalar: float = 0.0,
267268
) -> dict[str, Any]:
@@ -447,12 +448,36 @@ def build_macro_risk_governor_signal(
447448
threshold=float(pizza_index_watch_level),
448449
actionable=False,
449450
)
451+
realized_vol_confirmed_for_action = None
452+
realized_vol_check = checks.get("benchmark_realized_volatility_high")
453+
if realized_vol_check is not None:
454+
volatility_active = bool(realized_vol_check.get("active", False))
455+
if volatility_active:
456+
confirmation_checks = (
457+
"vix_watch_level",
458+
"vix_crisis_level",
459+
"vix_spike",
460+
"credit_pair_stress",
461+
"hy_oas_watch_level",
462+
"hy_oas_widening",
463+
"financial_stress_index_high",
464+
)
465+
realized_vol_confirmed_for_action = any(
466+
bool(checks.get(name, {}).get("active", False)) for name in confirmation_checks
467+
)
468+
if bool(realized_vol_requires_confirmation) and not realized_vol_confirmed_for_action:
469+
realized_vol_check["actionable"] = False
470+
realized_vol_check["suppression_reason"] = "missing_volatility_stress_confirmation"
471+
realized_vol_check["confirmation_required"] = bool(realized_vol_requires_confirmation)
472+
realized_vol_check["confirmed_for_action"] = realized_vol_confirmed_for_action
450473
evidence["metrics"].update(
451474
{
452475
"hy_oas": hy_oas,
453476
"hy_oas_delta_63d": hy_oas_delta,
454477
"financial_stress": financial_stress,
455478
"pentagon_pizza_index": pizza_index,
479+
"benchmark_realized_volatility_requires_confirmation": bool(realized_vol_requires_confirmation),
480+
"benchmark_realized_volatility_confirmed_for_action": realized_vol_confirmed_for_action,
456481
}
457482
)
458483

@@ -626,11 +651,18 @@ def build_parser() -> argparse.ArgumentParser:
626651
)
627652
parser.add_argument("--max-price-age-days", type=int, default=DEFAULT_MAX_PRICE_AGE_DAYS)
628653
parser.add_argument("--max-external-context-age-days", type=int, default=DEFAULT_MAX_EXTERNAL_CONTEXT_AGE_DAYS)
654+
parser.add_argument("--realized-vol-threshold", type=float, default=0.30)
655+
parser.add_argument(
656+
"--realized-vol-requires-confirmation",
657+
action=argparse.BooleanOptionalAction,
658+
default=True,
659+
help="Require VIX, credit, or financial-stress confirmation before realized volatility contributes to actionable score.",
660+
)
629661
parser.add_argument("--watch-score-threshold", type=float, default=3.0)
630662
parser.add_argument("--delever-score-threshold", type=float, default=5.0)
631663
parser.add_argument("--crisis-score-threshold", type=float, default=7.0)
632664
parser.add_argument("--delever-leverage-scalar", type=float, default=0.0)
633-
parser.add_argument("--delever-risk-asset-scalar", type=float, default=1.0)
665+
parser.add_argument("--delever-risk-asset-scalar", type=float, default=0.0)
634666
parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR)
635667
return parser
636668

@@ -663,6 +695,8 @@ def main(argv: list[str] | None = None) -> int:
663695
credit_pairs=credit_pairs,
664696
max_price_age_days=args.max_price_age_days,
665697
max_external_context_age_days=args.max_external_context_age_days,
698+
realized_vol_threshold=args.realized_vol_threshold,
699+
realized_vol_requires_confirmation=args.realized_vol_requires_confirmation,
666700
watch_score_threshold=args.watch_score_threshold,
667701
delever_score_threshold=args.delever_score_threshold,
668702
crisis_score_threshold=args.crisis_score_threshold,

src/quant_strategy_plugins/strategy_plugin_runner.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@ def _build_macro_risk_governor_kwargs(plugin_config: Mapping[str, Any]) -> dict[
331331
"credit_relative_lookback_days",
332332
"hy_oas_delta_lookback_days",
333333
}
334+
bool_keys = {
335+
"realized_vol_requires_confirmation",
336+
}
334337
for key in string_keys:
335338
if key in plugin_config and plugin_config[key] is not None:
336339
kwargs[key] = str(plugin_config[key]).strip()
@@ -340,6 +343,9 @@ def _build_macro_risk_governor_kwargs(plugin_config: Mapping[str, Any]) -> dict[
340343
for key in integer_keys:
341344
if key in plugin_config and plugin_config[key] is not None:
342345
kwargs[key] = int(plugin_config[key])
346+
for key in bool_keys:
347+
if key in plugin_config and plugin_config[key] is not None:
348+
kwargs[key] = _as_bool(plugin_config[key])
343349
if "vix_symbols" in plugin_config:
344350
kwargs["vix_symbols"] = _as_str_tuple(plugin_config["vix_symbols"])
345351
if "credit_pairs" in plugin_config:

tests/test_macro_risk_governor_plugin.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
)
1515

1616

17-
def _macro_prices(*, stress: bool = False) -> pd.DataFrame:
17+
def _macro_prices(*, stress: bool = False, volatility_spike: bool = False, vix_level: float = 15.0) -> pd.DataFrame:
1818
dates = pd.bdate_range("2025-01-02", periods=260)
1919
rows: list[dict[str, object]] = []
2020
qqq = pd.Series([100.0 + idx * 0.10 for idx in range(len(dates))], index=dates)
21-
vix = pd.Series(15.0, index=dates)
21+
vix = pd.Series(float(vix_level), index=dates)
2222
hyg = pd.Series(100.0, index=dates)
2323
ief = pd.Series(100.0, index=dates)
2424
if stress:
@@ -35,6 +35,11 @@ def _macro_prices(*, stress: bool = False) -> pd.DataFrame:
3535
[100.0 + idx * (3.0 / 21.0) for idx in range(22)],
3636
index=dates[-22:],
3737
)
38+
if volatility_spike:
39+
qqq.iloc[-10:] = pd.Series(
40+
[130.0, 122.0, 131.0, 121.0, 132.0, 122.0, 133.0, 123.0, 134.0, 124.0],
41+
index=dates[-10:],
42+
)
3843
prices = {
3944
"QQQ": qqq,
4045
"TQQQ": qqq * 3.0,
@@ -69,7 +74,7 @@ def test_macro_risk_governor_routes_stress_to_delever_when_below_crisis_threshol
6974
assert payload["suggested_action"] == "delever"
7075
assert payload["would_trade_if_enabled"] is True
7176
assert payload["leverage_scalar"] == 0.0
72-
assert payload["risk_asset_scalar"] == 1.0
77+
assert payload["risk_asset_scalar"] == 0.0
7378
assert "vix_crisis_level" in payload["reason_codes"]
7479
assert payload["checks"]["pentagon_pizza_watch"]["actionable"] is False
7580

@@ -109,6 +114,44 @@ def test_macro_risk_governor_keeps_pizza_index_watch_only() -> None:
109114
assert payload["checks"]["pentagon_pizza_watch"]["actionable"] is False
110115

111116

117+
def test_macro_risk_governor_requires_confirmation_for_realized_volatility_action() -> None:
118+
payload = build_macro_risk_governor_signal(
119+
_macro_prices(volatility_spike=True),
120+
as_of="2025-12-31",
121+
watch_score_threshold=1.0,
122+
delever_score_threshold=1.0,
123+
crisis_score_threshold=99.0,
124+
)
125+
126+
volatility_check = payload["checks"]["benchmark_realized_volatility_high"]
127+
assert volatility_check["active"] is True
128+
assert volatility_check["actionable"] is False
129+
assert volatility_check["confirmed_for_action"] is False
130+
assert volatility_check["suppression_reason"] == "missing_volatility_stress_confirmation"
131+
assert payload["actionable_score"] == 0.0
132+
assert payload["total_score"] == 1.0
133+
assert payload["canonical_route"] == ROUTE_WATCH
134+
assert payload["suggested_action"] == "watch_only"
135+
136+
137+
def test_macro_risk_governor_allows_realized_volatility_action_when_vix_confirms() -> None:
138+
payload = build_macro_risk_governor_signal(
139+
_macro_prices(volatility_spike=True, vix_level=30.0),
140+
as_of="2025-12-31",
141+
watch_score_threshold=1.0,
142+
delever_score_threshold=1.0,
143+
crisis_score_threshold=99.0,
144+
)
145+
146+
volatility_check = payload["checks"]["benchmark_realized_volatility_high"]
147+
assert volatility_check["active"] is True
148+
assert volatility_check["actionable"] is True
149+
assert volatility_check["confirmed_for_action"] is True
150+
assert payload["actionable_score"] >= 1.0
151+
assert payload["canonical_route"] == ROUTE_DELEVER
152+
assert payload["suggested_action"] == "delever"
153+
154+
112155
def test_macro_risk_governor_writes_json_csv_and_evidence(tmp_path) -> None:
113156
payload = build_macro_risk_governor_signal(_macro_prices(stress=True), as_of="2025-12-31")
114157

tests/test_strategy_plugin_runner.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def test_strategy_plugin_runner_runs_unified_market_regime_control_for_tqqq(tmp_
256256
assert payload["plugin"] == PLUGIN_MARKET_REGIME_CONTROL
257257
assert payload["canonical_route"] == "risk_reduced"
258258
assert payload["position_control"]["leverage_scalar"] == 0.0
259-
assert payload["position_control"]["risk_asset_scalar"] == 1.0
259+
assert payload["position_control"]["risk_asset_scalar"] == 0.0
260260
assert payload["position_control"]["taco_allowed"] is False
261261
assert payload["execution_controls"]["strategy_runtime_metadata_allowed"] is True
262262
assert payload["execution_controls"]["broker_order_allowed"] is False

0 commit comments

Comments
 (0)