@@ -15,6 +15,141 @@ def _format_text(value, *, fallback: str) -> str:
1515 return text or fallback
1616
1717
18+ def _format_symbol_preview (symbols , * , limit : int = 3 ) -> str :
19+ normalized = [str (symbol ).strip ().upper () for symbol in symbols if str (symbol ).strip ()]
20+ if not normalized :
21+ return ""
22+ shown = normalized [:limit ]
23+ remaining = len (normalized ) - len (shown )
24+ if remaining > 0 :
25+ shown .append (f"+{ remaining } " )
26+ return "," .join (shown )
27+
28+
29+ def _summarize_target_changes (target_vs_current , * , limit : int = 5 ) -> str | None :
30+ rows = []
31+ for row in target_vs_current or ():
32+ symbol = str (row .get ("symbol" ) or "" ).strip ().upper ()
33+ if not symbol :
34+ continue
35+ delta = float (row .get ("delta_weight" ) or 0.0 )
36+ if abs (delta ) < 0.001 :
37+ continue
38+ rows .append ((abs (delta ), symbol , delta ))
39+ if not rows :
40+ return None
41+ rows .sort (key = lambda item : (- item [0 ], item [1 ]))
42+ preview = [f"{ symbol } { delta :+.1%} " for _abs_delta , symbol , delta in rows [:limit ]]
43+ remaining = len (rows ) - len (preview )
44+ if remaining > 0 :
45+ preview .append (f"+{ remaining } " )
46+ return ", " .join (preview )
47+
48+
49+ def _summarize_orders (orders , * , limit : int = 3 ) -> str :
50+ preview = []
51+ for order in orders [:limit ]:
52+ symbol = str (order .get ("symbol" ) or "" ).strip ().upper ()
53+ quantity = int (order .get ("quantity" ) or 0 )
54+ if symbol and quantity > 0 :
55+ preview .append (f"{ symbol } { quantity } " )
56+ elif symbol :
57+ preview .append (symbol )
58+ remaining = len (orders ) - len (preview )
59+ if remaining > 0 :
60+ preview .append (f"+{ remaining } " )
61+ return ", " .join (preview )
62+
63+
64+ def _build_order_batch_lines (execution_summary , * , translator ) -> list [str ]:
65+ mode = str (execution_summary .get ("mode" ) or "" ).strip ().lower ()
66+ order_groups = [
67+ ("orders_submitted" , "dry_run" if mode == "dry_run" else "submitted" ),
68+ ("orders_filled" , "filled" ),
69+ ("orders_partially_filled" , "partial" ),
70+ ]
71+ lines : list [str ] = []
72+ for field_name , prefix in order_groups :
73+ orders = list (execution_summary .get (field_name ) or [])
74+ if not orders :
75+ continue
76+ buy_orders = [order for order in orders if str (order .get ("side" ) or "" ).strip ().lower () == "buy" ]
77+ sell_orders = [order for order in orders if str (order .get ("side" ) or "" ).strip ().lower () == "sell" ]
78+ if buy_orders :
79+ lines .append (
80+ translator (
81+ f"{ prefix } _buy_batch" ,
82+ count = len (buy_orders ),
83+ details = _summarize_orders (buy_orders ),
84+ )
85+ )
86+ if sell_orders :
87+ lines .append (
88+ translator (
89+ f"{ prefix } _sell_batch" ,
90+ count = len (sell_orders ),
91+ details = _summarize_orders (sell_orders ),
92+ )
93+ )
94+ return lines
95+
96+
97+ def _build_notification_trade_lines (
98+ trade_logs ,
99+ * ,
100+ execution_summary ,
101+ translator ,
102+ ) -> list [str ]:
103+ lines : list [str ] = []
104+ execution_summary = dict (execution_summary or {})
105+
106+ no_op_reason = str (execution_summary .get ("no_op_reason" ) or "" ).strip ()
107+ if no_op_reason .startswith ("same_day_execution_locked:" ):
108+ lines .append (
109+ translator (
110+ "same_day_execution_locked_notice" ,
111+ mode = _format_text (execution_summary .get ("mode" ), fallback = "<none>" ),
112+ trade_date = _format_text (execution_summary .get ("trade_date" ), fallback = "<none>" ),
113+ snapshot_date = _format_text (execution_summary .get ("snapshot_as_of" ), fallback = "<none>" ),
114+ )
115+ )
116+
117+ fallback_symbols = tuple (execution_summary .get ("snapshot_price_fallback_symbols" ) or ())
118+ if execution_summary .get ("snapshot_price_fallback_used" ) and fallback_symbols :
119+ lines .append (
120+ translator (
121+ "dry_run_snapshot_prices" ,
122+ count = len (fallback_symbols ),
123+ symbols = _format_symbol_preview (fallback_symbols ),
124+ )
125+ )
126+
127+ target_change_summary = _summarize_target_changes (execution_summary .get ("target_vs_current" ))
128+ if target_change_summary :
129+ lines .append (translator ("target_diff_summary" , details = target_change_summary ))
130+
131+ lines .extend (_build_order_batch_lines (execution_summary , translator = translator ))
132+
133+ for raw_line in trade_logs or ():
134+ text = str (raw_line ).strip ()
135+ if not text :
136+ continue
137+ if text .startswith (("目标差异 " , "target_diff " , "DRY_RUN buy " , "DRY_RUN sell " )):
138+ continue
139+ if text .startswith (("🧪 dry-run估价:" , "🧪 dry-run pricing:" )):
140+ continue
141+ if "execution_lock_acquired" in text or "已获取执行锁" in text :
142+ continue
143+ if text .startswith (("profile=" , "strategy_profile=" , "策略=" )):
144+ continue
145+ if "same_day_execution_locked" in text or "当日执行锁已存在" in text :
146+ continue
147+ if text not in lines :
148+ lines .append (text )
149+
150+ return lines
151+
152+
18153def build_dashboard (
19154 positions ,
20155 account_values ,
@@ -48,12 +183,7 @@ def build_dashboard(
48183 target_stock_weight = signal_metadata .get ("target_stock_weight" )
49184 realized_stock_weight = signal_metadata .get ("realized_stock_weight" )
50185 safe_haven_weight = signal_metadata .get ("safe_haven_weight" )
51- config_source = signal_metadata .get ("strategy_config_source" )
52186 snapshot_as_of = signal_metadata .get ("snapshot_as_of" )
53- snapshot_path = signal_metadata .get ("feature_snapshot_path" ) or signal_metadata .get ("snapshot_path" )
54- snapshot_age_days = signal_metadata .get ("snapshot_age_days" )
55- snapshot_file_timestamp = signal_metadata .get ("snapshot_file_timestamp" )
56- snapshot_decision = signal_metadata .get ("snapshot_guard_decision" )
57187 diagnostics = [
58188 translator ("strategy_profile_detail" , profile = _format_text (strategy_profile , fallback = "<unknown>" )),
59189 translator ("regime_detail" , value = _format_text (regime , fallback = "<none>" )) if regime is not None else None ,
@@ -63,22 +193,13 @@ def build_dashboard(
63193 else None ,
64194 translator ("realized_stock_detail" , value = f"{ realized_stock_weight :.1%} " )
65195 if isinstance (realized_stock_weight , (int , float ))
196+ and isinstance (target_stock_weight , (int , float ))
197+ and abs (float (realized_stock_weight ) - float (target_stock_weight )) >= 0.01
66198 else None ,
67199 translator ("safe_haven_target_detail" , value = f"{ safe_haven_weight :.1%} " )
68200 if isinstance (safe_haven_weight , (int , float ))
69201 else None ,
70- translator ("snapshot_decision_detail" , value = _format_text (snapshot_decision , fallback = "<none>" ))
71- if snapshot_decision
72- else None ,
73202 translator ("snapshot_as_of_detail" , value = _format_text (snapshot_as_of , fallback = "<none>" )) if snapshot_as_of else None ,
74- translator ("snapshot_age_days_detail" , value = _format_text (snapshot_age_days , fallback = "<none>" ))
75- if isinstance (snapshot_age_days , (int , float ))
76- else None ,
77- translator ("snapshot_file_ts_detail" , value = _format_text (snapshot_file_timestamp , fallback = "<none>" ))
78- if snapshot_file_timestamp
79- else None ,
80- translator ("snapshot_path_detail" , value = _format_text (snapshot_path , fallback = "<none>" )) if snapshot_path else None ,
81- translator ("config_source_detail" , value = _format_text (config_source , fallback = "<none>" )) if config_source else None ,
82203 ]
83204 diagnostics_text = " | " .join (part for part in diagnostics if part )
84205 return (
@@ -213,7 +334,12 @@ def run_strategy_core(
213334 flush = True ,
214335 )
215336 if trade_logs :
216- trade_lines = "\n " .join (trade_logs )
337+ notification_trade_lines = _build_notification_trade_lines (
338+ trade_logs ,
339+ execution_summary = execution_summary ,
340+ translator = translator ,
341+ )
342+ trade_lines = "\n " .join (notification_trade_lines )
217343 message = (
218344 f"{ translator ('rebalance_title' )} \n "
219345 f"{ dashboard } \n "
0 commit comments