66import json
77import tempfile
88from collections .abc import Mapping , Sequence
9- from dataclasses import dataclass
9+ from dataclasses import dataclass , field
1010from pathlib import Path
1111from typing import Any , Callable
1212
@@ -65,6 +65,8 @@ def report_summary(self) -> dict[str, Any]:
6565class StrategyPluginAlertMessage :
6666 subject : str
6767 body : str
68+ alert_key : str = ""
69+ metadata : Mapping [str , Any ] = field (default_factory = dict )
6870
6971
7072def normalize_strategy_plugin_mode (value : Any , * , field_name : str = "mode" ) -> str :
@@ -284,13 +286,51 @@ def should_alert_strategy_plugin_signal(signal: StrategyPluginSignal) -> bool:
284286 )
285287
286288
289+ def build_strategy_plugin_alert_key (
290+ signal : StrategyPluginSignal ,
291+ * ,
292+ strategy_label : str | None = None ,
293+ context_label : str | None = None ,
294+ namespace : str = "strategy_plugin_alert" ,
295+ ) -> str :
296+ payload = {
297+ "namespace" : _optional_key_part (namespace ) or "strategy_plugin_alert" ,
298+ "context" : _optional_key_part (context_label ) or "default" ,
299+ "strategy" : _optional_key_part (getattr (signal , "strategy" , None )) or _optional_key_part (strategy_label ) or "unknown" ,
300+ "plugin" : _optional_key_part (getattr (signal , "plugin" , None )) or "unknown" ,
301+ "mode" : _optional_key_part (getattr (signal , "effective_mode" , None )) or "unknown" ,
302+ "as_of" : _optional_key_part (getattr (signal , "as_of" , None )) or "unknown" ,
303+ "route" : _optional_key_part (getattr (signal , "canonical_route" , None )) or "unknown" ,
304+ "action" : _optional_key_part (getattr (signal , "suggested_action" , None )) or "unknown" ,
305+ "would_trade_if_enabled" : bool (getattr (signal , "would_trade_if_enabled" , False )),
306+ }
307+ digest = hashlib .sha256 (
308+ json .dumps (payload , ensure_ascii = False , sort_keys = True ).encode ("utf-8" )
309+ ).hexdigest ()[:16 ]
310+ return "/" .join (
311+ (
312+ _sanitize_key_part (payload ["namespace" ]),
313+ _sanitize_key_part (payload ["context" ]),
314+ _sanitize_key_part (payload ["strategy" ]),
315+ _sanitize_key_part (payload ["plugin" ]),
316+ _sanitize_key_part (payload ["as_of" ]),
317+ _sanitize_key_part (payload ["route" ]),
318+ _sanitize_key_part (payload ["action" ]),
319+ digest ,
320+ )
321+ )
322+
323+
287324def build_strategy_plugin_alert_messages (
288325 signals : Sequence [StrategyPluginSignal ],
289326 * ,
290327 translator : Callable [..., str ] | None = None ,
291328 strategy_label : str | None = None ,
329+ context_label : str | None = None ,
330+ alert_namespace : str = "strategy_plugin_alert" ,
292331) -> tuple [StrategyPluginAlertMessage , ...]:
293332 messages : list [StrategyPluginAlertMessage ] = []
333+ context = str (context_label or "" ).strip ()
294334 for signal in signals :
295335 if not should_alert_strategy_plugin_signal (signal ):
296336 continue
@@ -308,8 +348,22 @@ def build_strategy_plugin_alert_messages(
308348 plugin = plugin ,
309349 route = translated_route ,
310350 )
351+ if context :
352+ subject = f"[{ context } ] { subject } "
311353 body_lines = [
312354 _translate (translator , "strategy_plugin_alert_title" , fallback = "Strategy Plugin Alert" ),
355+ ]
356+ if context :
357+ body_lines .append (
358+ _translate (
359+ translator ,
360+ "strategy_plugin_alert_context" ,
361+ fallback = "Context: {context}" ,
362+ context = context ,
363+ )
364+ )
365+ body_lines .extend (
366+ [
313367 _translate (
314368 translator ,
315369 "strategy_plugin_line" ,
@@ -337,7 +391,8 @@ def build_strategy_plugin_alert_messages(
337391 fallback = "Would trade if enabled: {value}" ,
338392 value = str (bool (getattr (signal , "would_trade_if_enabled" , False ))).lower (),
339393 ),
340- ]
394+ ]
395+ )
341396 source = getattr (signal , "source_uri" , None ) or getattr (signal , "local_path" , None )
342397 if source :
343398 body_lines .append (
@@ -348,10 +403,55 @@ def build_strategy_plugin_alert_messages(
348403 source = source ,
349404 )
350405 )
351- messages .append (StrategyPluginAlertMessage (subject = subject , body = "\n " .join (body_lines )))
406+ metadata = {
407+ "strategy" : getattr (signal , "strategy" , None ),
408+ "strategy_label" : strategy ,
409+ "plugin" : getattr (signal , "plugin" , None ),
410+ "mode" : getattr (signal , "effective_mode" , None ),
411+ "as_of" : getattr (signal , "as_of" , None ),
412+ "canonical_route" : getattr (signal , "canonical_route" , None ),
413+ "suggested_action" : getattr (signal , "suggested_action" , None ),
414+ "would_trade_if_enabled" : bool (getattr (signal , "would_trade_if_enabled" , False )),
415+ "context_label" : context or None ,
416+ }
417+ messages .append (
418+ StrategyPluginAlertMessage (
419+ subject = subject ,
420+ body = "\n " .join (body_lines ),
421+ alert_key = build_strategy_plugin_alert_key (
422+ signal ,
423+ strategy_label = strategy ,
424+ context_label = context ,
425+ namespace = alert_namespace ,
426+ ),
427+ metadata = metadata ,
428+ )
429+ )
352430 return tuple (messages )
353431
354432
433+ def _optional_key_part (value : Any ) -> str | None :
434+ if value is None :
435+ return None
436+ text = str (value ).strip ()
437+ return text or None
438+
439+
440+ def _sanitize_key_part (value : Any ) -> str :
441+ text = str (value or "" ).strip ().lower ()
442+ chars : list [str ] = []
443+ previous_dash = False
444+ for char in text :
445+ if char .isalnum () or char in {"_" , "." }:
446+ chars .append (char )
447+ previous_dash = False
448+ elif not previous_dash :
449+ chars .append ("-" )
450+ previous_dash = True
451+ sanitized = "" .join (chars ).strip ("-._" )
452+ return sanitized [:80 ] or "unknown"
453+
454+
355455def _materialize_artifact_path (reference : str , * , client_factory : Any = None ) -> tuple [Path , dict [str , str | None ]]:
356456 raw_reference = _required_string (reference , field_name = "reference" )
357457 if not raw_reference .startswith ("gs://" ):
0 commit comments