Skip to content

Commit d62a34f

Browse files
committed
Add watch-only macro indicator coverage
1 parent 5b14894 commit d62a34f

7 files changed

Lines changed: 356 additions & 6 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ send notifications; plugin research and signal generation live here.
3535
optional external financial-stress fields. The artifact can expose
3636
`leverage_scalar` and `risk_asset_scalar` to strategy runtimes that explicitly
3737
opt in through mounted metadata. OSINT-style fields such as a Pentagon pizza
38-
index, plus sentiment fields such as Fear & Greed, put/call, and safe-haven
39-
demand, are kept as watch-only evidence and do not contribute to the
40-
actionable trading score.
38+
index, plus sentiment, options-volatility, rates, breadth, funding, and
39+
liquidity fields such as Fear & Greed, put/call, VVIX, SKEW, MOVE, yield
40+
curves, dollar stress, and safe-haven demand, are kept as watch-only evidence
41+
and do not contribute to the actionable trading score.
4142
- `market_regime_control`: unified deterministic facade for crisis, macro, and
4243
TACO signals. Only strategies with positive backtest evidence should mount
4344
position controls for automated consumption; SOXL/SOXX currently receives

README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Brokers、Schwab、LongBridge、Firstrade 等平台仓库只负责加载 artifac
2525

2626
- `crisis_response_shadow`:面向杠杆美股策略的黑天鹅防守观察插件。它只写入 shadow-mode artifact,不调用券商接口。
2727
可选启用 AI shadow audit:AI 只审计证据一致性和数据缺口,不改写确定性路线、不下单、不改仓位;默认优先尝试本机 Codex,失败后可走 OpenAI-compatible 或 Anthropic fallback endpoint。
28-
- `macro_risk_governor`:面向 TQQQ 的确定性宏观降杠杆插件。它按价格趋势、实现波动、VIX、信用相对压力和可选外部金融压力字段打分,输出 `leverage_scalar` / `risk_asset_scalar` 给显式 opt-in 的策略运行时消费。五角大楼比萨指数、Fear & Greed、put/call、safe-haven demand 这类 OSINT 或情绪字段只作为 watch-only 证据,不进入可执行分数。
28+
- `macro_risk_governor`:面向 TQQQ 的确定性宏观降杠杆插件。它按价格趋势、实现波动、VIX、信用相对压力和可选外部金融压力字段打分,输出 `leverage_scalar` / `risk_asset_scalar` 给显式 opt-in 的策略运行时消费。五角大楼比萨指数、Fear & Greed、put/call、VVIX、SKEW、MOVE、收益率曲线、美元压力、safe-haven demand OSINT、情绪或跨资产字段只作为 watch-only 证据,不进入可执行分数。
2929
- `market_regime_control`:统一确定性 facade,汇总 crisis、macro 和 TACO 信号,输出版本化的 `notification``position_control`。只有经过回测证明自动消费有效的策略才挂载仓位控制;SOXL/SOXX 这类未通过统一宏观插件复核的高波动行业杠杆策略只接收通用通知,人工决定是否干预。股票/ETF 轮动策略通过本地风险缩放策略消费;TACO 在统一插件里保持通知-only,并会被危机和宏观降风险路线 veto。设计说明见 [Market Regime Control 统一插件方案](docs/market-regime-control-plan.zh-CN.md)
3030
- `taco_rebound_shadow`:仅适用于 TQQQ 的事件反弹上下文通知插件。它只写入人工复核 artifact,不给仓位大小建议,也不改动配置或账户分配。缓和/降温事件会先保持 watch-only,只有事件后价格反弹确认通过后才触发人工复核通知,以减少过早抄底提醒。
3131
该插件也可选启用同样的 shadow-only AI audit,但 AI 只复核事件来源和反弹证据质量。

