Skip to content

Commit d62612f

Browse files
authored
Harden market regime plugin contract (#27)
1 parent 4c186d5 commit d62612f

7 files changed

Lines changed: 231 additions & 33 deletions

docs/examples/strategy_plugins.example.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ macro_enabled = true
7777
# Automation-approved deterministic SOXX price/volatility rebound context.
7878
# It only feeds volatility_delever_context retention profiles; TACO, panic
7979
# reversal, AI audit, OSINT, and localized copy remain manual-review/display
80-
# evidence and do not directly change SOXL allocation.
80+
# evidence and do not directly change SOXL allocation. The SOXL strategy default
81+
# consumes the aggressive soxl_step_rebound_0.25_0.50 profile; the plugin also
82+
# emits soxl_step_softzero_rebound_0.25_0.50 for conservative strategy configs.
8183
volatility_delever_price_rebound_enabled = true
8284

8385
[strategy_plugins.outputs]

docs/market-regime-control-plan.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,24 @@ Recommended policy:
114114
`position_control.volatility_delever_context` retention profiles when its
115115
local volatility gate triggers. TACO, panic reversal, AI audit, OSINT, and
116116
localized notification copy remain manual-review context only.
117+
The live default remains the aggressive
118+
`soxl_step_rebound_0.25_0.50` profile: retain 50% on constructive confirmed
119+
price rebound, retain 25% on a soft-risk price-rebound candidate, and retain
120+
0% under hard risk or without deterministic price rebound. The plugin also
121+
emits `soxl_step_softzero_rebound_0.25_0.50` as a conservative switch that
122+
clears retention under soft risk.
117123
- Global ETF, Russell 1000, and Mega Cap rotation
118124
strategies
119-
Support the unified plugin by default. `risk_reduced` should apply a 50% risk
120-
budget scale; `risk_off` should zero the risk-asset budget.
125+
May generate the unified plugin artifact for notification and evidence
126+
archiving, but their strategy-level `position_control_allowed` flag stays
127+
false until a dedicated 25-30 year validation package is archived. The
128+
candidate rule for that later promotion remains: `risk_reduced` applies a 50%
129+
risk-budget scale and `risk_off` zeros the risk-asset budget.
121130
- DCA or low-frequency income strategies
122-
Default to notification-only and may allow explicit operator opt-in for
123-
position impact.
131+
Default to notification-only. They should not sell or de-risk existing
132+
holdings from this plugin; any future automation should be a separate
133+
cash-deployment control for new buys, such as pause, halve, or catch-up,
134+
after its own validation.
124135

125136
Legacy plugins remain available for historical backtests and compatibility
126137
outputs. New integrations should prefer `market_regime_control`.
@@ -145,6 +156,21 @@ the human notification body:
145156
- Automated position impact happens only when the strategy side explicitly
146157
consumes `position_control`, and only when `position_control_allowed = true`
147158
and `evidence_status = automation_approved`.
159+
- New or expanded automated position consumers should clear a 25-30 year
160+
long-history validation gate before promotion. For TQQQ/SOXL-style products
161+
without enough live ETF history, the long replay must explicitly use a
162+
synthetic daily-reset 3x proxy built from the underlying QQQ/SOXX series and
163+
archive synthetic and real-ETF evidence separately.
164+
- The current automation-approved strategy consumers are TQQQ growth/income
165+
and SOXL/SOXX trend/income. Global ETF, Russell 1000, and Mega Cap rotation
166+
remain notification/evidence artifacts for this plugin until their own
167+
long-history promotion packages are archived. Their runner artifacts carry
168+
`position_control_allowed = false`,
169+
`strategy_runtime_metadata_allowed = false`, and
170+
`capital_impact = notification_only`.
171+
- The same automation evidence can support aggressive and conservative
172+
strategy-side consumption preferences. The plugin only emits deterministic
173+
fields and evidence; the strategy config chooses which preference to apply.
148174
- `notification_only`, TACO, panic reversal, AI audit, and general notification
149175
targets are for manual review only.
150176
- Dedicated plugin-alert bots should publish only manual-review or
@@ -170,6 +196,11 @@ must avoid hard or soft stress. This context only feeds
170196
`position_control.volatility_delever_context.retention_profiles`; it does not
171197
change the `risk_off`, `risk_reduced`, or `watch` route.
172198

199+
TACO and panic reversal may appear in
200+
`position_control.volatility_delever_context.rebound_sources` for display and
201+
manual-review context, but they do not raise automatic SOXL retention profiles.
202+
Automatic SOXL retention requires `price_rebound_context` hard-data evidence.
203+
173204
## Indicator Tiers
174205

175206
Current indicator tiers:
@@ -305,8 +336,9 @@ Design implications:
305336
volatility-delever retention context, but does not apply `risk_reduced`
306337
position impact by default.
307338
- Rotation strategies:
308-
enable 50% risk scaling for `risk_reduced` and zero risk-asset budget for
309-
`risk_off`.
339+
remain notification-only until their 25-30 year validation packages are
340+
archived. The later promotion candidate is 50% risk scaling for
341+
`risk_reduced` and zero risk-asset budget for `risk_off`.
310342
- TACO / panic reversal:
311343
notification-only by default; they can surface opportunity context only when
312344
no crisis or macro de-risking route is active. Panic reversal remains disabled

docs/market-regime-control-plan.zh-CN.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,12 @@
7373
- SOXL/SOXX 趋势收益策略
7474
默认挂载统一插件。`risk_off` 可把风险暴露转向防守资产;`risk_reduced` 在策略默认配置中仍不产生仓位影响。SOXL 继续使用已复核的 SOXX 趋势和本地波动率降杠杆门,并可在本地波动率触发时消费确定性的
7575
`position_control.volatility_delever_context` retention profiles。SOXL 的自动 retention 只允许使用可回测的价格/波动反弹上下文:SOXX 趋势仍成立、短期均线斜率为正、实现波动达到本地降杠杆动态阈值、且 VIX/信用/金融压力未触发硬/软过滤。TACO、panic reversal、AI audit、OSINT 和本地化通知文案仍只作为人工复核上下文。
76+
当前 live 默认保留偏进攻的 `soxl_step_rebound_0.25_0.50`:建设性确认反弹保留 50%,soft-risk 但有价格反弹候选时保留 25%,hard-risk 或没有确定性价格反弹时保留 0%。插件同时输出
77+
`soxl_step_softzero_rebound_0.25_0.50` 作为保守开关,soft-risk 下直接清零。
7678
- Global ETF、Russell 1000、Mega Cap 类轮动策略
77-
默认支持统一插件。`risk_reduced` 建议做 50% 风险预算缩放,`risk_off` 建议归零风险资产预算
79+
可以先生成统一插件 artifact,用于通知和证据归档;但策略级 `position_control_allowed` 在专门的 25-30 年长周期验证包归档前保持 false。后续若推广,候选规则仍是:`risk_reduced` 50% 风险预算缩放,`risk_off` 归零风险资产预算
7880
- DCA 或收入型低频策略
79-
默认 notification-only,允许用户显式开启仓位影响
81+
默认 notification-only。不应通过该插件卖出或降低已有持仓;未来如需自动化,只能单独设计新买入资金的 cash-deployment control(暂停、减半、补投),并通过独立验证
8082

8183
旧插件仍可运行历史回测和兼容输出,但新策略集成应优先挂载 `market_regime_control`
8284

@@ -92,6 +94,12 @@
9294
- 插件仓库只生成 artifact 和通知,不调用券商接口,也不直接改账户配置。
9395
- 自动仓位影响只发生在策略侧显式消费 `position_control` 时,并且必须同时满足
9496
`position_control_allowed = true``evidence_status = automation_approved`
97+
- 新增或扩大自动仓位消费范围前,应先通过 25-30 年长周期验证。TQQQ/SOXL 这类实盘产品历史不足的策略,长周期验证必须明确使用
98+
QQQ/SOXX 等底层资产合成的 3x 日重置代理,并把 synthetic 口径和真实 ETF 口径分开归档。
99+
- 当前已批准自动消费的策略级消费者只有 TQQQ 增长收益和 SOXL/SOXX 趋势收益。Global ETF、Russell 1000 和 Mega Cap 轮动在各自长周期推广包归档前,只保留通知和证据 artifact;runner artifact 会写入
100+
`position_control_allowed = false``strategy_runtime_metadata_allowed = false`
101+
`capital_impact = notification_only`
102+
- 同一个自动化证据可以有偏进攻和偏保守两种策略侧消费偏好;插件只输出确定性字段和证据,具体选用哪个偏好由策略配置决定。
95103
- `notification_only`、TACO、panic reversal、AI audit 和通用通知只用于人工查看。
96104
- 专用插件告警 bot 只发布人工复核或 notification-only 插件信号。如果策略消费了已通过自动化证据批准的
97105
`defend``delever` 路线,并实际产生仓位影响,面向用户的通知应由该策略运行结果承载。
@@ -106,6 +114,8 @@ SOXL/SOXX 现在出现在 `market_regime_control` 的策略级消费 registry
106114
- 已可执行打分:价格趋势、63/252 日回撤、实现波动、VIX 水平/尖峰、信用 ETF 相对压力。
107115
- 已可执行 retention 上下文:SOXL 本地波动降杠杆只消费 SOXX 价格趋势、均线斜率、动态实现波动阈值、VIX、HYG/IEF 和 XLF/SPY 这类可回测硬数据;该上下文只写入
108116
`volatility_delever_context.retention_profiles`,不改变 `risk_off` / `risk_reduced` / `watch` 路由。
117+
TACO 和 panic reversal 可以出现在 `volatility_delever_context.rebound_sources` 里作为展示和人工复核上下文,但不会抬高 SOXL 自动 retention;自动 SOXL retention 必须来自
118+
`price_rebound_context` 的硬数据证据。
109119
- 已接入 watch-only:HY OAS、金融压力指数、五角大楼比萨指数、Fear & Greed、put/call、safe-haven demand、VIX/VIX3M 期限结构、VVIX、SKEW、MOVE、IG OAS、资金压力利差、10Y-2Y/10Y-3M 曲线、DXY 21 日压力、50/200 日市场宽度、新高新低、涨跌线回撤、AAII bearish-bullish spread 和 NAAIM exposure。
110120
- 未进入自动仓位:所有 watch-only 指标。它们只用于通知、证据归档和后续历史回测。
111121
- 研究开关:`external_stress_actionable = true` 可让 HY OAS、HY OAS 63 日扩张和金融压力指数进入可执行分数;默认固定为 `false`
@@ -170,6 +180,6 @@ TQQQ 2010-2026 真实产品窗口:
170180

171181
- 杠杆策略:默认挂载统一插件,允许 `risk_off` 生效。
172182
- 高波动行业杠杆策略:SOXL 在 2026-06-16 retention replay 后默认挂载统一插件;默认策略配置允许 `risk_off` 和确定性波动率降杠杆 retention context,但不默认应用 `risk_reduced` 仓位影响。
173-
- 轮动策略:默认开启 50% risk scaling`risk_off` 归零
183+
- 轮动策略:在 25-30 年长周期验证包归档前保持 notification-only。后续推广候选规则是 `risk_reduced`50% risk scaling`risk_off` 归零风险资产预算
174184
- TACO / panic reversal:默认通知-only;只有没有危机和宏观降风险时才允许提示机会。panic reversal 默认保持研究开关关闭,直到事件窗口和 no-regression 报告证明可提升权限。
175185
- AI audit:默认不参与交易权限,只能写审计结论和通知证据。

src/quant_strategy_plugins/market_regime_control_plugin.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,21 @@ def _retention_profile(retention_ratio: float, *reason_codes: str) -> dict[str,
162162
}
163163

164164

165+
def _price_rebound_candidate(price_rebound_context: Mapping[str, Any] | None) -> bool:
166+
if not isinstance(price_rebound_context, Mapping):
167+
return False
168+
return bool(
169+
_as_bool(price_rebound_context.get("volatility_triggered"), default=False)
170+
and _as_bool(price_rebound_context.get("trend_ok"), default=False)
171+
and (
172+
_as_bool(price_rebound_context.get("rebound_1d"), default=False)
173+
or _as_bool(price_rebound_context.get("rebound_nd"), default=False)
174+
or _as_bool(price_rebound_context.get("confirmed"), default=False)
175+
)
176+
and not _as_bool(price_rebound_context.get("hard_filter"), default=False)
177+
)
178+
179+
165180
def _build_volatility_delever_context(
166181
*,
167182
final_route: str,
@@ -180,7 +195,11 @@ def _build_volatility_delever_context(
180195
isinstance(price_rebound_context, Mapping)
181196
and _as_bool(price_rebound_context.get("confirmed"), default=False)
182197
and not hard_risk
183-
and not soft_risk
198+
and not blocked
199+
)
200+
price_rebound_candidate = bool(
201+
_price_rebound_candidate(price_rebound_context)
202+
and not hard_risk
184203
and not blocked
185204
)
186205
opportunity_rebound_confirm = bool(taco_active or panic_reversal_active)
@@ -197,23 +216,28 @@ def _build_volatility_delever_context(
197216
tqqq_softzero_025 = _retention_profile(0.0, "hard_risk")
198217
tqqq_softzero_035 = _retention_profile(0.0, "hard_risk")
199218
soxl_rebound = _retention_profile(0.0, "hard_risk")
219+
soxl_softzero_rebound = _retention_profile(0.0, "hard_risk")
200220
elif soft_risk:
201221
tqqq_softzero_025 = _retention_profile(0.0, "soft_risk")
202222
tqqq_softzero_035 = _retention_profile(0.0, "soft_risk")
203-
soxl_rebound = _retention_profile(0.0, "soft_risk")
204-
elif rebound_confirm:
205-
if opportunity_rebound_confirm:
206-
tqqq_softzero_025 = _retention_profile(0.50, "constructive", "rebound_confirm")
207-
tqqq_softzero_035 = _retention_profile(0.50, "constructive", "rebound_confirm")
208-
soxl_rebound = _retention_profile(0.50 if constructive else 0.25, "constructive", "rebound_confirm")
209-
else:
210-
tqqq_softzero_025 = _retention_profile(0.25, "non_soft_risk")
211-
tqqq_softzero_035 = _retention_profile(0.35, "non_soft_risk")
212-
soxl_rebound = _retention_profile(0.50, "constructive", "price_rebound_confirm")
223+
soxl_rebound = (
224+
_retention_profile(0.25, "price_rebound_candidate", "soft_risk")
225+
if price_rebound_candidate
226+
else _retention_profile(0.0, "soft_risk")
227+
)
228+
soxl_softzero_rebound = _retention_profile(0.0, "soft_risk")
213229
else:
214230
tqqq_softzero_025 = _retention_profile(0.25, "non_soft_risk")
215231
tqqq_softzero_035 = _retention_profile(0.35, "non_soft_risk")
216-
soxl_rebound = _retention_profile(0.0, "rebound_not_confirmed")
232+
if price_rebound_confirm:
233+
soxl_rebound = _retention_profile(0.50, "constructive", "price_rebound_confirm")
234+
soxl_softzero_rebound = _retention_profile(0.50, "constructive", "price_rebound_confirm")
235+
elif price_rebound_candidate:
236+
soxl_rebound = _retention_profile(0.25, "price_rebound_candidate")
237+
soxl_softzero_rebound = _retention_profile(0.25, "price_rebound_candidate")
238+
else:
239+
soxl_rebound = _retention_profile(0.0, "rebound_not_confirmed")
240+
soxl_softzero_rebound = _retention_profile(0.0, "rebound_not_confirmed")
217241
context = {
218242
"schema_version": "volatility_delever_context.v1",
219243
"source": "deterministic_market_regime_components",
@@ -222,13 +246,15 @@ def _build_volatility_delever_context(
222246
"soft_risk": soft_risk,
223247
"constructive": constructive,
224248
"rebound_confirm": rebound_confirm,
249+
"price_rebound_candidate": price_rebound_candidate,
225250
"rebound_sources": tuple(rebound_sources),
226251
"macro_watch": bool(macro_watch),
227252
"reason_codes": tuple(dict.fromkeys(reason_codes)),
228253
"retention_profiles": {
229254
"tqqq_step_softzero_0.25_0.50": tqqq_softzero_025,
230255
"tqqq_step_softzero_0.35_0.50": tqqq_softzero_035,
231256
"soxl_step_rebound_0.25_0.50": soxl_rebound,
257+
"soxl_step_softzero_rebound_0.25_0.50": soxl_softzero_rebound,
232258
},
233259
}
234260
if isinstance(price_rebound_context, Mapping) and price_rebound_context:

src/quant_strategy_plugins/strategy_plugin_runner.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,28 +121,28 @@ class PluginNotificationTargetPolicy:
121121
plugin=PLUGIN_MARKET_REGIME_CONTROL,
122122
strategy="global_etf_rotation",
123123
notification_allowed=True,
124-
position_control_allowed=True,
125-
evidence_status=EVIDENCE_AUTOMATION_APPROVED,
124+
position_control_allowed=False,
125+
evidence_status=EVIDENCE_NOTIFICATION_ONLY,
126126
since_version="strategy_plugins.v1",
127-
description="Local risk-scaling consumer for broad ETF rotation.",
127+
description="Pending 25-30 year market-regime-control validation for broad ETF rotation.",
128128
),
129129
PluginConsumptionPolicy(
130130
plugin=PLUGIN_MARKET_REGIME_CONTROL,
131131
strategy="russell_1000_multi_factor_defensive",
132132
notification_allowed=True,
133-
position_control_allowed=True,
134-
evidence_status=EVIDENCE_AUTOMATION_APPROVED,
133+
position_control_allowed=False,
134+
evidence_status=EVIDENCE_NOTIFICATION_ONLY,
135135
since_version="strategy_plugins.v1",
136-
description="Local risk-scaling consumer for the Russell 1000 defensive sleeve.",
136+
description="Pending 25-30 year market-regime-control validation for the Russell 1000 defensive sleeve.",
137137
),
138138
PluginConsumptionPolicy(
139139
plugin=PLUGIN_MARKET_REGIME_CONTROL,
140140
strategy="mega_cap_leader_rotation_top50_balanced",
141141
notification_allowed=True,
142-
position_control_allowed=True,
143-
evidence_status=EVIDENCE_AUTOMATION_APPROVED,
142+
position_control_allowed=False,
143+
evidence_status=EVIDENCE_NOTIFICATION_ONLY,
144144
since_version="strategy_plugins.v1",
145-
description="Local risk-scaling consumer for the mega-cap leader rotation profile.",
145+
description="Pending 25-30 year market-regime-control validation for the mega-cap leader rotation profile.",
146146
),
147147
PluginConsumptionPolicy(
148148
plugin=PLUGIN_MARKET_REGIME_CONTROL,
@@ -1504,6 +1504,14 @@ def _apply_plugin_contract(
15041504
execution_controls["notification_allowed"] = bool(consumption_policy.notification_allowed)
15051505
execution_controls["position_control_allowed"] = bool(consumption_policy.position_control_allowed)
15061506
execution_controls["consumption_evidence_status"] = consumption_policy.evidence_status
1507+
if consumption_policy.position_control_allowed:
1508+
execution_controls["capital_impact"] = "strategy_opt_in"
1509+
execution_controls["strategy_runtime_metadata_allowed"] = True
1510+
execution_controls["position_control_shadow_only"] = False
1511+
else:
1512+
execution_controls["capital_impact"] = "notification_only"
1513+
execution_controls["strategy_runtime_metadata_allowed"] = False
1514+
execution_controls["position_control_shadow_only"] = True
15071515
if notification_target_policy is not None:
15081516
execution_controls["notification_allowed"] = bool(notification_target_policy.notification_allowed)
15091517
execution_controls["position_control_allowed"] = False

0 commit comments

Comments
 (0)