3434_TQQQ_ETF_ONLY_FACTORS = {"TQQQ" : 3 , "BOXX" : 1 }
3535_TQQQ_ETF_ONLY_NOMINAL_CAPS = {"TQQQ" : 0.15 , "BOXX" : 0.50 }
3636_TQQQ_ETF_ONLY_EFFECTIVE_CAPS = {"TQQQ" : 0.45 , "BOXX" : 0.50 }
37+ _GLOBAL_ETF_RESEARCH_MANDATE = "global_etf_rotation_etf_only_research_v1"
38+ _GLOBAL_ETF_STRATEGY_PROFILE = (
39+ "global_etf_rotation_etf_only_single_strategy_research_v1"
40+ )
41+ _GLOBAL_ETF_ACCOUNT_MODE = "single_strategy_research_v1"
42+ _GLOBAL_ETF_ALLOWED_ASSETS = (
43+ "EWY" ,
44+ "EWT" ,
45+ "INDA" ,
46+ "FXI" ,
47+ "EWJ" ,
48+ "VGK" ,
49+ "VOO" ,
50+ "XLK" ,
51+ "SMH" ,
52+ "GLD" ,
53+ "SLV" ,
54+ "USO" ,
55+ "DBA" ,
56+ "XLE" ,
57+ "XLF" ,
58+ "ITA" ,
59+ "XLP" ,
60+ "XLU" ,
61+ "XLV" ,
62+ "IHI" ,
63+ "VNQ" ,
64+ "KRE" ,
65+ "BIL" ,
66+ )
67+ _GLOBAL_ETF_FACTORS = {symbol : 1 for symbol in _GLOBAL_ETF_ALLOWED_ASSETS }
68+ _GLOBAL_ETF_CAPS = {symbol : 0.50 for symbol in _GLOBAL_ETF_ALLOWED_ASSETS }
69+ _GLOBAL_ETF_STOP_FILL_POLICY = "gap_aware_min_open_or_stop_v1"
3770_BOOTSTRAP_EFFECTIVE_EXPOSURE_CAP = 0.50
3871_BOOTSTRAP_NOMINAL_CAPS = {1 : 0.50 , 2 : 0.25 , 3 : 0.15 }
3972_ASSESSMENT_CONTRACT_VERSION = "qsl.risk_gate_assessment.v2"
@@ -297,6 +330,97 @@ def _exact_tqqq_mandate_errors(
297330 return {"invalid_tqqq_research_mandate" } if invalid else set ()
298331
299332
333+ def _exact_global_etf_mandate_errors (
334+ mandate_provenance : Mapping [str , Any ],
335+ * ,
336+ effective_at : datetime ,
337+ expires_at : datetime ,
338+ ) -> set [str ]:
339+ if mandate_provenance .get ("mandate_id" ) != _GLOBAL_ETF_RESEARCH_MANDATE :
340+ return set ()
341+ required = (
342+ "loss_budget_equity_reference" ,
343+ "product_effective_caps" ,
344+ "max_nonzero_assets" ,
345+ "broker_margin_factor" ,
346+ "margin_stacking" ,
347+ "borrowing" ,
348+ "shorting" ,
349+ "income_sleeve_enabled" ,
350+ "option_overlay_enabled" ,
351+ "ai_overlay_enabled" ,
352+ "market_regime_overlay_enabled" ,
353+ "precommitted_executable_stop_distance" ,
354+ "stop_fill_policy" ,
355+ "max_consecutive_completed_losing_exits" ,
356+ )
357+ allowed_assets = mandate_provenance .get ("allowed_nonzero_assets" )
358+ factors = mandate_provenance .get ("product_leverage_factors" )
359+ exact_factors = (
360+ isinstance (factors , Mapping )
361+ and set (factors ) == set (_GLOBAL_ETF_FACTORS )
362+ and all (
363+ not isinstance (factors [symbol ], bool )
364+ and isinstance (factors [symbol ], int )
365+ and factors [symbol ] == expected
366+ for symbol , expected in _GLOBAL_ETF_FACTORS .items ()
367+ )
368+ )
369+ invalid = (
370+ any (field not in mandate_provenance for field in required )
371+ or mandate_provenance .get ("mandate_version" ) != "v1"
372+ or mandate_provenance .get ("authority_scope" ) != "RESEARCH_ONLY"
373+ or mandate_provenance .get ("strategy_profile" )
374+ != _GLOBAL_ETF_STRATEGY_PROFILE
375+ or mandate_provenance .get ("account_mode" ) != _GLOBAL_ETF_ACCOUNT_MODE
376+ or _finite_number (mandate_provenance .get ("max_snapshot_age_seconds" ))
377+ != 300.0
378+ or _finite_number (mandate_provenance .get ("effective_exposure_cap" )) != 0.50
379+ or _finite_number (mandate_provenance .get ("loss_budget" )) != 0.01
380+ or mandate_provenance .get ("loss_budget_equity_reference" )
381+ != "completed_session_equity"
382+ or not _exact_numeric_mapping (
383+ mandate_provenance .get ("product_caps" ),
384+ _GLOBAL_ETF_CAPS ,
385+ )
386+ or not _exact_numeric_mapping (
387+ mandate_provenance .get ("nominal_caps" ),
388+ _GLOBAL_ETF_CAPS ,
389+ )
390+ or not _exact_numeric_mapping (
391+ mandate_provenance .get ("product_effective_caps" ),
392+ _GLOBAL_ETF_CAPS ,
393+ )
394+ or not exact_factors
395+ or not isinstance (allowed_assets , (list , tuple ))
396+ or tuple (allowed_assets ) != _GLOBAL_ETF_ALLOWED_ASSETS
397+ or isinstance (mandate_provenance .get ("max_nonzero_assets" ), bool )
398+ or mandate_provenance .get ("max_nonzero_assets" ) != 2
399+ or isinstance (mandate_provenance .get ("broker_margin_factor" ), bool )
400+ or mandate_provenance .get ("broker_margin_factor" ) != 1
401+ or mandate_provenance .get ("margin_stacking" ) is not False
402+ or mandate_provenance .get ("borrowing" ) is not False
403+ or mandate_provenance .get ("shorting" ) is not False
404+ or mandate_provenance .get ("income_sleeve_enabled" ) is not False
405+ or mandate_provenance .get ("option_overlay_enabled" ) is not False
406+ or mandate_provenance .get ("ai_overlay_enabled" ) is not False
407+ or mandate_provenance .get ("market_regime_overlay_enabled" ) is not False
408+ or _finite_number (
409+ mandate_provenance .get ("precommitted_executable_stop_distance" )
410+ )
411+ != 0.05
412+ or mandate_provenance .get ("stop_fill_policy" )
413+ != _GLOBAL_ETF_STOP_FILL_POLICY
414+ or isinstance (
415+ mandate_provenance .get ("max_consecutive_completed_losing_exits" ),
416+ bool ,
417+ )
418+ or mandate_provenance .get ("max_consecutive_completed_losing_exits" ) != 5
419+ or (expires_at - effective_at ).total_seconds () > 90 * 24 * 60 * 60
420+ )
421+ return {"invalid_global_etf_research_mandate" } if invalid else set ()
422+
423+
300424def _mandate_fields (
301425 mandate_provenance : Mapping [str , Any ] | None ,
302426 * ,
@@ -392,11 +516,18 @@ def _mandate_fields(
392516 return {}, {"invalid_mandate" }
393517 if effective_at > now or expires_at < now or expires_at <= effective_at :
394518 return {}, {"expired_mandate" }
395- exact_mandate_errors = _exact_tqqq_mandate_errors (
396- mandate_provenance ,
397- effective_at = effective_at ,
398- expires_at = expires_at ,
399- )
519+ exact_mandate_errors = set ()
520+ for validator in (
521+ _exact_tqqq_mandate_errors ,
522+ _exact_global_etf_mandate_errors ,
523+ ):
524+ exact_mandate_errors .update (
525+ validator (
526+ mandate_provenance ,
527+ effective_at = effective_at ,
528+ expires_at = expires_at ,
529+ )
530+ )
400531 if exact_mandate_errors :
401532 return {}, exact_mandate_errors
402533 factors = mandate_provenance .get ("product_leverage_factors" , {})
@@ -553,6 +684,13 @@ def _risk_control_fields(
553684 "drawdown_scalar" : None ,
554685 "risk_control_state_digest_sha256" : None ,
555686 }
687+ if mandate .get ("mandate_id" ) == _GLOBAL_ETF_RESEARCH_MANDATE :
688+ return _global_etf_risk_control_fields (
689+ risk_control_state ,
690+ mandate = mandate ,
691+ now = now ,
692+ active_positions = active_positions ,
693+ )
556694 if mandate .get ("mandate_id" ) != _TQQQ_ETF_ONLY_RESEARCH_MANDATE :
557695 return empty , set ()
558696 if not isinstance (risk_control_state , Mapping ):
@@ -676,6 +814,165 @@ def _risk_control_fields(
676814 }, errors
677815
678816
817+ def _global_etf_risk_control_fields (
818+ risk_control_state : Mapping [str , Any ] | None ,
819+ * ,
820+ mandate : Mapping [str , Any ],
821+ now : datetime ,
822+ active_positions : list [tuple [str , float ]],
823+ ) -> tuple [dict [str , Any ], set [str ]]:
824+ empty = {
825+ "stop_loss_distance" : None ,
826+ "stop_intent_ready" : None ,
827+ "strategy_breaker_triggered" : None ,
828+ "account_breaker_triggered" : None ,
829+ "account_drawdown_fraction" : None ,
830+ "drawdown_scalar" : None ,
831+ "risk_control_state_digest_sha256" : None ,
832+ }
833+ if not isinstance (risk_control_state , Mapping ):
834+ return empty , {"missing_risk_control_state" }
835+
836+ required = (
837+ "as_of" ,
838+ "mandate_id" ,
839+ "candidate_identity_sha256" ,
840+ "stop_loss_distance" ,
841+ "stop_fill_policy" ,
842+ "position_stop_states" ,
843+ "consecutive_completed_losing_exits" ,
844+ "account_drawdown_fraction" ,
845+ "drawdown_scalar" ,
846+ )
847+ errors : set [str ] = set ()
848+ if any (field not in risk_control_state for field in required ):
849+ errors .add ("invalid_risk_control_state" )
850+
851+ as_of = _parse_utc_timestamp (risk_control_state .get ("as_of" ))
852+ stop_loss_distance = _finite_number (risk_control_state .get ("stop_loss_distance" ))
853+ account_drawdown = _finite_number (
854+ risk_control_state .get ("account_drawdown_fraction" )
855+ )
856+ drawdown_scalar = _finite_number (risk_control_state .get ("drawdown_scalar" ))
857+ raw_losses = risk_control_state .get ("consecutive_completed_losing_exits" )
858+ losses = (
859+ raw_losses
860+ if not isinstance (raw_losses , bool )
861+ and isinstance (raw_losses , int )
862+ and raw_losses >= 0
863+ else None
864+ )
865+ max_age = _finite_number (mandate .get ("max_snapshot_age_seconds" ))
866+ if as_of is None or max_age is None :
867+ errors .add ("invalid_risk_control_state" )
868+ elif (age := (now - as_of ).total_seconds ()) < 0.0 or age > max_age :
869+ errors .add ("stale_risk_control_state" )
870+ if risk_control_state .get ("mandate_id" ) != mandate .get ("mandate_id" ):
871+ errors .add ("risk_control_mandate_mismatch" )
872+ if _sha256 (risk_control_state .get ("candidate_identity_sha256" )) != mandate .get (
873+ "candidate_identity_sha256"
874+ ):
875+ errors .add ("risk_control_candidate_mismatch" )
876+ if stop_loss_distance != 0.05 :
877+ errors .add ("invalid_stop_loss_distance" )
878+ if risk_control_state .get ("stop_fill_policy" ) != _GLOBAL_ETF_STOP_FILL_POLICY :
879+ errors .add ("invalid_stop_fill_policy" )
880+ if account_drawdown is None or not 0.0 <= account_drawdown <= 1.0 :
881+ errors .add ("invalid_account_drawdown" )
882+ if losses is None :
883+ errors .add ("invalid_strategy_breaker_state" )
884+
885+ expected_scalar : float | None = None
886+ if account_drawdown is not None and 0.0 <= account_drawdown <= 1.0 :
887+ if account_drawdown <= 0.05 :
888+ expected_scalar = 1.0
889+ elif account_drawdown <= 0.10 :
890+ expected_scalar = 0.50
891+ else :
892+ expected_scalar = 0.0
893+ if drawdown_scalar != expected_scalar :
894+ errors .add ("drawdown_scalar_mismatch" )
895+ elif drawdown_scalar is None :
896+ errors .add ("invalid_drawdown_scalar" )
897+
898+ active_symbols = [symbol for symbol , _weight in active_positions ]
899+ if len (active_symbols ) != len (set (active_symbols )):
900+ errors .add ("duplicate_active_symbol" )
901+ raw_stop_states = risk_control_state .get ("position_stop_states" )
902+ normalized_stop_states : dict [str , dict [str , Any ]] = {}
903+ all_stop_intents_ready = True
904+ if not isinstance (raw_stop_states , Mapping ):
905+ errors .add ("invalid_position_stop_states" )
906+ all_stop_intents_ready = False
907+ elif set (raw_stop_states ) != set (active_symbols ):
908+ errors .add ("stop_state_positions_mismatch" )
909+ all_stop_intents_ready = False
910+ else :
911+ expected_stop_fields = {
912+ "stop_intent_ready" ,
913+ "entry_fill_identity_sha256" ,
914+ "stop_entry_fill_identity_sha256" ,
915+ }
916+ for symbol in sorted (set (active_symbols )):
917+ raw_stop = raw_stop_states .get (symbol )
918+ if not isinstance (raw_stop , Mapping ) or set (raw_stop ) != expected_stop_fields :
919+ errors .add ("invalid_position_stop_state" )
920+ all_stop_intents_ready = False
921+ continue
922+ ready = raw_stop .get ("stop_intent_ready" )
923+ entry_fill_identity = _sha256 (
924+ raw_stop .get ("entry_fill_identity_sha256" )
925+ )
926+ stop_entry_fill_identity = _sha256 (
927+ raw_stop .get ("stop_entry_fill_identity_sha256" )
928+ )
929+ if ready is not True :
930+ errors .add ("stop_intent_not_ready" )
931+ all_stop_intents_ready = False
932+ if (
933+ entry_fill_identity is None
934+ or stop_entry_fill_identity is None
935+ or entry_fill_identity != stop_entry_fill_identity
936+ ):
937+ errors .add ("stop_entry_fill_identity_mismatch" )
938+ all_stop_intents_ready = False
939+ normalized_stop_states [symbol ] = {
940+ "stop_intent_ready" : ready if isinstance (ready , bool ) else None ,
941+ "entry_fill_identity_sha256" : entry_fill_identity ,
942+ "stop_entry_fill_identity_sha256" : stop_entry_fill_identity ,
943+ }
944+
945+ strategy_breaker = losses is not None and losses >= 5
946+ account_breaker = account_drawdown is not None and account_drawdown > 0.10
947+ if strategy_breaker :
948+ errors .add ("strategy_breaker_triggered" )
949+ if account_breaker :
950+ errors .add ("account_breaker_triggered" )
951+
952+ payload = {
953+ "as_of" : _utc_timestamp (as_of ) if as_of is not None else None ,
954+ "mandate_id" : risk_control_state .get ("mandate_id" ),
955+ "candidate_identity_sha256" : _sha256 (
956+ risk_control_state .get ("candidate_identity_sha256" )
957+ ),
958+ "stop_loss_distance" : stop_loss_distance ,
959+ "stop_fill_policy" : risk_control_state .get ("stop_fill_policy" ),
960+ "position_stop_states" : normalized_stop_states ,
961+ "consecutive_completed_losing_exits" : losses ,
962+ "account_drawdown_fraction" : account_drawdown ,
963+ "drawdown_scalar" : drawdown_scalar ,
964+ }
965+ return {
966+ "stop_loss_distance" : stop_loss_distance ,
967+ "stop_intent_ready" : all_stop_intents_ready ,
968+ "strategy_breaker_triggered" : strategy_breaker ,
969+ "account_breaker_triggered" : account_breaker ,
970+ "account_drawdown_fraction" : account_drawdown ,
971+ "drawdown_scalar" : drawdown_scalar ,
972+ "risk_control_state_digest_sha256" : _canonical_digest (payload ),
973+ }, errors
974+
975+
679976def assess_with_evidence (
680977 decision : StrategyDecision ,
681978 portfolio_snapshot : Any ,
@@ -733,8 +1030,13 @@ def assess_with_evidence(
7331030 weighted_exposure = 0.0
7341031 if mandate_provenance is None and len (active_positions ) > 1 :
7351032 reason_codes .add ("fallback_position_count" )
1033+ mandate_id = mandate .get ("mandate_id" )
1034+ exact_research_mandate = mandate_id in {
1035+ _TQQQ_ETF_ONLY_RESEARCH_MANDATE ,
1036+ _GLOBAL_ETF_RESEARCH_MANDATE ,
1037+ }
7361038 if (
737- mandate . get ( "mandate_id" ) == _TQQQ_ETF_ONLY_RESEARCH_MANDATE
1039+ exact_research_mandate
7381040 and len (active_positions ) > mandate ["max_nonzero_assets" ]
7391041 ):
7401042 reason_codes .add ("single_strategy_position_count" )
@@ -777,6 +1079,22 @@ def assess_with_evidence(
7771079 ):
7781080 reason_codes .add ("risk_budget_exposure_cap" )
7791081 weighted_exposure += weight * factor
1082+ if mandate_id == _GLOBAL_ETF_RESEARCH_MANDATE :
1083+ stop_distance = control_fields ["stop_loss_distance" ]
1084+ drawdown_scalar = control_fields ["drawdown_scalar" ]
1085+ loss_budget = mandate .get ("loss_budget" )
1086+ modeled_stop_loss = (
1087+ sum (weight for _symbol , weight in active_positions ) * stop_distance
1088+ if stop_distance is not None
1089+ else None
1090+ )
1091+ if (
1092+ modeled_stop_loss is not None
1093+ and drawdown_scalar is not None
1094+ and loss_budget is not None
1095+ and modeled_stop_loss > loss_budget * drawdown_scalar + 1e-9
1096+ ):
1097+ reason_codes .add ("risk_budget_exposure_cap" )
7801098 target_weights : dict [str , float ] = {}
7811099 for symbol , weight in active_positions :
7821100 target_weights [symbol ] = target_weights .get (symbol , 0.0 ) + weight
@@ -788,9 +1106,7 @@ def assess_with_evidence(
7881106 product_leverage_factors = factors ,
7891107 effective_exposure_cap = cap ,
7901108 observed_effective_exposure = observed ,
791- cash_only = (
792- mandate .get ("mandate_id" ) == _TQQQ_ETF_ONLY_RESEARCH_MANDATE
793- ),
1109+ cash_only = exact_research_mandate ,
7941110 )
7951111 if not valid_normalization :
7961112 reason_codes .add ("invalid_reduce_only_normalization" )
0 commit comments