docs/examples/strategy_plugins.example.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ event_set = "geopolitical-deescalation"
1717
benchmark_symbol = "QQQ"
1818
attack_symbol = "TQQQ"
1919
vix_symbols = ["VIX", "^VIX", "VIXCLS"]
20+
vix3m_symbols = ["VIX3M", "^VIX3M", "VXV", "^VXV"]
2021
credit_pairs = ["HYG:IEF", "LQD:IEF"]
2122
financial_symbols = ["XLF", "KRE"]
2223
rate_symbols = ["IEF", "TLT"]
@@ -47,6 +48,7 @@ event_set = "full"
4748
benchmark_symbol = "SOXX"
4849
attack_symbol = "SOXL"
4950
vix_symbols = ["VIX", "^VIX", "VIXCLS"]
51+
vix3m_symbols = ["VIX3M", "^VIX3M", "VXV", "^VXV"]
5052
credit_pairs = ["HYG:IEF", "LQD:IEF"]
5153
financial_symbols = ["XLF", "KRE"]
5254
rate_symbols = ["IEF", "TLT"]
@@ -93,6 +95,7 @@ external_context = "data/output/macro_risk_governor/input/external_context.csv"
9395
benchmark_symbol = "QQQ"
9496
attack_symbol = "TQQQ"
9597
vix_symbols = ["VIX", "^VIX", "VIXCLS"]
98+
vix3m_symbols = ["VIX3M", "^VIX3M", "VXV", "^VXV"]
9699
credit_pairs = ["HYG:IEF", "LQD:IEF"]
97100

98101
[strategy_plugins.outputs]

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
- `crisis_response_shadow`
2727
负责硬危机防守。`true_crisis` 和泡沫脆弱性触发后,统一插件输出 `risk_off` / `defend`,仓位目标交给策略侧 opt-in 执行。
2828
- `macro_risk_governor`
29-
负责宏观降杠杆。它看价格趋势、实现波动、VIX、信用相对压力和可选金融压力字段,输出 `risk_reduced``risk_off`。Fear & Greed、put/call、safe-haven demand 和五角大楼比萨指数只作为 watch-only 证据,先用于通知和回测观察。
29+
负责宏观降杠杆。它看价格趋势、实现波动、VIX、信用相对压力和可选金融压力字段,输出 `risk_reduced``risk_off`。Fear & Greed、put/call、safe-haven demand、VVIX、SKEW、MOVE、收益率曲线、美元压力、市场宽度、AAII/NAAIM 和五角大楼比萨指数只作为 watch-only 证据,先用于通知和回测观察。
3030
- `taco_rebound_shadow`
3131
负责 TQQQ 事件反弹通知。它输出人工复核通知和本地 veto 线索,不直接提高仓位。
3232

@@ -80,6 +80,12 @@
8080

8181
SOXL/SOXX 不出现在 `market_regime_control` 的策略级消费 registry 中;它通过 `market_regime_notification` 接收通用通知,避免配置误用把通知信号升级成自动调仓。
8282

83+
当前观察指标分层:
84+
85+
- 已可执行打分:价格趋势、63/252 日回撤、实现波动、VIX 水平/尖峰、信用 ETF 相对压力、高收益 OAS、金融压力指数。
86+
- 已接入 watch-only:五角大楼比萨指数、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。
87+
- 未进入自动仓位:所有 watch-only 指标。它们只用于通知、证据归档和后续历史回测。
88+
8389
## 版本管理
8490

8591
当前对外契约是:

src/quant_strategy_plugins/macro_risk_governor_plugin.py

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
DEFAULT_BENCHMARK_SYMBOL = "QQQ"
1919
DEFAULT_ATTACK_SYMBOL = "TQQQ"
2020
DEFAULT_VIX_SYMBOLS = ("VIX", "^VIX", "VIXCLS")
21+
DEFAULT_VIX3M_SYMBOLS = ("VIX3M", "^VIX3M", "VXV", "^VXV")
2122
DEFAULT_CREDIT_PAIRS = (("HYG", "IEF"), ("LQD", "IEF"))
2223
DEFAULT_OUTPUT_DIR = "data/output/macro_risk_governor"
2324
DEFAULT_MAX_PRICE_AGE_DAYS = 4
@@ -179,6 +180,35 @@ def _add_check(
179180
}
180181

181182

