5858 "monthly" : "月度" ,
5959}
6060
61+ SECTOR_LABELS_ZH = {
62+ "technology" : "科技" ,
63+ "energy" : "能源" ,
64+ "financials" : "金融" ,
65+ "healthcare" : "医疗" ,
66+ "industrials" : "工业" ,
67+ "consumer" : "消费" ,
68+ "communication_services" : "通信服务" ,
69+ "utilities" : "公用事业" ,
70+ }
71+
72+ EXCLUDED_THEME_PICK_SYMBOLS = {
73+ "DIA" ,
74+ "IWM" ,
75+ "QQQ" ,
76+ "SMH" ,
77+ "SOXL" ,
78+ "SOXX" ,
79+ "SPY" ,
80+ "TQQQ" ,
81+ "XLE" ,
82+ "XLK" ,
83+ }
84+
6185
6286@dataclass (frozen = True )
6387class Event :
@@ -196,6 +220,23 @@ def as_float(value: Any, *, default: float = 0.0) -> float:
196220 return default
197221
198222
223+ def display_number (value : Any , * , digits : int = 2 ) -> str :
224+ if value in {None , "" }:
225+ return "无"
226+ return f"{ as_float (value ):.{digits }f} "
227+
228+
229+ def display_percent (value : Any , * , digits : int = 1 ) -> str :
230+ if value in {None , "" }:
231+ return "无"
232+ return f"{ as_float (value ) * 100 :+.{digits }f} %"
233+
234+
235+ def sector_label (value : Any ) -> str :
236+ text = str (value or "" ).strip ()
237+ return SECTOR_LABELS_ZH .get (text , text or "未分类" )
238+
239+
199240def normalize_ai_mapping (mapping : Any ) -> dict [str , Any ]:
200241 if not isinstance (mapping , dict ):
201242 return {}
@@ -263,7 +304,7 @@ def strategy_style(item: WatchlistItem | None, events: list[Event], ai_bias: str
263304
264305def horizon_fit (style : str , rating : str ) -> tuple [str , list [str ], str , str ]:
265306 if rating in {"verify_source" , "defer" , "monitor" }:
266- return "not_applicable" , ["not_applicable" ], "不适用" , "该项不是当前推荐,仅用于来源核验、风险暂缓或持续监控 。"
307+ return "not_applicable" , ["not_applicable" ], "不适用" , "该项不是当前推荐,仅用于来源核验、风险暂缓或背景跟踪 。"
267308 if style == "event_driven" :
268309 return "medium" , ["short" , "medium" ], "中线" , "事件驱动主周期为2-12周;1-10个交易日只适合观察催化反应,波动和反转风险更高。"
269310 if style in {"long_horizon_growth" , "value_quality" }:
@@ -279,7 +320,7 @@ def rating_label(rating: str) -> str:
279320 "watch" : "观察" ,
280321 "verify_source" : "先核验来源" ,
281322 "defer" : "暂缓" ,
282- "monitor" : "监控 " ,
323+ "monitor" : "背景跟踪 " ,
283324 }
284325 return labels [rating ]
285326
@@ -310,14 +351,14 @@ def recommendation_tier(rating: str, score: float, confidence: str) -> tuple[str
310351 return "source_check" , "来源核验"
311352 if rating == "defer" :
312353 return "defer" , "暂缓"
313- return "monitor" , "监控 "
354+ return "monitor" , "背景跟踪 "
314355
315356
316357def build_theme_first_candidates (
317358 theme_momentum : dict [str , Any ] | None ,
318359 recommendations : list [dict [str , Any ]],
319360 * ,
320- max_candidates : int = 10 ,
361+ max_candidates : int = 8 ,
321362 max_themes : int = 5 ,
322363) -> list [dict [str , Any ]]:
323364 if not theme_momentum :
@@ -331,12 +372,15 @@ def build_theme_first_candidates(
331372 theme_rank = int (as_float (theme .get ("rank" ), default = 999 ))
332373 theme_id = str (theme .get ("theme_id" , "" ))
333374 theme_name = str (theme .get ("theme_name" , "" ))
375+ theme_sector = str (theme .get ("sector" , "" ))
334376 theme_score = round (as_float (theme .get ("momentum_score" )), 6 )
335377 theme_breadth = round (as_float (theme .get ("breadth_3m" )), 6 )
336378 for item in theme .get ("top_symbols" , []):
337379 if not isinstance (item , dict ) or not item .get ("symbol" ):
338380 continue
339381 symbol = str (item ["symbol" ]).upper ()
382+ if symbol in EXCLUDED_THEME_PICK_SYMBOLS :
383+ continue
340384 symbol_score = round (as_float (item .get ("momentum_score" )), 6 )
341385 candidate = by_symbol .setdefault (
342386 symbol ,
@@ -351,6 +395,7 @@ def build_theme_first_candidates(
351395 "best_theme_rank" : theme_rank ,
352396 "primary_theme_id" : theme_id ,
353397 "primary_theme_name" : theme_name ,
398+ "primary_theme_sector" : theme_sector ,
354399 "primary_theme_score" : theme_score ,
355400 "primary_theme_breadth_3m" : theme_breadth ,
356401 "theme_ids" : [],
@@ -366,6 +411,7 @@ def build_theme_first_candidates(
366411 candidate ["best_theme_rank" ] = theme_rank
367412 candidate ["primary_theme_id" ] = theme_id
368413 candidate ["primary_theme_name" ] = theme_name
414+ candidate ["primary_theme_sector" ] = theme_sector
369415 candidate ["primary_theme_score" ] = theme_score
370416 candidate ["primary_theme_breadth_3m" ] = theme_breadth
371417 if theme_id and theme_id not in candidate ["theme_ids" ]:
@@ -374,6 +420,7 @@ def build_theme_first_candidates(
374420 {
375421 "theme_id" : theme_id ,
376422 "theme_name" : theme_name ,
423+ "sector" : theme_sector ,
377424 "rank" : theme_rank ,
378425 "momentum_score" : theme_score ,
379426 "breadth_3m" : theme_breadth ,
@@ -402,7 +449,7 @@ def build_theme_first_candidates(
402449 source_confirmation = f"已有{ rec .get ('source_confidence_label' , '' )} 置信事件确认"
403450 theme_ids = candidate .get ("theme_ids" , [])
404451 reasons = [
405- "主题动量排序靠前 ,适合放入本期主题优先候选池。" ,
452+ "主题和个股动量靠前 ,适合放入本期主题优先候选池。" ,
406453 (
407454 f"主主题 #{ candidate .get ('best_theme_rank' )} { candidate .get ('primary_theme_id' )} "
408455 f"主题分数={ candidate .get ('primary_theme_score' )} ,3个月广度={ candidate .get ('primary_theme_breadth_3m' )} 。"
@@ -411,9 +458,23 @@ def build_theme_first_candidates(
411458 ]
412459 if len (theme_ids ) > 1 :
413460 reasons .append (f"同时暴露于多个强主题:{ ', ' .join (theme_ids )} 。" )
461+ industry_background = (
462+ f"{ sector_label (candidate .get ('primary_theme_sector' ))} / "
463+ f"{ candidate .get ('primary_theme_name' ) or candidate .get ('primary_theme_id' )} "
464+ )
465+ recommendation_summary = (
466+ f"属于{ industry_background } ,主主题排名 #{ candidate .get ('best_theme_rank' )} ;"
467+ f"个股动量 { display_number (candidate .get ('symbol_momentum_score' ))} ,"
468+ f"近3个月 { display_percent (candidate .get ('return_3m' ))} 。"
469+ )
470+ if len (theme_ids ) > 1 :
471+ recommendation_summary += f" 同时覆盖 { ', ' .join (theme_ids [:3 ])} 等主题。"
472+ risk_summary = "需复核估值、财报、回撤、流动性和稳定事件证据。"
473+ if source_confirmation != "待事件确认" :
474+ risk_summary = "已有事件确认,但仍需复核估值、财报、回撤和流动性。"
414475 risk_notes = [
415476 "该候选来自主题和价格动量排序,不代表个性化建议或下单信号。" ,
416- "升级为正式推荐前,需要继续复核估值、财报、回撤、流动性和稳定事件证据。" ,
477+ risk_summary ,
417478 ]
418479 if rec and rec .get ("risk_notes" ):
419480 risk_notes .append (str (rec ["risk_notes" ][0 ]))
@@ -423,6 +484,9 @@ def build_theme_first_candidates(
423484 "rank" : index ,
424485 "advisor_status" : advisor_status ,
425486 "source_confirmation" : source_confirmation ,
487+ "industry_background" : industry_background ,
488+ "recommendation_summary" : recommendation_summary ,
489+ "risk_summary" : risk_summary ,
426490 "reasons" : dedupe (reasons ),
427491 "risk_notes" : dedupe (risk_notes ),
428492 }
@@ -691,25 +755,24 @@ def render_markdown(report: dict[str, Any]) -> str:
691755 ]
692756 theme_candidates = report .get ("theme_first_candidates" , [])
693757 if theme_candidates :
694- lines .extend (["## 主题优先候选" , "" ])
695- lines .append ("- 说明: 该列表按主题/个股动量排序,用于突出 AI 和高科技等强主题候选;不是下单或仓位建议。" )
758+ lines .extend (["## 本期重点股票池" , "" ])
759+ lines .append ("- 先看这里: 每期选出 5-10 个股票/公司标的,说明行业主题、入选理由、事件确认和主要风险。" )
760+ lines .append ("- 边界: 这是非个性化模型股票池,不是买入清单;`待事件确认` 表示稳定事件证据还不足。" )
696761 lines .append ("" )
697762 for candidate in theme_candidates :
698763 lines .extend (
699764 [
700765 f"### #{ candidate .get ('rank' )} { candidate .get ('symbol' )} - { candidate .get ('advisor_status' )} " ,
701766 "" ,
702- f"- 主主题: `{ candidate .get ('primary_theme_id' )} ` / { candidate .get ('primary_theme_name' )} " ,
703- f"- 个股动量分数: `{ candidate .get ('symbol_momentum_score' )} `" ,
704- f"- 3个月收益: `{ candidate .get ('return_3m' , '' )} `" ,
767+ f"- 行业/主题: { candidate .get ('industry_background' )} " ,
768+ f"- 主主题: `{ candidate .get ('primary_theme_id' )} `" ,
769+ f"- 个股动量分数: `{ display_number (candidate .get ('symbol_momentum_score' ))} `" ,
770+ f"- 近3个月: `{ display_percent (candidate .get ('return_3m' ))} `" ,
705771 f"- 来源确认: `{ candidate .get ('source_confirmation' )} `" ,
706- f"- 相关主题: ` { ', ' . join ( candidate .get ('theme_ids' , [])) } ` " ,
707- "- 理由: " ,
772+ f"- 为什么入选: { candidate .get ('recommendation_summary' ) } " ,
773+ f "- 主要风险: { candidate . get ( 'risk_summary' ) } " ,
708774 ]
709775 )
710- lines .extend (f" - { reason } " for reason in candidate .get ("reasons" , []))
711- lines .append ("- 风险:" )
712- lines .extend (f" - { risk } " for risk in candidate .get ("risk_notes" , []))
713776 lines .append ("" )
714777 theme_momentum = report .get ("theme_momentum" , {})
715778 if theme_momentum .get ("available" ):
0 commit comments