@@ -817,6 +817,290 @@ def _payload_should_notify(payload: Mapping[str, Any], route: str) -> bool:
817817 return route != "no_action"
818818
819819
820+ def _as_float_or_none (value : Any ) -> float | None :
821+ try :
822+ result = float (value )
823+ except (TypeError , ValueError ):
824+ return None
825+ return result if pd .notna (result ) else None
826+
827+
828+ def _format_number (value : Any , * , digits : int = 2 ) -> str :
829+ number = _as_float_or_none (value )
830+ return "n/a" if number is None else f"{ number :.{digits }f} "
831+
832+
833+ def _format_pct (value : Any , * , digits : int = 1 , signed : bool = False ) -> str :
834+ number = _as_float_or_none (value )
835+ if number is None :
836+ return "n/a"
837+ sign = "+" if signed and number > 0 else ""
838+ return f"{ sign } { number * 100 :.{digits }f} %"
839+
840+
841+ def _component_payload (payload : Mapping [str , Any ], component : str ) -> Mapping [str , Any ]:
842+ components = _nested_mapping (payload , "component_signals" )
843+ value = components .get (component )
844+ return value if isinstance (value , Mapping ) and value .get ("available" , True ) else {}
845+
846+
847+ def _active_panic_payload (payload : Mapping [str , Any ], plugin : str ) -> Mapping [str , Any ]:
848+ if plugin == PLUGIN_PANIC_REVERSAL_SHADOW or "reversal_confirmation" in payload :
849+ return payload
850+ component = _component_payload (payload , "panic_reversal" )
851+ if not component :
852+ return {}
853+ if _as_bool (component .get ("manual_review_required" ), default = False ) or _as_bool (
854+ component .get ("panic_reversal_context_active" ),
855+ default = False ,
856+ ):
857+ return component
858+ return {}
859+
860+
861+ def _active_taco_payload (payload : Mapping [str , Any ], plugin : str ) -> Mapping [str , Any ]:
862+ if plugin == PLUGIN_TACO_REBOUND_SHADOW or "rebound_confirmation" in payload :
863+ return payload
864+ component = _component_payload (payload , "taco" )
865+ if not component :
866+ return {}
867+ if _as_bool (component .get ("manual_review_required" ), default = False ) or _as_bool (
868+ component .get ("rebound_context_active" ),
869+ default = False ,
870+ ):
871+ return component
872+ return {}
873+
874+
875+ def _review_attack_symbol (* sources : Mapping [str , Any ]) -> str :
876+ for source in sources :
877+ for container_key in ("metrics" , "rebound_confirmation" ):
878+ container = source .get (container_key )
879+ if isinstance (container , Mapping ):
880+ symbol = str (container .get ("attack_symbol" ) or "" ).strip ().upper ()
881+ if symbol :
882+ return symbol
883+ return ""
884+
885+
886+ def _manual_review_source_title (
887+ * ,
888+ panic_payload : Mapping [str , Any ],
889+ taco_payload : Mapping [str , Any ],
890+ locale : str ,
891+ ) -> str :
892+ panic_active = bool (panic_payload )
893+ taco_active = bool (taco_payload )
894+ if locale == "zh-CN" :
895+ if panic_active and taco_active :
896+ return "事件缓和 + VIX 恐慌反转共振"
897+ if panic_active :
898+ return "VIX 恐慌反转"
899+ if taco_active :
900+ return "TACO 事件反弹"
901+ return "机会观察"
902+ if panic_active and taco_active :
903+ return "event de-escalation + VIX panic reversal"
904+ if panic_active :
905+ return "VIX panic reversal"
906+ if taco_active :
907+ return "TACO event rebound"
908+ return "opportunity watch"
909+
910+
911+ def _append_panic_review_lines (lines : list [str ], panic_payload : Mapping [str , Any ], * , locale : str ) -> None :
912+ metrics = _nested_mapping (panic_payload , "metrics" )
913+ confirmation = _nested_mapping (panic_payload , "reversal_confirmation" )
914+ thresholds = _nested_mapping (confirmation , "thresholds" )
915+ benchmark = str (metrics .get ("benchmark_symbol" ) or "benchmark" ).upper ()
916+ attack = str (metrics .get ("attack_symbol" ) or "attack" ).upper ()
917+ if locale == "zh-CN" :
918+ lines .extend (
919+ [
920+ "触发原因:" ,
921+ (
922+ "- VIX 曾达到恐慌区间:"
923+ f"{ int (_as_float_or_none (thresholds .get ('vix_high_lookback_days' )) or 5 )} 日高点 "
924+ f"{ _format_number (metrics .get ('vix_lookback_high' ))} ,阈值 "
925+ f"{ _format_number (thresholds .get ('min_vix_high' ))} "
926+ ),
927+ (
928+ "- VIX 已从高点回落:"
929+ f"当前 { _format_number (metrics .get ('vix' ))} ,较高点回落 "
930+ f"{ _format_pct (metrics .get ('vix_pullback_from_high' ))} "
931+ ),
932+ (
933+ "- VIX 继续下降:"
934+ f"前值 { _format_number (metrics .get ('vix_previous' ))} ,当前 { _format_number (metrics .get ('vix' ))} "
935+ ),
936+ (
937+ "- VIX/VIX3M = "
938+ f"{ _format_number (metrics .get ('vix_vix3m_ratio' ))} ,用于确认恐慌结构仍可观测"
939+ ),
940+ (
941+ f"- { benchmark } 3 日收益 { _format_pct (metrics .get ('benchmark_3d_return' ), signed = True )} ,"
942+ f"从近 5 日低点反弹 { _format_pct (metrics .get ('benchmark_rebound_from_recent_low' ), signed = True )} "
943+ ),
944+ f"- { attack } 从近 5 日低点反弹 { _format_pct (metrics .get ('attack_rebound_from_recent_low' ), signed = True )} " ,
945+ ]
946+ )
947+ return
948+ lines .extend (
949+ [
950+ "Trigger evidence:" ,
951+ (
952+ "- VIX reached panic territory: "
953+ f"{ int (_as_float_or_none (thresholds .get ('vix_high_lookback_days' )) or 5 )} -day high "
954+ f"{ _format_number (metrics .get ('vix_lookback_high' ))} ; threshold "
955+ f"{ _format_number (thresholds .get ('min_vix_high' ))} "
956+ ),
957+ (
958+ "- VIX has pulled back from the high: "
959+ f"current { _format_number (metrics .get ('vix' ))} , pullback "
960+ f"{ _format_pct (metrics .get ('vix_pullback_from_high' ))} "
961+ ),
962+ (
963+ "- VIX is still falling: "
964+ f"previous { _format_number (metrics .get ('vix_previous' ))} , current { _format_number (metrics .get ('vix' ))} "
965+ ),
966+ f"- VIX/VIX3M = { _format_number (metrics .get ('vix_vix3m_ratio' ))} " ,
967+ (
968+ f"- { benchmark } 3-day return { _format_pct (metrics .get ('benchmark_3d_return' ), signed = True )} ; "
969+ f"rebound from recent low { _format_pct (metrics .get ('benchmark_rebound_from_recent_low' ), signed = True )} "
970+ ),
971+ f"- { attack } rebound from recent low { _format_pct (metrics .get ('attack_rebound_from_recent_low' ), signed = True )} " ,
972+ ]
973+ )
974+
975+
976+ def _append_taco_review_lines (lines : list [str ], taco_payload : Mapping [str , Any ], * , locale : str ) -> None :
977+ event = _nested_mapping (taco_payload , "selected_event" )
978+ confirmation = _nested_mapping (taco_payload , "rebound_confirmation" )
979+ benchmark = str (confirmation .get ("benchmark_symbol" ) or "benchmark" ).upper ()
980+ attack = str (confirmation .get ("attack_symbol" ) or "attack" ).upper ()
981+ if locale == "zh-CN" :
982+ lines .extend (
983+ [
984+ "事件:" ,
985+ f"- 类型:{ event .get ('kind' ) or 'n/a' } / 区域:{ event .get ('region' ) or 'n/a' } " ,
986+ f"- 日期:{ event .get ('event_date' ) or 'n/a' } " ,
987+ f"- 标题:{ event .get ('title' ) or 'n/a' } " ,
988+ f"- 来源:{ event .get ('source' ) or 'n/a' } " ,
989+ "价格确认:" ,
990+ f"- 事件后已过 { confirmation .get ('trading_days_after_event' , 'n/a' )} 个交易日" ,
991+ (
992+ f"- { benchmark } 3 日收益 { _format_pct (confirmation .get ('benchmark_3d_return' ), signed = True )} ,"
993+ f"从近 5 日低点反弹 "
994+ f"{ _format_pct (confirmation .get ('benchmark_rebound_from_recent_low' ), signed = True )} "
995+ ),
996+ f"- { attack } 从近 5 日低点反弹 { _format_pct (confirmation .get ('attack_rebound_from_recent_low' ), signed = True )} " ,
997+ ]
998+ )
999+ return
1000+ lines .extend (
1001+ [
1002+ "Event:" ,
1003+ f"- Type: { event .get ('kind' ) or 'n/a' } / region: { event .get ('region' ) or 'n/a' } " ,
1004+ f"- Date: { event .get ('event_date' ) or 'n/a' } " ,
1005+ f"- Title: { event .get ('title' ) or 'n/a' } " ,
1006+ f"- Source: { event .get ('source' ) or 'n/a' } " ,
1007+ "Price confirmation:" ,
1008+ f"- { confirmation .get ('trading_days_after_event' , 'n/a' )} trading days after the event" ,
1009+ (
1010+ f"- { benchmark } 3-day return { _format_pct (confirmation .get ('benchmark_3d_return' ), signed = True )} ; "
1011+ f"rebound from recent low "
1012+ f"{ _format_pct (confirmation .get ('benchmark_rebound_from_recent_low' ), signed = True )} "
1013+ ),
1014+ f"- { attack } rebound from recent low { _format_pct (confirmation .get ('attack_rebound_from_recent_low' ), signed = True )} " ,
1015+ ]
1016+ )
1017+
1018+
1019+ def _format_manual_review_notification_message (
1020+ payload : Mapping [str , Any ],
1021+ * ,
1022+ locale : str ,
1023+ target_label : str ,
1024+ plugin : str ,
1025+ as_of : str ,
1026+ route : str ,
1027+ ) -> str | None :
1028+ if _payload_action (payload ) != "notify_manual_review" :
1029+ return None
1030+ panic_payload = _active_panic_payload (payload , plugin )
1031+ taco_payload = _active_taco_payload (payload , plugin )
1032+ if not panic_payload and not taco_payload :
1033+ return None
1034+
1035+ position_control = _nested_mapping (payload , "position_control" )
1036+ vetoes = _message_reason_codes (position_control .get ("vetoes" ) or _nested_mapping (payload , "arbiter" ).get ("vetoes" ))
1037+ attack_symbol = _review_attack_symbol (panic_payload , taco_payload ) or target_label
1038+ source_title = _manual_review_source_title (panic_payload = panic_payload , taco_payload = taco_payload , locale = locale )
1039+ if locale == "zh-CN" :
1040+ lines = [
1041+ f"【机会复核|{ attack_symbol } |{ source_title } 】" ,
1042+ f"日期:{ as_of or '未知日期' } " ,
1043+ "结论:触发人工复核,不自动加仓。" ,
1044+ (
1045+ f"仲裁:{ plugin } = { route } ;"
1046+ f"{ 'crisis/macro 未 veto' if not vetoes else '存在 veto:' + _message_join (vetoes , locale )} 。"
1047+ ),
1048+ "执行权限:只通知;不下单;不修改仓位。" ,
1049+ ]
1050+ if position_control :
1051+ scalar_bits = []
1052+ if "taco_size_scalar" in position_control :
1053+ scalar_bits .append (f"taco_size_scalar = { _format_number (position_control .get ('taco_size_scalar' ))} " )
1054+ if "panic_reversal_size_scalar" in position_control :
1055+ scalar_bits .append (
1056+ f"panic_reversal_size_scalar = { _format_number (position_control .get ('panic_reversal_size_scalar' ))} "
1057+ )
1058+ if scalar_bits :
1059+ lines .append ("仓位权限:" + ";" .join (scalar_bits ) + "。" )
1060+ if panic_payload :
1061+ _append_panic_review_lines (lines , panic_payload , locale = locale )
1062+ if taco_payload :
1063+ _append_taco_review_lines (lines , taco_payload , locale = locale )
1064+ lines .extend (
1065+ [
1066+ "人工复核建议:" ,
1067+ "- 只评估是否停止继续降风险或恢复观察,不作为自动买入信号。" ,
1068+ "- 若 crisis/macro 后续转为 risk_reduced 或 risk_off,本机会信号自动失效。" ,
1069+ ]
1070+ )
1071+ return "\n " .join (lines )
1072+
1073+ lines = [
1074+ f"[Opportunity Review | { attack_symbol } | { source_title } ]" ,
1075+ f"Date: { as_of or 'unknown date' } " ,
1076+ "Conclusion: manual review triggered; no automatic position increase." ,
1077+ f"Arbiter: { plugin } = { route } ; { 'crisis/macro did not veto' if not vetoes else 'vetoes: ' + _message_join (vetoes , locale )} ." ,
1078+ "Execution: notify only; no broker orders; no allocation mutation." ,
1079+ ]
1080+ if position_control :
1081+ scalar_bits = []
1082+ if "taco_size_scalar" in position_control :
1083+ scalar_bits .append (f"taco_size_scalar = { _format_number (position_control .get ('taco_size_scalar' ))} " )
1084+ if "panic_reversal_size_scalar" in position_control :
1085+ scalar_bits .append (
1086+ f"panic_reversal_size_scalar = { _format_number (position_control .get ('panic_reversal_size_scalar' ))} "
1087+ )
1088+ if scalar_bits :
1089+ lines .append ("Position authority: " + "; " .join (scalar_bits ) + "." )
1090+ if panic_payload :
1091+ _append_panic_review_lines (lines , panic_payload , locale = locale )
1092+ if taco_payload :
1093+ _append_taco_review_lines (lines , taco_payload , locale = locale )
1094+ lines .extend (
1095+ [
1096+ "Manual review guidance:" ,
1097+ "- Review only whether to stop further de-risking or return to watch; this is not an automatic buy signal." ,
1098+ "- If crisis/macro later moves to risk_reduced or risk_off, this opportunity signal is invalidated." ,
1099+ ]
1100+ )
1101+ return "\n " .join (lines )
1102+
1103+
8201104def _format_notification_message (
8211105 * ,
8221106 locale : str ,
@@ -898,7 +1182,15 @@ def _build_localized_messages(
8981182 locale : list (_localized_reason_labels (reason_codes , locale )) for locale in SUPPORTED_MESSAGE_LOCALES
8991183 }
9001184 notification_messages = {
901- locale : _format_notification_message (
1185+ locale : _format_manual_review_notification_message (
1186+ payload ,
1187+ locale = locale ,
1188+ target_label = target_label ,
1189+ plugin = plugin ,
1190+ as_of = as_of ,
1191+ route = route ,
1192+ )
1193+ or _format_notification_message (
9021194 locale = locale ,
9031195 target_label = target_label ,
9041196 target_type = target_type ,
0 commit comments