Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/examples/strategy_plugins.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ taco_opportunity_size_scalar = 0.0
crisis_enabled = true
macro_enabled = true
taco_enabled = true
# Research-only VIX panic reversal evidence. Keep disabled by default until
# event-window and no-regression reports justify promotion beyond notification.
panic_reversal_enabled = false

[strategy_plugins.outputs]
output_dir = "data/output/tqqq_growth_income/plugins/market_regime_control"
Expand Down Expand Up @@ -66,6 +69,7 @@ realized_vol_requires_confirmation = true
external_stress_actionable = false
delever_risk_asset_scalar = 0.0
taco_enabled = false
panic_reversal_enabled = false
crisis_enabled = true
macro_enabled = true

Expand Down Expand Up @@ -123,3 +127,37 @@ start_date = "2026-01-01"

[strategy_plugins.outputs]
output_dir = "data/output/tqqq_growth_income/plugins/taco_rebound_shadow"

[[strategy_plugins]]
strategy = "tqqq_growth_income"
plugin = "panic_reversal_shadow"
enabled = false

[strategy_plugins.inputs]
prices = "data/output/panic_reversal_shadow/input/price_history.csv"
external_context = "data/output/market_regime_control/input/external_context.csv"
benchmark_symbol = "QQQ"
attack_symbol = "TQQQ"
vix_symbols = ["VIX", "^VIX", "VIXCLS"]
vix3m_symbols = ["VIX3M", "^VIX3M", "VXV", "^VXV"]
start_date = "2010-01-01"

[strategy_plugins.outputs]
output_dir = "data/output/tqqq_growth_income/plugins/panic_reversal_shadow"

[[notification_targets]]
notification_target = "market_regime_notification"
plugin = "panic_reversal_shadow"
enabled = false

[notification_targets.inputs]
prices = "data/output/panic_reversal_shadow/input/soxl_price_history.csv"
external_context = "data/output/market_regime_control/input/external_context.csv"
benchmark_symbol = "SOXX"
attack_symbol = "SOXL"
vix_symbols = ["VIX", "^VIX", "VIXCLS"]
vix3m_symbols = ["VIX3M", "^VIX3M", "VXV", "^VXV"]
start_date = "2016-06-06"

[notification_targets.outputs]
output_dir = "data/output/market_regime_notification/plugins/panic_reversal_shadow"
30 changes: 18 additions & 12 deletions docs/market-regime-control-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ Non-goals:
- `taco_rebound_shadow`
Handles TQQQ event-rebound notification. It emits manual-review notification
context and local veto evidence, but it does not raise position size.
- `panic_reversal_shadow`
Handles research-only VIX panic-reversal notification after volatility has
fallen from a panic high and price confirmation is present. It emits
manual-review context only; the sample is still too small, so it is disabled
by default and cannot raise position size.

The unified artifact exposes five main sections:

Expand All @@ -74,15 +79,15 @@ The unified artifact exposes five main sections:
The current order is risk-first:

1. `crisis_response_shadow` `true_crisis` or bubble fragility has top priority,
emits `risk_off`, and vetoes TACO.
emits `risk_off`, and vetoes TACO and panic reversal.
2. `macro_risk_governor` crisis state is next, emits `risk_off`, and vetoes
TACO.
TACO and panic reversal.
3. `macro_risk_governor` de-leveraging emits `risk_reduced`, scales down
leverage or risk-asset budget, and vetoes TACO.
leverage or risk-asset budget, and vetoes TACO and panic reversal.
4. Data-quality kill switches or blocked component states block opportunity-side
actions.
5. TACO may emit `opportunity_watch` and manual-review notification only when
there is no crisis or macro de-risking route.
5. TACO or panic reversal may emit `opportunity_watch` and manual-review
notification only when there is no crisis or macro de-risking route.
6. Watch-only signals notify but never grant position-control authority.

