2828 }
2929)
3030TACO_REBOUND_SHADOW_SUPPORTED_STRATEGIES = frozenset ({"tqqq_growth_income" })
31+ _DEFAULT_STRATEGY_PLUGIN_ALERT_GUIDANCE : Mapping [tuple [str , str , str ], str ] = {
32+ (
33+ PLUGIN_CRISIS_RESPONSE_SHADOW ,
34+ "true_crisis" ,
35+ "defend" ,
36+ ): (
37+ "Consider reducing leveraged exposure, moving to defensive or cash-like positions, "
38+ "and pausing new risk additions until the signal de-escalates."
39+ ),
40+ (
41+ PLUGIN_CRISIS_RESPONSE_SHADOW ,
42+ "no_action" ,
43+ "blocked" ,
44+ ): (
45+ "The crisis route was blocked by a guard; review data freshness and context before "
46+ "acting on the signal."
47+ ),
48+ (
49+ PLUGIN_TACO_REBOUND_SHADOW ,
50+ "taco_rebound" ,
51+ "notify_manual_review" ,
52+ ): (
53+ "Manual review only: consider a small, pre-sized probe or staged entry with a "
54+ "predefined invalidation level; avoid full-size deployment from this alert alone."
55+ ),
56+ }
57+ _DEFAULT_STRATEGY_PLUGIN_ALERT_SCOPE_NOTE = (
58+ "Manual review notice only; the plugin does not place orders or change allocations."
59+ )
3160
3261
3362@dataclass (frozen = True )
@@ -445,6 +474,64 @@ def should_alert_strategy_plugin_signal(signal: StrategyPluginSignal) -> bool:
445474 )
446475
447476
477+ def build_strategy_plugin_alert_guidance (
478+ signal : StrategyPluginSignal ,
479+ * ,
480+ translator : Callable [..., str ] | None = None ,
481+ ) -> str | None :
482+ plugin = _normalize_strategy_plugin_field (getattr (signal , "plugin" , None ))
483+ route = _normalize_strategy_plugin_field (getattr (signal , "canonical_route" , None ))
484+ action = _normalize_strategy_plugin_field (getattr (signal , "suggested_action" , None ))
485+ translated = _translate_first (
486+ translator ,
487+ (
488+ f"strategy_plugin_guidance_{ plugin } _{ route } _{ action } " ,
489+ f"strategy_plugin_guidance_{ plugin } _{ route } " ,
490+ f"strategy_plugin_guidance_{ plugin } _{ action } " ,
491+ f"strategy_plugin_guidance_{ plugin } " ,
492+ f"strategy_plugin_guidance_{ route } _{ action } " ,
493+ f"strategy_plugin_guidance_{ action } " ,
494+ ),
495+ )
496+ if translated :
497+ return translated
498+ return _DEFAULT_STRATEGY_PLUGIN_ALERT_GUIDANCE .get ((plugin , route , action ))
499+
500+
501+ def build_strategy_plugin_alert_scope_note (
502+ signal : StrategyPluginSignal ,
503+ * ,
504+ translator : Callable [..., str ] | None = None ,
505+ ) -> str | None :
506+ controls = getattr (signal , "execution_controls" , {}) or {}
507+ if not isinstance (controls , Mapping ):
508+ controls = {}
509+ notification_profile = str (controls .get ("notification_profile" ) or "" ).strip ().lower ()
510+ if notification_profile != "shadow_only" and any (
511+ _as_bool (controls .get (field ), default = False )
512+ for field in (
513+ "broker_order_allowed" ,
514+ "repository_broker_write_allowed" ,
515+ "live_allocation_mutation_allowed" ,
516+ "repository_allocation_mutation_allowed" ,
517+ "allocation_recommendation_allowed" ,
518+ "position_sizing_allowed" ,
519+ "selection_allowed" ,
520+ )
521+ ):
522+ return None
523+ return (
524+ _translate_first (
525+ translator ,
526+ (
527+ f"strategy_plugin_alert_scope_{ _normalize_strategy_plugin_field (getattr (signal , 'plugin' , None ))} " ,
528+ "strategy_plugin_alert_scope" ,
529+ ),
530+ )
531+ or _DEFAULT_STRATEGY_PLUGIN_ALERT_SCOPE_NOTE
532+ )
533+
534+
448535def build_strategy_plugin_alert_key (
449536 signal : StrategyPluginSignal ,
450537 * ,
@@ -504,6 +591,8 @@ def build_strategy_plugin_alert_messages(
504591 translated_route = translate_strategy_plugin_value ("route" , route , translator = translator )
505592 translated_action = translate_strategy_plugin_value ("action" , action , translator = translator )
506593 strategy = str (strategy_label or getattr (signal , "strategy" , None ) or "" ).strip () or "unknown"
594+ guidance = build_strategy_plugin_alert_guidance (signal , translator = translator )
595+ scope_note = build_strategy_plugin_alert_scope_note (signal , translator = translator )
507596 subject = _translate (
508597 translator ,
509598 "strategy_plugin_alert_subject" ,
@@ -567,6 +656,24 @@ def build_strategy_plugin_alert_messages(
567656 ),
568657 ]
569658 )
659+ if guidance :
660+ body_lines .append (
661+ _translate (
662+ translator ,
663+ "strategy_plugin_alert_guidance" ,
664+ fallback = "Manual guidance: {guidance}" ,
665+ guidance = guidance ,
666+ )
667+ )
668+ if scope_note :
669+ body_lines .append (
670+ _translate (
671+ translator ,
672+ "strategy_plugin_alert_scope_note" ,
673+ fallback = "Scope: {scope_note}" ,
674+ scope_note = scope_note ,
675+ )
676+ )
570677 metadata = {
571678 "strategy" : getattr (signal , "strategy" , None ),
572679 "strategy_label" : strategy ,
@@ -577,6 +684,8 @@ def build_strategy_plugin_alert_messages(
577684 "suggested_action" : getattr (signal , "suggested_action" , None ),
578685 "would_trade_if_enabled" : bool (getattr (signal , "would_trade_if_enabled" , False )),
579686 "context_label" : context or None ,
687+ "guidance" : guidance ,
688+ "scope_note" : scope_note ,
580689 }
581690 messages .append (
582691 StrategyPluginAlertMessage (
@@ -687,6 +796,21 @@ def _translate(
687796 return translated if translated != key else fallback .format (** kwargs )
688797
689798
799+ def _translate_first (
800+ translator : Callable [..., str ] | None ,
801+ keys : Sequence [str ],
802+ ) -> str | None :
803+ if translator is None :
804+ return None
805+ for key in keys :
806+ translated = translator (key )
807+ if translated != key :
808+ text = str (translated ).strip ()
809+ if text :
810+ return text
811+ return None
812+
813+
690814def _required_string (value : Any , * , field_name : str ) -> str :
691815 text = _optional_string (value )
692816 if text is None :
0 commit comments