183+
def _add_external_watch_check(
184+
checks: dict[str, dict[str, Any]],
185+
external_row: pd.Series,
186+
name: str,
187+
aliases: Sequence[str],
188+
*,
189+
threshold: float,
190+
direction: str,
191+
weight: float = 1.0,
192+
) -> float | None:
193+
value = _external_float(external_row, *aliases)
194+
if direction == "lte":
195+
active = value is not None and value <= float(threshold)
196+
elif direction == "gte":
197+
active = value is not None and value >= float(threshold)
198+
else:
199+
raise ValueError(f"unsupported watch direction: {direction!r}")
200+
_add_check(
201+
checks,
202+
name,
203+
active,
204+
weight=float(weight),
205+
value=value,
206+
threshold=float(threshold),
207+
actionable=False,
208+
)
209+
return value
210+
211+
182212
def _score_checks(checks: Mapping[str, Mapping[str, Any]], *, actionable_only: bool) -> float:
183213
score = 0.0
184214
for check in checks.values():
@@ -238,6 +268,7 @@ def build_macro_risk_governor_signal(
238268
benchmark_symbol: str = DEFAULT_BENCHMARK_SYMBOL,
239269
attack_symbol: str = DEFAULT_ATTACK_SYMBOL,
240270
vix_symbols: Sequence[str] = DEFAULT_VIX_SYMBOLS,
271+
vix3m_symbols: Sequence[str] = DEFAULT_VIX3M_SYMBOLS,
241272
credit_pairs: Sequence[tuple[str, str]] = DEFAULT_CREDIT_PAIRS,
242273
max_price_age_days: int = DEFAULT_MAX_PRICE_AGE_DAYS,
243274
max_external_context_age_days: int = DEFAULT_MAX_EXTERNAL_CONTEXT_AGE_DAYS,
@@ -261,6 +292,21 @@ def build_macro_risk_governor_signal(
261292
fear_greed_extreme_fear_level: float = 25.0,
262293
put_call_watch_level: float = 1.20,
263294
safe_haven_demand_watch_level: float = 1.0,
295+
vix_term_structure_watch_level: float = 1.0,
296+
vvix_watch_level: float = 110.0,
297+
skew_watch_level: float = 150.0,
298+
move_watch_level: float = 130.0,
299+
ig_oas_watch_level: float = 2.0,
300+
ig_oas_delta_threshold: float = 0.5,
301+
funding_stress_watch_level: float = 0.5,
302+
yield_curve_inversion_watch_level: float = -0.50,
303+
dollar_stress_return_threshold: float = 0.03,
304+
pct_above_200d_watch_level: float = 0.35,
305+
pct_above_50d_watch_level: float = 0.30,
306+
new_high_new_low_spread_watch_level: float = -0.10,
307+
advance_decline_drawdown_watch_level: float = -0.10,
308+
aaii_bear_bull_spread_watch_level: float = 0.25,
309+
naaim_exposure_watch_level: float = 40.0,
264310
watch_score_threshold: float = 3.0,
265311
delever_score_threshold: float = 5.0,
266312
crisis_score_threshold: float = 7.0,
@@ -345,6 +391,8 @@ def build_macro_risk_governor_signal(
345391
vix_symbol = _first_available_symbol(close, vix_symbols)
346392
vix_level = None
347393
vix_spike = None
394+
vix3m_level = None
395+
vix_term_structure = None
348396
if vix_symbol is not None:
349397
vix = pd.to_numeric(close[vix_symbol], errors="coerce")
350398
vix_level = _as_float(vix.loc[signal_date])
@@ -355,6 +403,21 @@ def build_macro_risk_governor_signal(
355403
vix_spike = _external_float(external_row, "vix_5d_change", "vix_spike_5d")
356404
if vix_level is not None:
357405
evidence["vix_symbol"] = "external_context"
406+
vix3m_symbol = _first_available_symbol(close, vix3m_symbols)
407+
if vix3m_symbol is not None:
408+
vix3m = pd.to_numeric(close[vix3m_symbol], errors="coerce")
409+
vix3m_level = _as_float(vix3m.loc[signal_date])
410+
else:
411+
vix3m_level = _external_float(external_row, "vix3m", "vix_3m", "vix3m_level", "vxv", "vxv_level")
412+
vix_term_structure = _external_float(
413+
external_row,
414+
"vix_term_structure",
415+
"vix_vix3m_ratio",
416+
"vix3m_ratio",
417+
"vix_to_vix3m",
418+
)
419+
if vix_term_structure is None and vix_level is not None and vix3m_level is not None and vix3m_level > 0:
420+
vix_term_structure = vix_level / vix3m_level
358421
_add_check(
359422
checks,
360423
"vix_watch_level",
@@ -379,7 +442,23 @@ def build_macro_risk_governor_signal(
379442
value=vix_spike,
380443
threshold=float(vix_spike_threshold),
381444
)
382-
evidence["metrics"].update({"vix_level": vix_level, "vix_spike": vix_spike})
445+
_add_check(
446+
checks,
447+
"vix_term_structure_inverted_watch",
448+
vix_term_structure is not None and vix_term_structure >= float(vix_term_structure_watch_level),
449+
weight=1.0,
450+
value=vix_term_structure,
451+
threshold=float(vix_term_structure_watch_level),
452+
actionable=False,
453+
)
454+
evidence["metrics"].update(
455+
{
456+
"vix_level": vix_level,
457+
"vix_spike": vix_spike,
458+
"vix3m_level": vix3m_level,
459+
"vix_term_structure": vix_term_structure,
460+
}
461+
)
383462

384463
credit_returns: dict[str, float | None] = {}
385464
credit_context_available = False
@@ -498,6 +577,175 @@ def build_macro_risk_governor_signal(
498577
threshold=float(safe_haven_demand_watch_level),
499578
actionable=False,
500579
)
580+
vvix = _add_external_watch_check(
581+
checks,
582+
external_row,
583+
"vvix_high_watch",
584+
("vvix", "vvix_index", "cboe_vvix"),
585+
threshold=float(vvix_watch_level),
586+
direction="gte",
587+
)
588+
skew = _add_external_watch_check(
589+
checks,
590+
external_row,
591+
"skew_high_watch",
592+
("skew", "skew_index", "cboe_skew"),
593+
threshold=float(skew_watch_level),
594+
direction="gte",
595+
)
596+
move = _add_external_watch_check(
597+
checks,
598+
external_row,
599+
"move_high_watch",
600+
("move", "move_index", "ice_bofaml_move", "bond_volatility_index"),
601+
threshold=float(move_watch_level),
602+
direction="gte",
603+
)
604+
ig_oas = _add_external_watch_check(
605+
checks,
606+
external_row,
607+
"ig_oas_watch_level",
608+
("ig_oas", "investment_grade_oas", "bamLC0A0CM", "bam_lc0a0cm"),
609+
threshold=float(ig_oas_watch_level),
610+
direction="gte",
611+
weight=1.0,
612+
)
613+
ig_oas_delta = _external_float(external_row, "ig_oas_delta_63d", "investment_grade_oas_delta_63d")
614+
if ig_oas_delta is None:
615+
ig_oas_delta = _external_delta(
616+
external_context,
617+
signal_date,
618+
("ig_oas", "investment_grade_oas", "bamLC0A0CM", "bam_lc0a0cm"),
619+
int(hy_oas_delta_lookback_days),
620+
)
621+
_add_check(
622+
checks,
623+
"ig_oas_widening_watch",
624+
ig_oas_delta is not None and ig_oas_delta >= float(ig_oas_delta_threshold),
625+
weight=1.0,
626+
value=ig_oas_delta,
627+
threshold=float(ig_oas_delta_threshold),
628+
actionable=False,
629+
)
630+
funding_stress = _add_external_watch_check(
631+
checks,
632+
external_row,
633+
"funding_stress_watch",
634+
(
635+
"ted_spread",
636+
"libor_ois_spread",
637+
"sofr_ois_spread",
638+
"fra_ois_spread",
639+
"funding_stress",
640+
"funding_stress_index",
641+
),
642+
threshold=float(funding_stress_watch_level),
643+
direction="gte",
644+
)
645+
yield_curve_10y2y = _external_float(
646+
external_row,
647+
"yield_curve_10y2y",
648+
"10y2y",
649+
"t10y2y",
650+
"us10y2y",
651+
"treasury_10y2y",
652+
)
653+
yield_curve_10y3m = _external_float(
654+
external_row,
655+
"yield_curve_10y3m",
656+
"10y3m",
657+
"t10y3m",
658+
"us10y3m",
659+
"treasury_10y3m",
660+
)
661+
yield_curve_min = min(
662+
(value for value in (yield_curve_10y2y, yield_curve_10y3m) if value is not None),
663+
default=None,
664+
)
665+
_add_check(
666+
checks,
667+
"yield_curve_inversion_watch",
668+
yield_curve_min is not None and yield_curve_min <= float(yield_curve_inversion_watch_level),
669+
weight=1.0,
670+
value=yield_curve_min,
671+
threshold=float(yield_curve_inversion_watch_level),
672+
actionable=False,
673+
)
674+
dollar_return_21d = _add_external_watch_check(
675+
checks,
676+
external_row,
677+
"dollar_stress_watch",
678+
("dxy_return_21d", "dollar_index_return_21d", "usd_return_21d", "dxy_21d_return"),
679+
threshold=float(dollar_stress_return_threshold),
680+
direction="gte",
681+
)
682+
pct_above_200d = _add_external_watch_check(
683+
checks,
684+
external_row,
685+
"market_breadth_pct_above_200d_watch",
686+
(
687+
"pct_above_200d",
688+
"percent_above_200d",
689+
"spx_pct_above_200d",
690+
"nasdaq_100_pct_above_200d",
691+
),
692+
threshold=float(pct_above_200d_watch_level),
693+
direction="lte",
694+
)
695+
pct_above_50d = _add_external_watch_check(
696+
checks,
697+
external_row,
698+
"market_breadth_pct_above_50d_watch",
699+
(
700+
"pct_above_50d",
701+
"percent_above_50d",
702+
"spx_pct_above_50d",
703+
"nasdaq_100_pct_above_50d",
704+
),
705+
threshold=float(pct_above_50d_watch_level),
706+
direction="lte",
707+
)
708+
new_high_new_low_spread = _add_external_watch_check(
709+
checks,
710+
external_row,
711+
"new_high_new_low_spread_watch",
712+
(
713+
"new_high_new_low_spread",
714+
"nh_nl_spread",
715+
"new_highs_new_lows_spread",
716+
"new_high_new_low_ratio",
717+
),
718+
threshold=float(new_high_new_low_spread_watch_level),
719+
direction="lte",
720+
)
721+
advance_decline_drawdown = _add_external_watch_check(
722+
checks,
723+
external_row,
724+
"advance_decline_drawdown_watch",
725+
(
726+
"advance_decline_drawdown",
727+
"ad_line_drawdown",
728+
"advance_decline_line_drawdown",
729+
),
730+
threshold=float(advance_decline_drawdown_watch_level),
731+
direction="lte",
732+
)
733+
aaii_bear_bull_spread = _add_external_watch_check(
734+
checks,
735+
external_row,
736+
"aaii_bear_bull_spread_watch",
737+
("aaii_bear_bull_spread", "aaii_bears_minus_bulls", "aaii_bearish_bullish_spread"),
738+
threshold=float(aaii_bear_bull_spread_watch_level),
739+
direction="gte",
740+
)
741+
naaim_exposure = _add_external_watch_check(
742+
checks,
743+
external_row,
744+
"naaim_exposure_low_watch",
745+
("naaim_exposure", "naaim_exposure_index", "naaim_manager_exposure"),
746+
threshold=float(naaim_exposure_watch_level),
747+
direction="lte",
748+
)
501749
realized_vol_confirmed_for_action = None
502750
realized_vol_check = checks.get("benchmark_realized_volatility_high")
503751
if realized_vol_check is not None:
@@ -529,6 +777,22 @@ def build_macro_risk_governor_signal(
529777
"fear_greed_index": fear_greed_index,
530778
"put_call_ratio": put_call_ratio,
531779
"safe_haven_demand": safe_haven_demand,
780+
"vvix": vvix,
781+
"skew": skew,
782+
"move": move,
783+
"ig_oas": ig_oas,
784+
"ig_oas_delta_63d": ig_oas_delta,
785+
"funding_stress": funding_stress,
786+
"yield_curve_10y2y": yield_curve_10y2y,
787+
"yield_curve_10y3m": yield_curve_10y3m,
788+
"yield_curve_min": yield_curve_min,
789+
"dollar_return_21d": dollar_return_21d,
790+
"pct_above_200d": pct_above_200d,
791+
"pct_above_50d": pct_above_50d,
792+
"new_high_new_low_spread": new_high_new_low_spread,
793+
"advance_decline_drawdown": advance_decline_drawdown,
794+
"aaii_bear_bull_spread": aaii_bear_bull_spread,
795+
"naaim_exposure": naaim_exposure,
532796
"benchmark_realized_volatility_requires_confirmation": bool(realized_vol_requires_confirmation),
533797
"benchmark_realized_volatility_confirmed_for_action": realized_vol_confirmed_for_action,
534798
}

0 commit comments

Comments
 (0)