This keeps crisis, macro, and TACO behavior separate: defense first,
Expand All @@ -99,13 +104,13 @@ Recommended policy:
- TQQQ growth/income strategy
Consumes `position_control` by default. `risk_off` moves toward cash-like or
non-risk assets; `risk_reduced` lowers leverage or risk budget based on local
strategy configuration. TACO remains manual-review notification and local
veto context only.
strategy configuration. TACO and panic reversal remain manual-review
notification and local veto context only.
- SOXL/SOXX trend/income strategy
Does not mount the unified plugin by default and does not consume
`position_control`. SOXL keeps its own reviewed SOXX trend and volatility
de-leveraging gates. Macro, crisis, and OSINT signals are delivered as general
notifications for manual review.
de-leveraging gates. Macro, crisis, panic reversal, and OSINT signals are
delivered as general notifications for manual review.
- Global ETF, Russell 1000, and Mega Cap rotation
strategies
Support the unified plugin by default. `risk_reduced` should apply a 50% risk
Expand Down Expand Up @@ -267,9 +272,10 @@ Design implications:
- Rotation strategies:
enable 50% risk scaling for `risk_reduced` and zero risk-asset budget for
`risk_off`.
- TACO:
notification-only by default; it can surface opportunity context only when no
crisis or macro de-risking route is active.
- TACO / panic reversal:
notification-only by default; they can surface opportunity context only when
no crisis or macro de-risking route is active. Panic reversal remains disabled
by default until event-window and no-regression reports justify promotion.
- AI audit:
no trading authority by default. It may write audit conclusions and
notification evidence only.
10 changes: 6 additions & 4 deletions docs/market-regime-control-plan.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
只有显式研究开关 `external_stress_actionable` 开启后,外部压力字段才允许进入可执行分数。
- `taco_rebound_shadow`
负责 TQQQ 事件反弹通知。它输出人工复核通知和本地 veto 线索,不直接提高仓位。
- `panic_reversal_shadow`
负责 VIX 恐慌高位回落后的研究通知。它要求 VIX 高位回落、VIX/VIX3M 期限结构和价格反弹确认同时满足,只输出人工复核通知;样本仍不足,默认不自动加仓,也不默认开启。

统一插件输出四组主要字段:

Expand All @@ -55,7 +57,7 @@
2. `macro_risk_governor` 的 `crisis` 其次,输出 `risk_off`,并 veto TACO。
3. `macro_risk_governor` 的 `delever` 输出 `risk_reduced`,降低杠杆或风险资产预算,并 veto TACO。
4. 数据质量 kill switch 或组件 blocked 状态会阻断机会侧动作。
5. 只有没有危机和宏观降风险时,TACO 才能输出 `opportunity_watch` 和人工复核通知。
5. 只有没有危机和宏观降风险时,TACO 或 panic reversal 才能输出 `opportunity_watch` 和人工复核通知。
6. watch-only 信号只通知,不给仓位权限。

这个顺序保证危机插件、宏观插件和 TACO 不冲突:防守优先,机会次之,通知和执行权限分离。
Expand All @@ -67,9 +69,9 @@
建议消费规则:

- TQQQ 杠杆增长收益策略
默认消费 `position_control`。`risk_off` 降到现金类或非风险资产;`risk_reduced` 按策略配置降低杠杆或风险预算;TACO 只触发人工复核和本地 veto。
默认消费 `position_control`。`risk_off` 降到现金类或非风险资产;`risk_reduced` 按策略配置降低杠杆或风险预算;TACO 和 panic reversal 只触发人工复核和本地 veto,不触发自动加仓
- SOXL/SOXX 趋势收益策略
不默认挂载统一插件,也不消费 `position_control`。SOXL 继续只使用已经通过复核的 SOXX 自身趋势和波动率降杠杆门;宏观、危机和 OSINT 信号只进入通用通知,由人工决定是否干预。
不默认挂载统一插件,也不消费 `position_control`。SOXL 继续只使用已经通过复核的 SOXX 自身趋势和波动率降杠杆门;宏观、危机、panic reversal 和 OSINT 信号只进入通用通知,由人工决定是否干预。
- Global ETF、Russell 1000、Mega Cap 类轮动策略
默认支持统一插件。`risk_reduced` 建议做 50% 风险预算缩放,`risk_off` 建议归零风险资产预算。
- DCA 或收入型低频策略
Expand Down Expand Up @@ -157,5 +159,5 @@ TQQQ 2010-2026 真实产品窗口:
- 杠杆策略:默认挂载统一插件,允许 `risk_off` 生效。
- 高波动行业杠杆策略:除非回测证明自动消费能提升收益/回撤组合,否则不默认挂载统一插件;SOXL 当前只接收通用通知。
- 轮动策略:默认开启 50% risk scaling 和 `risk_off` 归零。
- TACO:默认通知-only;只有没有危机和宏观降风险时才允许提示机会。
- TACO / panic reversal:默认通知-only;只有没有危机和宏观降风险时才允许提示机会。panic reversal 默认保持研究开关关闭,直到事件窗口和 no-regression 报告证明可提升权限
- AI audit:默认不参与交易权限,只能写审计结论和通知证据。
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ qsp-build-crisis-response-shadow-signal = "quant_strategy_plugins.crisis_respons
qsp-build-macro-external-context = "quant_strategy_plugins.macro_external_context:main"
qsp-build-macro-risk-governor-signal = "quant_strategy_plugins.macro_risk_governor_plugin:main"
qsp-build-taco-rebound-shadow-signal = "quant_strategy_plugins.taco_rebound_shadow_plugin:main"
qsp-build-panic-reversal-shadow-signal = "quant_strategy_plugins.panic_reversal_shadow_plugin:main"
qsp-run-strategy-plugins = "quant_strategy_plugins.strategy_plugin_runner:main"

