1818DEFAULT_BENCHMARK_SYMBOL = "QQQ"
1919DEFAULT_ATTACK_SYMBOL = "TQQQ"
2020DEFAULT_VIX_SYMBOLS = ("VIX" , "^VIX" , "VIXCLS" )
21+ DEFAULT_VIX3M_SYMBOLS = ("VIX3M" , "^VIX3M" , "VXV" , "^VXV" )
2122DEFAULT_CREDIT_PAIRS = (("HYG" , "IEF" ), ("LQD" , "IEF" ))
2223DEFAULT_OUTPUT_DIR = "data/output/macro_risk_governor"
2324DEFAULT_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+
182212def _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