|
39 | 39 | PLUGIN_MACRO_RISK_GOVERNOR = MACRO_RISK_GOVERNOR_PROFILE |
40 | 40 | PLUGIN_TACO_REBOUND_SHADOW = TACO_REBOUND_PROFILE |
41 | 41 | SUPPORTED_PLUGIN_MODES = (SHADOW_MODE,) |
42 | | -PLUGIN_COMPATIBLE_STRATEGIES: dict[str, tuple[str, ...]] = { |
43 | | - PLUGIN_CRISIS_RESPONSE_SHADOW: ("tqqq_growth_income",), |
44 | | - PLUGIN_MARKET_REGIME_CONTROL: ( |
45 | | - GENERAL_MARKET_REGIME_NOTIFICATION_STRATEGY, |
46 | | - "tqqq_growth_income", |
47 | | - "global_etf_rotation", |
48 | | - "russell_1000_multi_factor_defensive", |
49 | | - "tech_communication_pullback_enhancement", |
50 | | - "mega_cap_leader_rotation_top50_balanced", |
51 | | - ), |
52 | | - PLUGIN_MACRO_RISK_GOVERNOR: ("tqqq_growth_income",), |
53 | | - PLUGIN_TACO_REBOUND_SHADOW: ("tqqq_growth_income",), |
54 | | -} |
| 42 | +EVIDENCE_AUTOMATION_APPROVED = "automation_approved" |
| 43 | +EVIDENCE_NOTIFICATION_ONLY = "notification_only" |
| 44 | +EVIDENCE_DEPRECATED_COMPATIBILITY = "deprecated_compatibility" |
55 | 45 | PLUGIN_SCHEMA_VERSIONS: dict[str, tuple[str, ...]] = { |
56 | 46 | PLUGIN_CRISIS_RESPONSE_SHADOW: ("crisis_response_shadow.v1",), |
57 | 47 | PLUGIN_MARKET_REGIME_CONTROL: ("market_regime_control.v1",), |
@@ -79,6 +69,115 @@ class PluginRunResult: |
79 | 69 | message: str = "" |
80 | 70 |
|
81 | 71 |
|
| 72 | +@dataclass(frozen=True) |
| 73 | +class PluginConsumptionPolicy: |
| 74 | + plugin: str |
| 75 | + strategy: str |
| 76 | + notification_allowed: bool |
| 77 | + position_control_allowed: bool |
| 78 | + evidence_status: str |
| 79 | + since_version: str |
| 80 | + description: str |
| 81 | + intended_strategy_role: str | None = None |
| 82 | + |
| 83 | + |
| 84 | +PLUGIN_CONSUMPTION_POLICIES: tuple[PluginConsumptionPolicy, ...] = ( |
| 85 | + PluginConsumptionPolicy( |
| 86 | + plugin=PLUGIN_MARKET_REGIME_CONTROL, |
| 87 | + strategy=GENERAL_MARKET_REGIME_NOTIFICATION_STRATEGY, |
| 88 | + notification_allowed=True, |
| 89 | + position_control_allowed=False, |
| 90 | + evidence_status=EVIDENCE_NOTIFICATION_ONLY, |
| 91 | + since_version="strategy_plugins.v1", |
| 92 | + description="General market-regime notice. Not mounted into an automated strategy runtime.", |
| 93 | + intended_strategy_role="general_market_regime_notification", |
| 94 | + ), |
| 95 | + PluginConsumptionPolicy( |
| 96 | + plugin=PLUGIN_MARKET_REGIME_CONTROL, |
| 97 | + strategy="tqqq_growth_income", |
| 98 | + notification_allowed=True, |
| 99 | + position_control_allowed=True, |
| 100 | + evidence_status=EVIDENCE_AUTOMATION_APPROVED, |
| 101 | + since_version="strategy_plugins.v1", |
| 102 | + description="Backtested automatic macro/crisis risk controls for the TQQQ growth-income strategy.", |
| 103 | + ), |
| 104 | + PluginConsumptionPolicy( |
| 105 | + plugin=PLUGIN_MARKET_REGIME_CONTROL, |
| 106 | + strategy="global_etf_rotation", |
| 107 | + notification_allowed=True, |
| 108 | + position_control_allowed=True, |
| 109 | + evidence_status=EVIDENCE_AUTOMATION_APPROVED, |
| 110 | + since_version="strategy_plugins.v1", |
| 111 | + description="Local risk-scaling consumer for broad ETF rotation.", |
| 112 | + ), |
| 113 | + PluginConsumptionPolicy( |
| 114 | + plugin=PLUGIN_MARKET_REGIME_CONTROL, |
| 115 | + strategy="russell_1000_multi_factor_defensive", |
| 116 | + notification_allowed=True, |
| 117 | + position_control_allowed=True, |
| 118 | + evidence_status=EVIDENCE_AUTOMATION_APPROVED, |
| 119 | + since_version="strategy_plugins.v1", |
| 120 | + description="Local risk-scaling consumer for the Russell 1000 defensive sleeve.", |
| 121 | + ), |
| 122 | + PluginConsumptionPolicy( |
| 123 | + plugin=PLUGIN_MARKET_REGIME_CONTROL, |
| 124 | + strategy="tech_communication_pullback_enhancement", |
| 125 | + notification_allowed=True, |
| 126 | + position_control_allowed=True, |
| 127 | + evidence_status=EVIDENCE_AUTOMATION_APPROVED, |
| 128 | + since_version="strategy_plugins.v1", |
| 129 | + description="Local risk-scaling consumer for the tech/communication pullback profile.", |
| 130 | + ), |
| 131 | + PluginConsumptionPolicy( |
| 132 | + plugin=PLUGIN_MARKET_REGIME_CONTROL, |
| 133 | + strategy="mega_cap_leader_rotation_top50_balanced", |
| 134 | + notification_allowed=True, |
| 135 | + position_control_allowed=True, |
| 136 | + evidence_status=EVIDENCE_AUTOMATION_APPROVED, |
| 137 | + since_version="strategy_plugins.v1", |
| 138 | + description="Local risk-scaling consumer for the mega-cap leader rotation profile.", |
| 139 | + ), |
| 140 | + PluginConsumptionPolicy( |
| 141 | + plugin=PLUGIN_CRISIS_RESPONSE_SHADOW, |
| 142 | + strategy="tqqq_growth_income", |
| 143 | + notification_allowed=True, |
| 144 | + position_control_allowed=False, |
| 145 | + evidence_status=EVIDENCE_DEPRECATED_COMPATIBILITY, |
| 146 | + since_version="strategy_plugins.v1", |
| 147 | + description="Deprecated direct crisis shadow mount kept for historical replay; new consumers use market_regime_control.", |
| 148 | + ), |
| 149 | + PluginConsumptionPolicy( |
| 150 | + plugin=PLUGIN_MACRO_RISK_GOVERNOR, |
| 151 | + strategy="tqqq_growth_income", |
| 152 | + notification_allowed=True, |
| 153 | + position_control_allowed=False, |
| 154 | + evidence_status=EVIDENCE_DEPRECATED_COMPATIBILITY, |
| 155 | + since_version="strategy_plugins.v1", |
| 156 | + description="Deprecated direct macro governor mount kept for historical replay; new consumers use market_regime_control.", |
| 157 | + ), |
| 158 | + PluginConsumptionPolicy( |
| 159 | + plugin=PLUGIN_TACO_REBOUND_SHADOW, |
| 160 | + strategy="tqqq_growth_income", |
| 161 | + notification_allowed=True, |
| 162 | + position_control_allowed=False, |
| 163 | + evidence_status=EVIDENCE_NOTIFICATION_ONLY, |
| 164 | + since_version="strategy_plugins.v1", |
| 165 | + description="Manual-review event rebound notifier for TQQQ only.", |
| 166 | + ), |
| 167 | +) |
| 168 | +PLUGIN_CONSUMPTION_POLICY_REGISTRY: dict[tuple[str, str], PluginConsumptionPolicy] = { |
| 169 | + (policy.plugin, policy.strategy): policy for policy in PLUGIN_CONSUMPTION_POLICIES |
| 170 | +} |
| 171 | +PLUGIN_COMPATIBLE_STRATEGIES: dict[str, tuple[str, ...]] = { |
| 172 | + plugin: tuple( |
| 173 | + policy.strategy |
| 174 | + for policy in PLUGIN_CONSUMPTION_POLICIES |
| 175 | + if policy.plugin == plugin and policy.notification_allowed |
| 176 | + ) |
| 177 | + for plugin in sorted({policy.plugin for policy in PLUGIN_CONSUMPTION_POLICIES}) |
| 178 | +} |
| 179 | + |
| 180 | + |
82 | 181 | PluginRunner = Callable[[Mapping[str, Any], str], PluginRunResult] |
83 | 182 | PluginPayloadBuilder = Callable[[pd.DataFrame, Mapping[str, Any]], dict[str, Any]] |
84 | 183 | PluginOutputWriter = Callable[[Mapping[str, Any], str | Path], Mapping[str, Path]] |
@@ -159,14 +258,19 @@ def _validate_plugin_strategy(plugin_name: str, strategy: str) -> None: |
159 | 258 | raise ValueError( |
160 | 259 | f"{plugin_name} is research-only and cannot be mounted to {strategy!r}: {research_only_reason}" |
161 | 260 | ) |
162 | | - compatible = PLUGIN_COMPATIBLE_STRATEGIES.get(plugin_name, ()) |
163 | | - if compatible and strategy not in compatible: |
164 | | - choices = ", ".join(compatible) |
| 261 | + policy = PLUGIN_CONSUMPTION_POLICY_REGISTRY.get((plugin_name, strategy)) |
| 262 | + if policy is None or not policy.notification_allowed: |
| 263 | + compatible = PLUGIN_COMPATIBLE_STRATEGIES.get(plugin_name, ()) |
| 264 | + choices = ", ".join(compatible) if compatible else "(none)" |
165 | 265 | raise ValueError( |
166 | 266 | f"{plugin_name} is strategy-limited and can only be mounted to: {choices}; got strategy={strategy!r}" |
167 | 267 | ) |
168 | 268 |
|
169 | 269 |
|
| 270 | +def _plugin_consumption_policy(plugin_name: str, strategy: str) -> PluginConsumptionPolicy | None: |
| 271 | + return PLUGIN_CONSUMPTION_POLICY_REGISTRY.get((plugin_name, strategy)) |
| 272 | + |
| 273 | + |
170 | 274 | def _flatten_strategy_plugin_entry(entry: Mapping[str, Any]) -> dict[str, Any]: |
171 | 275 | plugin_config = { |
172 | 276 | key: value |
@@ -333,6 +437,9 @@ def _build_macro_risk_governor_kwargs(plugin_config: Mapping[str, Any]) -> dict[ |
333 | 437 | "hy_oas_delta_threshold", |
334 | 438 | "financial_stress_watch_level", |
335 | 439 | "pizza_index_watch_level", |
| 440 | + "fear_greed_extreme_fear_level", |
| 441 | + "put_call_watch_level", |
| 442 | + "safe_haven_demand_watch_level", |
336 | 443 | "watch_score_threshold", |
337 | 444 | "delever_score_threshold", |
338 | 445 | "crisis_score_threshold", |
@@ -389,21 +496,34 @@ def _mode_execution_controls(mode: str) -> dict[str, Any]: |
389 | 496 | raise ValueError(f"unsupported plugin mode: {mode!r}") from exc |
390 | 497 |
|
391 | 498 |
|
392 | | -def _apply_plugin_contract(payload: Mapping[str, Any], *, strategy: str, plugin: str, mode: str) -> dict[str, Any]: |
| 499 | +def _apply_plugin_contract( |
| 500 | + payload: Mapping[str, Any], |
| 501 | + *, |
| 502 | + strategy: str, |
| 503 | + plugin: str, |
| 504 | + mode: str, |
| 505 | + consumption_policy: PluginConsumptionPolicy | None = None, |
| 506 | +) -> dict[str, Any]: |
393 | 507 | contracted_payload = dict(payload) |
394 | 508 | contracted_payload["strategy"] = strategy |
395 | 509 | contracted_payload["plugin"] = plugin |
396 | 510 | contracted_payload["mode"] = mode |
397 | 511 | contracted_payload["configured_mode"] = mode |
398 | 512 | contracted_payload["effective_mode"] = mode |
| 513 | + if consumption_policy is not None: |
| 514 | + contracted_payload["consumption_policy"] = asdict(consumption_policy) |
399 | 515 |
|
400 | 516 | execution_controls = dict(contracted_payload.get("execution_controls") or {}) |
401 | 517 | execution_controls.update(_mode_execution_controls(mode)) |
402 | 518 | execution_controls["configured_mode"] = mode |
403 | 519 | execution_controls["effective_mode"] = mode |
404 | 520 | execution_controls["repository_broker_write_allowed"] = False |
405 | 521 | execution_controls["repository_allocation_mutation_allowed"] = False |
406 | | - if strategy == GENERAL_MARKET_REGIME_NOTIFICATION_STRATEGY: |
| 522 | + if consumption_policy is not None: |
| 523 | + execution_controls["notification_allowed"] = bool(consumption_policy.notification_allowed) |
| 524 | + execution_controls["position_control_allowed"] = bool(consumption_policy.position_control_allowed) |
| 525 | + execution_controls["consumption_evidence_status"] = consumption_policy.evidence_status |
| 526 | + if consumption_policy is not None and consumption_policy.intended_strategy_role == "general_market_regime_notification": |
407 | 527 | execution_controls["capital_impact"] = "notification_only" |
408 | 528 | execution_controls["strategy_runtime_metadata_allowed"] = False |
409 | 529 | execution_controls["position_control_shadow_only"] = True |
@@ -482,12 +602,20 @@ def _run_table_strategy_plugin( |
482 | 602 | message="plugin disabled", |
483 | 603 | ) |
484 | 604 | _validate_plugin_mode(plugin, mode) |
| 605 | + _validate_plugin_strategy(plugin, strategy) |
| 606 | + consumption_policy = _plugin_consumption_policy(plugin, strategy) |
485 | 607 |
|
486 | 608 | prices_path = str(plugin_config.get("prices", "")).strip() |
487 | 609 | if not prices_path: |
488 | 610 | raise ValueError(f"{plugin} for strategy={strategy} requires a prices path") |
489 | 611 | payload = spec.build_payload(read_table(prices_path), plugin_config) |
490 | | - payload = _apply_plugin_contract(payload, strategy=strategy, plugin=plugin, mode=mode) |
| 612 | + payload = _apply_plugin_contract( |
| 613 | + payload, |
| 614 | + strategy=strategy, |
| 615 | + plugin=plugin, |
| 616 | + mode=mode, |
| 617 | + consumption_policy=consumption_policy, |
| 618 | + ) |
491 | 619 | paths = spec.write_outputs(payload, output_dir) |
492 | 620 | return PluginRunResult( |
493 | 621 | strategy=strategy, |
@@ -640,9 +768,12 @@ def main(argv: list[str] | None = None) -> int: |
640 | 768 | "PLUGIN_MACRO_RISK_GOVERNOR", |
641 | 769 | "PLUGIN_TACO_REBOUND_SHADOW", |
642 | 770 | "PLUGIN_COMPATIBLE_STRATEGIES", |
| 771 | + "PLUGIN_CONSUMPTION_POLICIES", |
| 772 | + "PLUGIN_CONSUMPTION_POLICY_REGISTRY", |
643 | 773 | "PLUGIN_DEPRECATED_SUCCESSORS", |
644 | 774 | "PLUGIN_RESEARCH_ONLY_REASONS", |
645 | 775 | "PLUGIN_SCHEMA_VERSIONS", |
| 776 | + "PluginConsumptionPolicy", |
646 | 777 | "PluginRunResult", |
647 | 778 | "load_plugin_config", |
648 | 779 | "main", |
|
0 commit comments