[tool.setuptools]
Expand Down
10 changes: 10 additions & 0 deletions src/quant_strategy_plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
build_market_regime_control_signal,
write_market_regime_control_outputs,
)
from .panic_reversal_shadow_plugin import (
PANIC_REVERSAL_PROFILE,
SCHEMA_VERSION as PANIC_REVERSAL_SHADOW_SCHEMA_VERSION,
build_panic_reversal_shadow_signal,
write_panic_reversal_shadow_outputs,
)
from .strategy_plugin_runner import run_configured_plugins
from .taco_rebound_shadow_plugin import (
SCHEMA_VERSION as TACO_REBOUND_SHADOW_SCHEMA_VERSION,
Expand All @@ -33,15 +39,19 @@
"MACRO_RISK_GOVERNOR_SCHEMA_VERSION",
"MARKET_REGIME_CONTROL_PROFILE",
"MARKET_REGIME_CONTROL_SCHEMA_VERSION",
"PANIC_REVERSAL_PROFILE",
"PANIC_REVERSAL_SHADOW_SCHEMA_VERSION",
"TACO_REBOUND_PROFILE",
"TACO_REBOUND_SHADOW_SCHEMA_VERSION",
"build_crisis_response_shadow_signal",
"build_macro_risk_governor_signal",
"build_market_regime_control_signal",
"build_panic_reversal_shadow_signal",
"build_taco_rebound_shadow_signal",
"run_configured_plugins",
"write_crisis_response_shadow_outputs",
"write_macro_risk_governor_outputs",
"write_market_regime_control_outputs",
"write_panic_reversal_shadow_outputs",
"write_taco_rebound_shadow_outputs",
]
61 changes: 52 additions & 9 deletions src/quant_strategy_plugins/market_regime_control_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
COMPONENT_CRISIS = "crisis"
COMPONENT_MACRO = "macro"
COMPONENT_TACO = "taco"
COMPONENT_PANIC_REVERSAL = "panic_reversal"

ROUTE_NO_ACTION = "no_action"
ROUTE_WATCH = "watch"
Expand All @@ -37,6 +38,7 @@
MACRO_ACTIVE_ROUTES = frozenset({"delever", "crisis"})
MACRO_WATCH_ROUTES = frozenset({"watch"})
TACO_ACTIVE_ROUTES = frozenset({"taco_rebound", "taco_fake_crisis"})
PANIC_REVERSAL_ACTIVE_ROUTES = frozenset({"panic_reversal"})


def _as_bool(value: Any, *, default: bool = False) -> bool:
Expand Down Expand Up @@ -92,6 +94,8 @@ def _component_key(payload: Mapping[str, Any]) -> str | None:
return COMPONENT_MACRO
if "taco" in plugin:
return COMPONENT_TACO
if "panic_reversal" in plugin:
return COMPONENT_PANIC_REVERSAL
return None


