diff --git a/README.md b/README.md index 1824068..e04cf3a 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ python -m pytest -q ``` ## Useful docs - +- [`docs/plugin_lifecycle_policy.md`](docs/plugin_lifecycle_policy.md) - [`docs/market-regime-control-plan.md`](docs/market-regime-control-plan.md) - [`docs/market-regime-control-plan.zh-CN.md`](docs/market-regime-control-plan.zh-CN.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index cb4663e..34e169b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -17,6 +17,10 @@ QuantStrategyPlugins 是 QuantStrategyLab 的策略插件包。提供 market-reg - 密钥和环境专属配置不要写进共享库代码。 - 会影响多个平台或策略包的改动,需要在文档中说明。 +## 延伸文档 + +- [`docs/plugin_lifecycle_policy.zh-CN.md`](docs/plugin_lifecycle_policy.zh-CN.md) + ## 仓库结构 - `src/`:库代码和运行时代码。 @@ -34,6 +38,7 @@ python -m pytest -q ## 延伸文档 +- [`docs/plugin_lifecycle_policy.zh-CN.md`](docs/plugin_lifecycle_policy.zh-CN.md) - [`docs/market-regime-control-plan.md`](docs/market-regime-control-plan.md) - [`docs/market-regime-control-plan.zh-CN.md`](docs/market-regime-control-plan.zh-CN.md) diff --git a/docs/market-regime-control-plan.md b/docs/market-regime-control-plan.md index eeb6042..7b298eb 100644 --- a/docs/market-regime-control-plan.md +++ b/docs/market-regime-control-plan.md @@ -5,6 +5,9 @@ This document records the current design boundary, arbitration order, strategy consumption policy, and backtest evidence for `market_regime_control`. +See also: [`plugin_lifecycle_policy.md`](./plugin_lifecycle_policy.md) for the +shared plugin stage ladder and gate model. + ## Goals `market_regime_control` is a unified deterministic market-regime facade. It diff --git a/docs/plugin_lifecycle_policy.md b/docs/plugin_lifecycle_policy.md new file mode 100644 index 0000000..eeec1c2 --- /dev/null +++ b/docs/plugin_lifecycle_policy.md @@ -0,0 +1,72 @@ +# Plugin Lifecycle Policy + +[简体中文](./plugin_lifecycle_policy.zh-CN.md) + +This document defines the lifecycle ladder and gate model for strategy plugins. + +## Design Goal + +Plugins should be easy to observe, hard to misuse, and very hard to let affect +capital without explicit approval. + +- AI may monitor and summarize plugin evidence. +- AI must not upgrade a plugin into live authority by itself. +- Notification delivery is not the same as position-control authority. +- Compatibility mounts can exist for history, but they should not expand live +permission by accident. + +## Recommended Plugin Stages + +| Stage | Meaning | Capital impact | +| --- | --- | --- | +| `research_only` | Research artifact, not meant for platform runtime | none | +| `notification_only` | Can notify humans, but cannot control position | none | +| `shadow_observer` | Can be attached to runtime metadata and audit trails | none | +| `automation_candidate` | Has enough evidence to be considered for automation | gated | +| `automation_approved` | Strategy-side automation is allowed when the platform gate also passes | yes | +| `deprecated_compatibility` | Kept for replay or staged migration only | none | + +## Three-Gate Rule + +For any plugin-driven capital impact, all three gates must pass: + +1. **Plugin schema gate** + - The artifact must match a supported schema version and remain in `shadow` + mode for the shared contract. +2. **Plugin evidence gate** + - The plugin must be marked `automation_approved` and + `position_control_allowed = true`. +3. **Strategy/platform gate** + - The consuming strategy must explicitly opt in to the plugin and remain + allowed by the platform catalog. + +If any gate fails, the plugin should stay notification-only or compatibility-only. + +## Current Policy Shape + +- `src/quant_strategy_plugins/plugin_policies.py` is the machine-readable source + for lifecycle, consumption, notification-target, schema-version, and + deprecated-successor policy. +- `market_regime_control` is the unified default runtime plugin. +- `crisis_response_shadow`, `macro_risk_governor`, and `taco_rebound_shadow` + remain compatibility mounts or notification-only sidecars. +- `panic_reversal_shadow` remains research-heavy and should stay notification-only + unless it is separately promoted. + +## Recommended Operating Rules + +- Keep `notification_allowed` broad for research visibility. +- Keep `position_control_allowed` narrow and explicit. +- Prefer a single shared policy registry instead of spreading allowlists across + runners. +- When a strategy consumes a plugin for live capital impact, keep the platform + notification path separate from the strategy execution path. + +## Practical Interpretation + +- If the artifact is only for human review, use `notification_only`. +- If the artifact is still a sidecar evidence layer, use `shadow_observer`. +- If the plugin is being prepared for automation, keep it in + `automation_candidate` until the strategy gate also passes. +- If the plugin is no longer the preferred path, mark it + `deprecated_compatibility` and keep it out of new runtime defaults. diff --git a/docs/plugin_lifecycle_policy.zh-CN.md b/docs/plugin_lifecycle_policy.zh-CN.md new file mode 100644 index 0000000..58f9ef2 --- /dev/null +++ b/docs/plugin_lifecycle_policy.zh-CN.md @@ -0,0 +1,62 @@ +# 插件生命周期策略 + +[English](./plugin_lifecycle_policy.md) + +本文定义策略插件的生命周期阶梯和门槛模型。 + +## 设计目标 + +插件应该容易观察、难以误用,而且在没有显式批准前非常难影响资金。 + +- AI 可以监控和总结插件证据。 +- AI 不能单独把插件升级成 live 权限。 +- 通知投递不等于仓位控制权限。 +- 兼容挂载可以保留用于历史回放,但不能意外扩大 live 权限。 + +## 推荐插件阶段 + +| 阶段 | 含义 | 资金影响 | +| --- | --- | --- | +| `research_only` | 研究 artifact,不打算进入平台 runtime | 无 | +| `notification_only` | 可以通知人工,但不能控制仓位 | 无 | +| `shadow_observer` | 可以挂到 runtime metadata 和审计轨迹 | 无 | +| `automation_candidate` | 证据足够,进入自动化候选 | 受门槛控制 | +| `automation_approved` | 当平台门槛也通过时,策略侧可自动消费 | 有 | +| `deprecated_compatibility` | 仅用于回放或迁移兼容 | 无 | + +## 三道门槛 + +任何由插件驱动的资金影响,都必须同时通过三道门槛: + +1. **插件 schema 门槛** + - artifact 必须匹配支持的 schema version,并在共享契约中保持 `shadow` 模式。 +2. **插件证据门槛** + - 插件必须标记为 `automation_approved`,且 `position_control_allowed = true`。 +3. **策略 / 平台门槛** + - 消费策略必须显式 opt-in,并且仍然被平台 catalog 允许。 + +任意一项失败,插件就应该停留在 notification-only 或兼容模式。 + +## 当前策略形态 + +- `src/quant_strategy_plugins/plugin_policies.py` 是 lifecycle、消费权限、 + notification target、schema version 和 deprecated successor 的机器可读来源。 +- `market_regime_control` 是统一默认 runtime 插件。 +- `crisis_response_shadow`、`macro_risk_governor` 和 `taco_rebound_shadow` + 仍然是兼容挂载或 notification-only sidecar。 +- `panic_reversal_shadow` 仍然偏研究,应继续保持 notification-only, + 除非后续单独晋级。 + +## 推荐运行规则 + +- `notification_allowed` 可以保持宽松,方便研究可见。 +- `position_control_allowed` 必须保持窄而明确。 +- 尽量使用统一的 policy registry,不要把 allowlist 分散到多个 runner。 +- 当策略消费插件并产生 live 资金影响时,平台通知路径应与策略执行路径分离。 + +## 实际解释 + +- 只给人工复核看的 artifact,用 `notification_only`。 +- 仍然只是 sidecar 证据层的 artifact,用 `shadow_observer`。 +- 正在准备自动化的插件,用 `automation_candidate`,直到策略门槛也通过。 +- 不再是新默认路径的插件,用 `deprecated_compatibility`,并避免出现在新的 runtime 默认值里。 diff --git a/src/quant_strategy_plugins/plugin_policies.py b/src/quant_strategy_plugins/plugin_policies.py new file mode 100644 index 0000000..2ae5434 --- /dev/null +++ b/src/quant_strategy_plugins/plugin_policies.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +from dataclasses import dataclass + +EVIDENCE_AUTOMATION_APPROVED = "automation_approved" +EVIDENCE_NOTIFICATION_ONLY = "notification_only" +EVIDENCE_DEPRECATED_COMPATIBILITY = "deprecated_compatibility" +PLUGIN_LIFECYCLE_AUTOMATION_APPROVED = "automation_approved" +PLUGIN_LIFECYCLE_NOTIFICATION_ONLY = "notification_only" +PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY = "deprecated_compatibility" + +GENERAL_MARKET_REGIME_NOTIFICATION_TARGET = "market_regime_notification" + +PLUGIN_CRISIS_RESPONSE_SHADOW = "crisis_response_shadow" +PLUGIN_MARKET_REGIME_CONTROL = "market_regime_control" +PLUGIN_MACRO_RISK_GOVERNOR = "macro_risk_governor" +PLUGIN_PANIC_REVERSAL_SHADOW = "panic_reversal_shadow" +PLUGIN_TACO_REBOUND_SHADOW = "taco_rebound_shadow" + +PLUGIN_SCHEMA_VERSIONS: dict[str, tuple[str, ...]] = { + PLUGIN_CRISIS_RESPONSE_SHADOW: ("crisis_response_shadow.v1",), + PLUGIN_MARKET_REGIME_CONTROL: ("market_regime_control.v1",), + PLUGIN_MACRO_RISK_GOVERNOR: ("macro_risk_governor.v1",), + PLUGIN_PANIC_REVERSAL_SHADOW: ("panic_reversal_shadow.v1",), + PLUGIN_TACO_REBOUND_SHADOW: ("taco_rebound_shadow.v2",), +} + +PLUGIN_DEPRECATED_SUCCESSORS: dict[str, str] = { + PLUGIN_CRISIS_RESPONSE_SHADOW: PLUGIN_MARKET_REGIME_CONTROL, + PLUGIN_MACRO_RISK_GOVERNOR: PLUGIN_MARKET_REGIME_CONTROL, + PLUGIN_TACO_REBOUND_SHADOW: PLUGIN_MARKET_REGIME_CONTROL, +} +PLUGIN_RESEARCH_ONLY_REASONS: dict[str, str] = {} + + +@dataclass(frozen=True) +class PluginConsumptionPolicy: + plugin: str + strategy: str + notification_allowed: bool + position_control_allowed: bool + evidence_status: str + since_version: str + description: str + intended_strategy_role: str | None = None + manual_review_notification_target: str | None = None + + +@dataclass(frozen=True) +class PluginNotificationTargetPolicy: + plugin: str + notification_target: str + notification_allowed: bool + position_control_allowed: bool + evidence_status: str + since_version: str + description: str + notification_role: str + + +@dataclass(frozen=True) +class PluginLifecyclePolicy: + plugin: str + lifecycle_stage: str + schema_versions: tuple[str, ...] + new_mount_allowed: bool + replay_only: bool + description: str + successor: str | None = None + + +PLUGIN_LIFECYCLE_POLICIES: tuple[PluginLifecyclePolicy, ...] = ( + PluginLifecyclePolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + lifecycle_stage=PLUGIN_LIFECYCLE_AUTOMATION_APPROVED, + schema_versions=PLUGIN_SCHEMA_VERSIONS[PLUGIN_MARKET_REGIME_CONTROL], + new_mount_allowed=True, + replay_only=False, + description="Unified market-regime sidecar; automated consumption still requires per-strategy policy approval.", + ), + PluginLifecyclePolicy( + plugin=PLUGIN_PANIC_REVERSAL_SHADOW, + lifecycle_stage=PLUGIN_LIFECYCLE_NOTIFICATION_ONLY, + schema_versions=PLUGIN_SCHEMA_VERSIONS[PLUGIN_PANIC_REVERSAL_SHADOW], + new_mount_allowed=True, + replay_only=False, + description="Research notification sidecar; never grants automated position control.", + ), + PluginLifecyclePolicy( + plugin=PLUGIN_CRISIS_RESPONSE_SHADOW, + lifecycle_stage=PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY, + schema_versions=PLUGIN_SCHEMA_VERSIONS[PLUGIN_CRISIS_RESPONSE_SHADOW], + new_mount_allowed=False, + replay_only=True, + successor=PLUGIN_MARKET_REGIME_CONTROL, + description="Legacy crisis sidecar retained for historical replay; new mounts use market_regime_control.", + ), + PluginLifecyclePolicy( + plugin=PLUGIN_MACRO_RISK_GOVERNOR, + lifecycle_stage=PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY, + schema_versions=PLUGIN_SCHEMA_VERSIONS[PLUGIN_MACRO_RISK_GOVERNOR], + new_mount_allowed=False, + replay_only=True, + successor=PLUGIN_MARKET_REGIME_CONTROL, + description="Legacy macro sidecar retained for historical replay; new mounts use market_regime_control.", + ), + PluginLifecyclePolicy( + plugin=PLUGIN_TACO_REBOUND_SHADOW, + lifecycle_stage=PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY, + schema_versions=PLUGIN_SCHEMA_VERSIONS[PLUGIN_TACO_REBOUND_SHADOW], + new_mount_allowed=False, + replay_only=True, + successor=PLUGIN_MARKET_REGIME_CONTROL, + description="Legacy event rebound notifier retained for replay; new mounts use market_regime_control/manual review.", + ), +) + +PLUGIN_LIFECYCLE_POLICY_REGISTRY: dict[str, PluginLifecyclePolicy] = { + policy.plugin: policy for policy in PLUGIN_LIFECYCLE_POLICIES +} + + +PLUGIN_CONSUMPTION_POLICIES: tuple[PluginConsumptionPolicy, ...] = ( + PluginConsumptionPolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + strategy="tqqq_growth_income", + notification_allowed=True, + position_control_allowed=True, + evidence_status=EVIDENCE_AUTOMATION_APPROVED, + since_version="strategy_plugins.v1", + description="Backtested automatic macro/crisis risk controls for the TQQQ growth-income strategy.", + manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + ), + PluginConsumptionPolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + strategy="global_etf_rotation", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="Pending 25-30 year market-regime-control validation for broad ETF rotation.", + manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + ), + PluginConsumptionPolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + strategy="russell_1000_multi_factor_defensive", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="Pending 25-30 year market-regime-control validation for the Russell 1000 defensive sleeve.", + manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + ), + PluginConsumptionPolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + strategy="mega_cap_leader_rotation_top50_balanced", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="Pending 25-30 year market-regime-control validation for the mega-cap leader rotation profile.", + manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + ), + PluginConsumptionPolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + strategy="soxl_soxx_trend_income", + notification_allowed=True, + position_control_allowed=True, + evidence_status=EVIDENCE_AUTOMATION_APPROVED, + since_version="strategy_plugins.v1", + description="Backtested automatic macro/crisis risk controls for the SOXL/SOXX trend-income strategy.", + manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + ), + PluginConsumptionPolicy( + plugin=PLUGIN_CRISIS_RESPONSE_SHADOW, + strategy="tqqq_growth_income", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_DEPRECATED_COMPATIBILITY, + since_version="strategy_plugins.v1", + description="Deprecated direct crisis shadow mount kept for historical replay; new consumers use market_regime_control.", + ), + PluginConsumptionPolicy( + plugin=PLUGIN_MACRO_RISK_GOVERNOR, + strategy="tqqq_growth_income", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_DEPRECATED_COMPATIBILITY, + since_version="strategy_plugins.v1", + description="Deprecated direct macro governor mount kept for historical replay; new consumers use market_regime_control.", + ), + PluginConsumptionPolicy( + plugin=PLUGIN_TACO_REBOUND_SHADOW, + strategy="tqqq_growth_income", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="Manual-review event rebound notifier for TQQQ only.", + ), + PluginConsumptionPolicy( + plugin=PLUGIN_PANIC_REVERSAL_SHADOW, + strategy="tqqq_growth_income", + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="Research-only VIX panic reversal notifier for TQQQ manual review.", + ), +) + +PLUGIN_CONSUMPTION_POLICY_REGISTRY: dict[tuple[str, str], PluginConsumptionPolicy] = { + (policy.plugin, policy.strategy): policy for policy in PLUGIN_CONSUMPTION_POLICIES +} + +PLUGIN_NOTIFICATION_TARGET_POLICIES: tuple[PluginNotificationTargetPolicy, ...] = ( + PluginNotificationTargetPolicy( + plugin=PLUGIN_MARKET_REGIME_CONTROL, + notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="General market-regime notice. Not mounted into an automated strategy runtime.", + notification_role="general_market_regime_notification", + ), + PluginNotificationTargetPolicy( + plugin=PLUGIN_PANIC_REVERSAL_SHADOW, + notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + notification_allowed=True, + position_control_allowed=False, + evidence_status=EVIDENCE_NOTIFICATION_ONLY, + since_version="strategy_plugins.v1", + description="General research-only panic reversal notice. Not mounted into an automated strategy runtime.", + notification_role="panic_reversal_notification", + ), +) + +PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY: dict[tuple[str, str], PluginNotificationTargetPolicy] = { + (policy.plugin, policy.notification_target): policy for policy in PLUGIN_NOTIFICATION_TARGET_POLICIES +} + +PLUGIN_COMPATIBLE_STRATEGIES: dict[str, tuple[str, ...]] = { + plugin: tuple( + policy.strategy + for policy in PLUGIN_CONSUMPTION_POLICIES + if policy.plugin == plugin and policy.notification_allowed + ) + for plugin in sorted({policy.plugin for policy in PLUGIN_CONSUMPTION_POLICIES}) +} + +PLUGIN_COMPATIBLE_NOTIFICATION_TARGETS: dict[str, tuple[str, ...]] = { + plugin: tuple( + policy.notification_target + for policy in PLUGIN_NOTIFICATION_TARGET_POLICIES + if policy.plugin == plugin and policy.notification_allowed + ) + for plugin in sorted({policy.plugin for policy in PLUGIN_NOTIFICATION_TARGET_POLICIES}) +} + +__all__ = [ + "EVIDENCE_AUTOMATION_APPROVED", + "EVIDENCE_DEPRECATED_COMPATIBILITY", + "EVIDENCE_NOTIFICATION_ONLY", + "GENERAL_MARKET_REGIME_NOTIFICATION_TARGET", + "PLUGIN_COMPATIBLE_NOTIFICATION_TARGETS", + "PLUGIN_COMPATIBLE_STRATEGIES", + "PLUGIN_CONSUMPTION_POLICIES", + "PLUGIN_CONSUMPTION_POLICY_REGISTRY", + "PLUGIN_CRISIS_RESPONSE_SHADOW", + "PLUGIN_DEPRECATED_SUCCESSORS", + "PLUGIN_LIFECYCLE_AUTOMATION_APPROVED", + "PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY", + "PLUGIN_LIFECYCLE_NOTIFICATION_ONLY", + "PLUGIN_LIFECYCLE_POLICIES", + "PLUGIN_LIFECYCLE_POLICY_REGISTRY", + "PLUGIN_MACRO_RISK_GOVERNOR", + "PLUGIN_MARKET_REGIME_CONTROL", + "PLUGIN_NOTIFICATION_TARGET_POLICIES", + "PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY", + "PLUGIN_PANIC_REVERSAL_SHADOW", + "PLUGIN_SCHEMA_VERSIONS", + "PLUGIN_TACO_REBOUND_SHADOW", + "PLUGIN_RESEARCH_ONLY_REASONS", + "PluginConsumptionPolicy", + "PluginLifecyclePolicy", + "PluginNotificationTargetPolicy", +] diff --git a/src/quant_strategy_plugins/strategy_plugin_runner.py b/src/quant_strategy_plugins/strategy_plugin_runner.py index 599e50c..8fa2f5b 100644 --- a/src/quant_strategy_plugins/strategy_plugin_runner.py +++ b/src/quant_strategy_plugins/strategy_plugin_runner.py @@ -15,57 +15,56 @@ write_crisis_response_shadow_outputs, ) from .macro_risk_governor_plugin import ( - MACRO_RISK_GOVERNOR_PROFILE, build_macro_risk_governor_signal, write_macro_risk_governor_outputs, ) from .market_regime_control_plugin import ( - MARKET_REGIME_CONTROL_PROFILE, build_market_regime_control_signal, write_market_regime_control_outputs, ) from .panic_reversal_shadow_plugin import ( - PANIC_REVERSAL_PROFILE, build_panic_reversal_shadow_signal, write_panic_reversal_shadow_outputs, ) from .russell_1000_multi_factor_defensive_snapshot import read_table from .taco_panic_rebound_research import DEFAULT_EVENT_SET, resolve_trade_war_event_set from .taco_rebound_shadow_plugin import ( - TACO_REBOUND_PROFILE, build_taco_rebound_shadow_signal, write_taco_rebound_shadow_outputs, ) from .volatility_delever_price_rebound import build_volatility_delever_price_rebound_context +from .plugin_policies import ( + EVIDENCE_AUTOMATION_APPROVED, + EVIDENCE_DEPRECATED_COMPATIBILITY, + EVIDENCE_NOTIFICATION_ONLY, + GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + PLUGIN_COMPATIBLE_NOTIFICATION_TARGETS, + PLUGIN_COMPATIBLE_STRATEGIES, + PLUGIN_CONSUMPTION_POLICIES, + PLUGIN_CONSUMPTION_POLICY_REGISTRY, + PLUGIN_CRISIS_RESPONSE_SHADOW, + PLUGIN_DEPRECATED_SUCCESSORS, + PLUGIN_LIFECYCLE_POLICIES, + PLUGIN_LIFECYCLE_POLICY_REGISTRY, + PLUGIN_MACRO_RISK_GOVERNOR, + PLUGIN_MARKET_REGIME_CONTROL, + PLUGIN_NOTIFICATION_TARGET_POLICIES, + PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY, + PLUGIN_PANIC_REVERSAL_SHADOW, + PLUGIN_RESEARCH_ONLY_REASONS, + PLUGIN_SCHEMA_VERSIONS, + PLUGIN_TACO_REBOUND_SHADOW, + PluginConsumptionPolicy, + PluginLifecyclePolicy, + PluginNotificationTargetPolicy, +) DEFAULT_RUNNER_OUTPUT_DIR = "data/output/strategy_plugins" -GENERAL_MARKET_REGIME_NOTIFICATION_TARGET = "market_regime_notification" -PLUGIN_CRISIS_RESPONSE_SHADOW = "crisis_response_shadow" -PLUGIN_MARKET_REGIME_CONTROL = MARKET_REGIME_CONTROL_PROFILE -PLUGIN_MACRO_RISK_GOVERNOR = MACRO_RISK_GOVERNOR_PROFILE -PLUGIN_PANIC_REVERSAL_SHADOW = PANIC_REVERSAL_PROFILE -PLUGIN_TACO_REBOUND_SHADOW = TACO_REBOUND_PROFILE SUPPORTED_PLUGIN_MODES = (SHADOW_MODE,) STRATEGY_PLUGIN_MESSAGE_SCHEMA_VERSION = "strategy_plugin_messages.v1" STRATEGY_PLUGIN_LOG_SCHEMA_VERSION = "strategy_plugin_log.v1" DEFAULT_MESSAGE_LOCALE = "en-US" SUPPORTED_MESSAGE_LOCALES = ("en-US", "zh-CN") -EVIDENCE_AUTOMATION_APPROVED = "automation_approved" -EVIDENCE_NOTIFICATION_ONLY = "notification_only" -EVIDENCE_DEPRECATED_COMPATIBILITY = "deprecated_compatibility" -PLUGIN_SCHEMA_VERSIONS: dict[str, tuple[str, ...]] = { - PLUGIN_CRISIS_RESPONSE_SHADOW: ("crisis_response_shadow.v1",), - PLUGIN_MARKET_REGIME_CONTROL: ("market_regime_control.v1",), - PLUGIN_MACRO_RISK_GOVERNOR: ("macro_risk_governor.v1",), - PLUGIN_PANIC_REVERSAL_SHADOW: ("panic_reversal_shadow.v1",), - PLUGIN_TACO_REBOUND_SHADOW: ("taco_rebound_shadow.v2",), -} -PLUGIN_DEPRECATED_SUCCESSORS: dict[str, str] = { - PLUGIN_CRISIS_RESPONSE_SHADOW: PLUGIN_MARKET_REGIME_CONTROL, - PLUGIN_MACRO_RISK_GOVERNOR: PLUGIN_MARKET_REGIME_CONTROL, - PLUGIN_TACO_REBOUND_SHADOW: PLUGIN_MARKET_REGIME_CONTROL, -} -PLUGIN_RESEARCH_ONLY_REASONS: dict[str, str] = {} @dataclass(frozen=True) @@ -82,165 +81,6 @@ class PluginRunResult: target_type: str = "strategy" notification_target: str | None = None - -@dataclass(frozen=True) -class PluginConsumptionPolicy: - plugin: str - strategy: str - notification_allowed: bool - position_control_allowed: bool - evidence_status: str - since_version: str - description: str - intended_strategy_role: str | None = None - manual_review_notification_target: str | None = None - - -@dataclass(frozen=True) -class PluginNotificationTargetPolicy: - plugin: str - notification_target: str - notification_allowed: bool - position_control_allowed: bool - evidence_status: str - since_version: str - description: str - notification_role: str - - -PLUGIN_CONSUMPTION_POLICIES: tuple[PluginConsumptionPolicy, ...] = ( - PluginConsumptionPolicy( - plugin=PLUGIN_MARKET_REGIME_CONTROL, - strategy="tqqq_growth_income", - notification_allowed=True, - position_control_allowed=True, - evidence_status=EVIDENCE_AUTOMATION_APPROVED, - since_version="strategy_plugins.v1", - description="Backtested automatic macro/crisis risk controls for the TQQQ growth-income strategy.", - manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - ), - PluginConsumptionPolicy( - plugin=PLUGIN_MARKET_REGIME_CONTROL, - strategy="global_etf_rotation", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="Pending 25-30 year market-regime-control validation for broad ETF rotation.", - manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - ), - PluginConsumptionPolicy( - plugin=PLUGIN_MARKET_REGIME_CONTROL, - strategy="russell_1000_multi_factor_defensive", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="Pending 25-30 year market-regime-control validation for the Russell 1000 defensive sleeve.", - manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - ), - PluginConsumptionPolicy( - plugin=PLUGIN_MARKET_REGIME_CONTROL, - strategy="mega_cap_leader_rotation_top50_balanced", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="Pending 25-30 year market-regime-control validation for the mega-cap leader rotation profile.", - manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - ), - PluginConsumptionPolicy( - plugin=PLUGIN_MARKET_REGIME_CONTROL, - strategy="soxl_soxx_trend_income", - notification_allowed=True, - position_control_allowed=True, - evidence_status=EVIDENCE_AUTOMATION_APPROVED, - since_version="strategy_plugins.v1", - description="Backtested automatic macro/crisis risk controls for the SOXL/SOXX trend-income strategy.", - manual_review_notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - ), - PluginConsumptionPolicy( - plugin=PLUGIN_CRISIS_RESPONSE_SHADOW, - strategy="tqqq_growth_income", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_DEPRECATED_COMPATIBILITY, - since_version="strategy_plugins.v1", - description="Deprecated direct crisis shadow mount kept for historical replay; new consumers use market_regime_control.", - ), - PluginConsumptionPolicy( - plugin=PLUGIN_MACRO_RISK_GOVERNOR, - strategy="tqqq_growth_income", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_DEPRECATED_COMPATIBILITY, - since_version="strategy_plugins.v1", - description="Deprecated direct macro governor mount kept for historical replay; new consumers use market_regime_control.", - ), - PluginConsumptionPolicy( - plugin=PLUGIN_TACO_REBOUND_SHADOW, - strategy="tqqq_growth_income", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="Manual-review event rebound notifier for TQQQ only.", - ), - PluginConsumptionPolicy( - plugin=PLUGIN_PANIC_REVERSAL_SHADOW, - strategy="tqqq_growth_income", - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="Research-only VIX panic reversal notifier for TQQQ manual review.", - ), -) -PLUGIN_CONSUMPTION_POLICY_REGISTRY: dict[tuple[str, str], PluginConsumptionPolicy] = { - (policy.plugin, policy.strategy): policy for policy in PLUGIN_CONSUMPTION_POLICIES -} -PLUGIN_NOTIFICATION_TARGET_POLICIES: tuple[PluginNotificationTargetPolicy, ...] = ( - PluginNotificationTargetPolicy( - plugin=PLUGIN_MARKET_REGIME_CONTROL, - notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="General market-regime notice. Not mounted into an automated strategy runtime.", - notification_role="general_market_regime_notification", - ), - PluginNotificationTargetPolicy( - plugin=PLUGIN_PANIC_REVERSAL_SHADOW, - notification_target=GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, - notification_allowed=True, - position_control_allowed=False, - evidence_status=EVIDENCE_NOTIFICATION_ONLY, - since_version="strategy_plugins.v1", - description="General research-only panic reversal notice. Not mounted into an automated strategy runtime.", - notification_role="panic_reversal_notification", - ), -) -PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY: dict[tuple[str, str], PluginNotificationTargetPolicy] = { - (policy.plugin, policy.notification_target): policy for policy in PLUGIN_NOTIFICATION_TARGET_POLICIES -} -PLUGIN_COMPATIBLE_STRATEGIES: dict[str, tuple[str, ...]] = { - plugin: tuple( - policy.strategy - for policy in PLUGIN_CONSUMPTION_POLICIES - if policy.plugin == plugin and policy.notification_allowed - ) - for plugin in sorted({policy.plugin for policy in PLUGIN_CONSUMPTION_POLICIES}) -} -PLUGIN_COMPATIBLE_NOTIFICATION_TARGETS: dict[str, tuple[str, ...]] = { - plugin: tuple( - policy.notification_target - for policy in PLUGIN_NOTIFICATION_TARGET_POLICIES - if policy.plugin == plugin and policy.notification_allowed - ) - for plugin in sorted({policy.plugin for policy in PLUGIN_NOTIFICATION_TARGET_POLICIES}) -} - LOCALIZED_ROUTE_LABELS: dict[str, dict[str, str]] = { "blocked": {"en-US": "Blocked", "zh-CN": "已阻断"}, "crisis": {"en-US": "Crisis", "zh-CN": "危机"}, @@ -1949,6 +1789,9 @@ def main(argv: list[str] | None = None) -> int: __all__ = [ + "EVIDENCE_AUTOMATION_APPROVED", + "EVIDENCE_DEPRECATED_COMPATIBILITY", + "EVIDENCE_NOTIFICATION_ONLY", "GENERAL_MARKET_REGIME_NOTIFICATION_TARGET", "PLUGIN_CRISIS_RESPONSE_SHADOW", "PLUGIN_MARKET_REGIME_CONTROL", @@ -1960,6 +1803,8 @@ def main(argv: list[str] | None = None) -> int: "PLUGIN_CONSUMPTION_POLICIES", "PLUGIN_CONSUMPTION_POLICY_REGISTRY", "PLUGIN_DEPRECATED_SUCCESSORS", + "PLUGIN_LIFECYCLE_POLICIES", + "PLUGIN_LIFECYCLE_POLICY_REGISTRY", "PLUGIN_NOTIFICATION_TARGET_POLICIES", "PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY", "PLUGIN_RESEARCH_ONLY_REASONS", @@ -1967,6 +1812,7 @@ def main(argv: list[str] | None = None) -> int: "STRATEGY_PLUGIN_LOG_SCHEMA_VERSION", "STRATEGY_PLUGIN_MESSAGE_SCHEMA_VERSION", "PluginConsumptionPolicy", + "PluginLifecyclePolicy", "PluginNotificationTargetPolicy", "PluginRunResult", "load_plugin_config", diff --git a/tests/test_plugin_policies.py b/tests/test_plugin_policies.py new file mode 100644 index 0000000..03e6d3a --- /dev/null +++ b/tests/test_plugin_policies.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import quant_strategy_plugins.strategy_plugin_runner as strategy_plugin_runner +from quant_strategy_plugins.plugin_policies import ( + EVIDENCE_AUTOMATION_APPROVED, + GENERAL_MARKET_REGIME_NOTIFICATION_TARGET, + PLUGIN_COMPATIBLE_STRATEGIES, + PLUGIN_CONSUMPTION_POLICY_REGISTRY, + PLUGIN_CRISIS_RESPONSE_SHADOW, + PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY, + PLUGIN_LIFECYCLE_POLICY_REGISTRY, + PLUGIN_MARKET_REGIME_CONTROL, + PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY, +) + + +def test_market_regime_control_tqqq_policy_is_automation_approved() -> None: + policy = PLUGIN_CONSUMPTION_POLICY_REGISTRY[(PLUGIN_MARKET_REGIME_CONTROL, "tqqq_growth_income")] + + assert policy.evidence_status == EVIDENCE_AUTOMATION_APPROVED + assert policy.notification_allowed is True + assert policy.position_control_allowed is True + assert policy.manual_review_notification_target == GENERAL_MARKET_REGIME_NOTIFICATION_TARGET + + +def test_runner_reexports_policy_registries() -> None: + assert strategy_plugin_runner.PLUGIN_CONSUMPTION_POLICY_REGISTRY is PLUGIN_CONSUMPTION_POLICY_REGISTRY + assert strategy_plugin_runner.PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY is PLUGIN_NOTIFICATION_TARGET_POLICY_REGISTRY + assert strategy_plugin_runner.PLUGIN_COMPATIBLE_STRATEGIES is PLUGIN_COMPATIBLE_STRATEGIES + assert strategy_plugin_runner.PLUGIN_LIFECYCLE_POLICY_REGISTRY is PLUGIN_LIFECYCLE_POLICY_REGISTRY + + +def test_deprecated_plugin_lifecycle_blocks_new_mounts_but_keeps_replay() -> None: + policy = PLUGIN_LIFECYCLE_POLICY_REGISTRY[PLUGIN_CRISIS_RESPONSE_SHADOW] + + assert policy.lifecycle_stage == PLUGIN_LIFECYCLE_DEPRECATED_COMPATIBILITY + assert policy.new_mount_allowed is False + assert policy.replay_only is True + assert policy.successor == PLUGIN_MARKET_REGIME_CONTROL