5151 "long" : "1-3年" ,
5252 "not_applicable" : "不适用" ,
5353}
54+ THEME_MOMENTUM_ARTIFACT_TYPE = "medium_horizon_theme_context"
5455
5556CADENCE_LABELS_ZH = {
5657 "daily" : "日度" ,
@@ -284,12 +285,17 @@ def summarize_theme_momentum(payload: dict[str, Any] | None, *, max_themes: int
284285 return {
285286 "available" : True ,
286287 "as_of" : payload .get ("as_of" , "" ),
288+ "artifact_type" : payload .get ("artifact_type" , THEME_MOMENTUM_ARTIFACT_TYPE ),
289+ "horizon" : payload .get ("horizon" , "medium" ),
290+ "horizon_window" : payload .get ("horizon_window" , "2-12 weeks" ),
291+ "horizon_window_label" : payload .get ("horizon_window_label" , HORIZON_WINDOWS ["medium" ]),
287292 "taxonomy_version" : payload .get ("taxonomy_version" , "" ),
288293 "top_themes" : top_themes ,
289294 "data_quality" : payload .get ("data_quality" , {}),
290295 "policy" : {
291296 "execution_allowed" : False ,
292297 "theme_rank_is_research_context_only" : True ,
298+ "direct_short_term_recommendation_allowed" : False ,
293299 },
294300 }
295301
@@ -728,6 +734,10 @@ def build_recommendation(
728734 ]
729735 )
730736
737+ long_horizon_ai_score = 0.0
738+ if ai_bias in {"positive" , "watch" , "neutral" }:
739+ long_horizon_ai_score = round (clamp (ai_confidence , 0 , 1 ), 3 )
740+
731741 return {
732742 "symbol" : symbol ,
733743 "name" : item .name if item else symbol ,
@@ -752,6 +762,15 @@ def build_recommendation(
752762 "evidence_summary" : " " .join (reasons ),
753763 "evidence_refs" : dedupe (evidence_refs ),
754764 "review_checklist" : dedupe (review_checklist ),
765+ "ai_context" : {
766+ "source" : "latest_signal" if ai_signal else "" ,
767+ "horizon" : "long" if ai_signal else "" ,
768+ "horizon_window" : HORIZON_WINDOWS ["long" ] if ai_signal else "" ,
769+ "bias" : ai_bias or "" ,
770+ "confidence" : round (ai_confidence , 3 ),
771+ "theme_ids" : ai_bias_source_themes ,
772+ },
773+ "long_horizon_ai_score" : long_horizon_ai_score ,
755774 }
756775
757776
@@ -829,6 +848,7 @@ def theme_symbol_context(theme_momentum: dict[str, Any] | None) -> dict[str, dic
829848 "primary_theme_label" : theme_label (theme_id , theme_name ),
830849 "best_theme_rank" : rank ,
831850 "ai_signal_score" : ai_signal_score ,
851+ "medium_context_score" : ai_signal_score ,
832852 "symbol_momentum_score" : symbol_momentum_score ,
833853 "momentum_score" : momentum_score ,
834854 "return_3m" : return_3m ,
@@ -843,6 +863,7 @@ def theme_symbol_context(theme_momentum: dict[str, Any] | None) -> dict[str, dic
843863 current ["best_theme_rank" ] = rank
844864 if ai_signal_score > as_float (current .get ("ai_signal_score" )):
845865 current ["ai_signal_score" ] = ai_signal_score
866+ current ["medium_context_score" ] = ai_signal_score
846867 if momentum_score > as_float (current .get ("momentum_score" )):
847868 current ["symbol_momentum_score" ] = symbol_momentum_score
848869 current ["momentum_score" ] = momentum_score
@@ -881,6 +902,24 @@ def final_action_label(action: str) -> str:
881902 }.get (action , "跳过" )
882903
883904
905+ def supporting_context_for (
906+ rec : dict [str , Any ] | None ,
907+ * ,
908+ source_score : float ,
909+ momentum_score : float ,
910+ medium_context_score : float ,
911+ long_context_score : float ,
912+ ) -> dict [str , list [str ]]:
913+ context = {"short" : [], "medium" : [], "long" : []}
914+ if source_score >= 0.35 and rec :
915+ context ["short" ].append ("source_events" )
916+ if momentum_score >= 0.35 or medium_context_score >= 0.35 :
917+ context ["medium" ].append ("theme_momentum_snapshot" )
918+ if long_context_score >= 0.35 and rec :
919+ context ["long" ].append ("latest_signal" )
920+ return context
921+
922+
884923def build_final_decisions (
885924 recommendations : list [dict [str , Any ]],
886925 theme_momentum : dict [str , Any ] | None ,
@@ -898,10 +937,13 @@ def build_final_decisions(
898937 theme = theme_context .get (symbol , {})
899938 source_score = normalize_source_score (rec )
900939 momentum_score = as_float (theme .get ("momentum_score" ))
901- ai_signal_score = as_float (theme .get ("ai_signal_score" ))
902- support_count = sum (score >= 0.35 for score in (source_score , momentum_score , ai_signal_score ))
903- combined_score = round (source_score * 0.15 + momentum_score * 0.40 + ai_signal_score * 0.45 , 3 )
904- action = final_action_for (rec , combined_score , support_count , momentum_score , ai_signal_score )
940+ medium_context_score = as_float (theme .get ("medium_context_score" , theme .get ("ai_signal_score" )))
941+ long_context_score = as_float (rec .get ("long_horizon_ai_score" )) if rec else 0.0
942+ support_count = sum (
943+ score >= 0.35 for score in (source_score , momentum_score , medium_context_score , long_context_score )
944+ )
945+ combined_score = round (source_score * 0.15 + momentum_score * 0.40 + medium_context_score * 0.45 , 3 )
946+ action = final_action_for (rec , combined_score , support_count , momentum_score , medium_context_score )
905947 if action == "skip" :
906948 continue
907949 name = rec .get ("name" , symbol ) if rec else symbol
@@ -915,9 +957,12 @@ def build_final_decisions(
915957 reasons .append (
916958 f"动量:个股动量分数={ display_number (theme .get ('symbol_momentum_score' ))} ,近3个月={ display_percent (theme .get ('return_3m' ))} 。"
917959 )
918- if ai_signal_score >= 0.35 :
960+ if medium_context_score >= 0.35 :
919961 labels = ", " .join (theme .get ("theme_labels" , [])[:3 ]) or str (theme .get ("primary_theme_label" , "" ))
920- reasons .append (f"AI信号仓库:{ labels } 。" )
962+ reasons .append (f"中线主题上下文:{ labels } 。" )
963+ if long_context_score >= 0.35 and rec :
964+ bias = rec .get ("ai_context" , {}).get ("bias" , "" )
965+ reasons .append (f"长线AI背景:{ bias or '已读取' } 。" )
921966 if not reasons :
922967 reasons .append ("当前进入观察名单,但多源证据仍需继续补强。" )
923968 if rec and rec .get ("primary_horizon" ) != "not_applicable" :
@@ -944,7 +989,16 @@ def build_final_decisions(
944989 "combined_score" : combined_score ,
945990 "source_score" : round (source_score , 3 ),
946991 "momentum_score" : round (momentum_score , 3 ),
947- "ai_signal_score" : round (ai_signal_score , 3 ),
992+ "ai_signal_score" : round (medium_context_score , 3 ),
993+ "medium_context_score" : round (medium_context_score , 3 ),
994+ "long_context_score" : round (long_context_score , 3 ),
995+ "supporting_context" : supporting_context_for (
996+ rec ,
997+ source_score = source_score ,
998+ momentum_score = momentum_score ,
999+ medium_context_score = medium_context_score ,
1000+ long_context_score = long_context_score ,
1001+ ),
9481002 "business_summary" : profile ["business" ],
9491003 "prospect_summary" : profile ["prospect" ],
9501004 "why_selected" : dedupe (reasons ),
@@ -1051,6 +1105,9 @@ def build_advisory_report(
10511105 "source_mode" : source_mode ,
10521106 "data_quality_warnings" : data_quality_warnings ,
10531107 "theme_momentum_available" : theme_momentum_summary ["available" ],
1108+ "theme_momentum_artifact_type" : theme_momentum_summary .get ("artifact_type" , "" ),
1109+ "theme_momentum_horizon" : theme_momentum_summary .get ("horizon" , "" ),
1110+ "theme_momentum_horizon_window" : theme_momentum_summary .get ("horizon_window_label" , "" ),
10541111 "top_theme_ids" : [theme ["theme_id" ] for theme in theme_momentum_summary ["top_themes" ]],
10551112 "theme_first_candidate_count" : len (theme_first_candidates ),
10561113 "top_theme_candidate_symbols" : [item ["symbol" ] for item in theme_first_candidates [:8 ]],
0 commit comments