Expand All @@ -104,7 +108,7 @@ def _normalize_component_signals(
if not isinstance(payload, Mapping):
continue
component = str(key or "").strip().lower()
if component in {COMPONENT_CRISIS, COMPONENT_MACRO, COMPONENT_TACO}:
if component in {COMPONENT_CRISIS, COMPONENT_MACRO, COMPONENT_TACO, COMPONENT_PANIC_REVERSAL}:
normalized[component] = payload
continue
inferred = _component_key(payload)
Expand Down Expand Up @@ -165,6 +169,7 @@ def _compact_signal(payload: Mapping[str, Any] | None) -> dict[str, Any]:
"manual_review_required",
"rebound_context_active",
"event_context_active",
"panic_reversal_context_active",
"price_crisis_guard_active",
"watch_label",
"notification_reason",
Expand Down Expand Up @@ -198,10 +203,12 @@ def build_market_regime_control_signal(
crisis = components.get(COMPONENT_CRISIS)
macro = components.get(COMPONENT_MACRO)
taco = components.get(COMPONENT_TACO)
panic_reversal = components.get(COMPONENT_PANIC_REVERSAL)

crisis_route = _normalized_route(crisis)
macro_route = _normalized_route(macro)
taco_route = _normalized_route(taco)
panic_reversal_route = _normalized_route(panic_reversal)
crisis_active = bool(crisis_route in CRISIS_ACTIVE_ROUTES and not _blocked(crisis))
crisis_watch = bool(
crisis_route in CRISIS_WATCH_ROUTES
Expand All @@ -223,6 +230,25 @@ def build_market_regime_control_signal(
)
)
taco_watch = bool(isinstance(taco, Mapping) and _normalized_action(taco) == ACTION_WATCH_ONLY and not _blocked(taco))
panic_reversal_active = bool(
panic_reversal_route in PANIC_REVERSAL_ACTIVE_ROUTES
and not _blocked(panic_reversal)
and (
_as_bool(
panic_reversal.get("manual_review_required") if isinstance(panic_reversal, Mapping) else None,
default=False,
)
or _as_bool(
panic_reversal.get("panic_reversal_context_active") if isinstance(panic_reversal, Mapping) else None,
default=False,
)
)
)
panic_reversal_watch = bool(
isinstance(panic_reversal, Mapping)
and _normalized_action(panic_reversal) == ACTION_WATCH_ONLY
and not _blocked(panic_reversal)
)
blocked = any(_blocked(payload) for payload in components.values())

final_route = ROUTE_NO_ACTION
Expand All @@ -233,6 +259,7 @@ def build_market_regime_control_signal(
leverage_scalar = 1.0
risk_asset_scalar = 1.0
taco_allowed = False
panic_reversal_allowed = False
local_delever_veto_allowed = False
crisis_defense_required = False
blocked_actions: tuple[str, ...] = ()
Expand All @@ -248,10 +275,12 @@ def build_market_regime_control_signal(
leverage_scalar = 0.0
risk_asset_scalar = 0.0
crisis_defense_required = True
blocked_actions = ("increase_leverage", "increase_risk", "taco_rebound_veto")
blocked_actions = ("increase_leverage", "increase_risk", "taco_rebound_veto", "panic_reversal_veto")
reason_codes.extend(f"crisis:{code}" for code in _reason_codes(crisis) or ("true_crisis",))
if taco_active:
vetoes.append("crisis_blocks_taco")
if panic_reversal_active:
vetoes.append("crisis_blocks_panic_reversal")
elif macro_active and macro_route == "crisis":
final_route = ROUTE_RISK_OFF
suggested_action = ACTION_DEFEND
Expand All @@ -260,10 +289,12 @@ def build_market_regime_control_signal(
leverage_scalar = _clamp_ratio(macro.get("leverage_scalar") if isinstance(macro, Mapping) else None, default=0.0)
risk_asset_scalar = _clamp_ratio(macro.get("risk_asset_scalar") if isinstance(macro, Mapping) else None, default=0.0)
risk_budget_scalar = risk_asset_scalar
blocked_actions = ("increase_leverage", "increase_risk", "taco_rebound_veto")
blocked_actions = ("increase_leverage", "increase_risk", "taco_rebound_veto", "panic_reversal_veto")
reason_codes.extend(f"macro:{code}" for code in _reason_codes(macro) or ("crisis",))
if taco_active:
vetoes.append("macro_crisis_blocks_taco")
if panic_reversal_active:
vetoes.append("macro_crisis_blocks_panic_reversal")
elif macro_active:
final_route = ROUTE_RISK_REDUCED
suggested_action = ACTION_DELEVER
Expand All @@ -272,29 +303,38 @@ def build_market_regime_control_signal(
leverage_scalar = _clamp_ratio(macro.get("leverage_scalar") if isinstance(macro, Mapping) else None, default=0.0)
risk_asset_scalar = _clamp_ratio(macro.get("risk_asset_scalar") if isinstance(macro, Mapping) else None, default=1.0)
risk_budget_scalar = risk_asset_scalar
blocked_actions = ("increase_leverage", "taco_rebound_veto")
blocked_actions = ("increase_leverage", "taco_rebound_veto", "panic_reversal_veto")
reason_codes.extend(f"macro:{code}" for code in _reason_codes(macro) or ("delever",))
if taco_active:
vetoes.append("macro_delever_blocks_taco")
if panic_reversal_active:
vetoes.append("macro_delever_blocks_panic_reversal")
elif blocked:
final_route = ROUTE_BLOCKED
suggested_action = ACTION_BLOCKED
route_source = "data_quality"
reason_codes.extend(f"{key}:blocked" for key, payload in components.items() if _blocked(payload))
elif taco_active:
elif taco_active or panic_reversal_active:
final_route = ROUTE_OPPORTUNITY_WATCH
suggested_action = ACTION_NOTIFY_MANUAL_REVIEW
route_source = COMPONENT_TACO
taco_allowed = True
route_source = COMPONENT_TACO if taco_active else COMPONENT_PANIC_REVERSAL
taco_allowed = bool(taco_active)
panic_reversal_allowed = bool(panic_reversal_active)
local_delever_veto_allowed = True
reason_codes.extend(f"taco:{code}" for code in _reason_codes(taco) or ("taco_rebound",))
elif macro_watch or crisis_watch or taco_watch:
if taco_active:
reason_codes.extend(f"taco:{code}" for code in _reason_codes(taco) or ("taco_rebound",))
if panic_reversal_active:
reason_codes.extend(
f"panic_reversal:{code}" for code in _reason_codes(panic_reversal) or ("panic_reversal",)
)
elif macro_watch or crisis_watch or taco_watch or panic_reversal_watch:
final_route = ROUTE_WATCH
suggested_action = ACTION_WATCH_ONLY
route_source = "watch"
reason_codes.extend(f"macro:{code}" for code in _reason_codes(macro))
reason_codes.extend(f"crisis:{code}" for code in _reason_codes(crisis))
reason_codes.extend(f"taco:{code}" for code in _reason_codes(taco))
reason_codes.extend(f"panic_reversal:{code}" for code in _reason_codes(panic_reversal))

notification = {
"allowed": True,
Expand All @@ -317,6 +357,8 @@ def build_market_regime_control_signal(
"risk_asset_scalar": _clamp_ratio(risk_asset_scalar, default=1.0),
"taco_allowed": taco_allowed,
"taco_size_scalar": _clamp_ratio(taco_opportunity_size_scalar, default=0.0) if taco_allowed else 0.0,
"panic_reversal_allowed": panic_reversal_allowed,
"panic_reversal_size_scalar": 0.0,
"local_delever_veto_allowed": local_delever_veto_allowed,
"crisis_defense_required": crisis_defense_required,
"blocked_actions": blocked_actions,
Expand Down Expand Up @@ -348,6 +390,7 @@ def build_market_regime_control_signal(
COMPONENT_CRISIS: _compact_signal(crisis),
COMPONENT_MACRO: _compact_signal(macro),
COMPONENT_TACO: _compact_signal(taco),
COMPONENT_PANIC_REVERSAL: _compact_signal(panic_reversal),
},
"execution_controls": {
"capital_impact": "strategy_opt_in",
Expand Down
Loading