4444 build_semiconductor_rotation_indicators_from_history ,
4545 required_semiconductor_rotation_history_lookback ,
4646)
47+ from quant_platform_kit .common .strategy_plugins import (
48+ build_strategy_plugin_alert_messages ,
49+ build_strategy_plugin_notification_lines ,
50+ build_strategy_plugin_report_payload ,
51+ load_configured_strategy_plugin_signals ,
52+ parse_strategy_plugin_mounts ,
53+ )
54+ from quant_platform_kit .notifications .email import send_smtp_email
4755from quant_platform_kit .notifications .events import NotificationPublisher , RenderedNotification
4856from quant_platform_kit .strategy_contracts import build_strategy_evaluation_inputs
4957from runtime_config_support import PlatformRuntimeSettings , load_platform_runtime_settings
@@ -162,6 +170,101 @@ def publish_log(text: str) -> None:
162170 return True
163171
164172
173+ def load_strategy_plugin_signals (
174+ raw_mounts ,
175+ * ,
176+ strategy_profile : str ,
177+ parse_mounts_fn = parse_strategy_plugin_mounts ,
178+ load_signals_fn = load_configured_strategy_plugin_signals ,
179+ ):
180+ if not raw_mounts :
181+ return (), None
182+ try :
183+ mounts = parse_mounts_fn (raw_mounts )
184+ if not mounts :
185+ return (), None
186+ return load_signals_fn (mounts , strategy_profile = strategy_profile ), None
187+ except Exception as exc :
188+ return (), f"{ type (exc ).__name__ } : { exc } "
189+
190+
191+ def attach_strategy_plugin_result (
192+ result : dict [str , Any ],
193+ * ,
194+ signals ,
195+ error : str | None ,
196+ translator : Callable [..., str ],
197+ ) -> dict [str , Any ]:
198+ if signals :
199+ result .update (build_strategy_plugin_report_payload (signals ))
200+ notification_lines = build_strategy_plugin_notification_lines (
201+ signals ,
202+ translator = translator ,
203+ )
204+ if notification_lines :
205+ result ["strategy_plugin_lines" ] = notification_lines
206+ if error :
207+ result ["strategy_plugin_error" ] = error
208+ return result
209+
210+
211+ def _call_log_message (log_message : Callable [..., Any ], text : str ) -> None :
212+ try :
213+ log_message (text , flush = True )
214+ except TypeError :
215+ log_message (text )
216+
217+
218+ def send_crisis_alert_email (
219+ alert_message ,
220+ * ,
221+ settings : PlatformRuntimeSettings ,
222+ smtp_module = None ,
223+ log_message : Callable [..., Any ] = print ,
224+ ) -> bool :
225+ send_kwargs : dict [str , Any ] = {}
226+ if smtp_module is not None :
227+ send_kwargs ["smtp_module" ] = smtp_module
228+ return send_smtp_email (
229+ subject = alert_message .subject ,
230+ body = alert_message .body ,
231+ smtp_host = getattr (settings , "crisis_alert_smtp_host" , None ),
232+ smtp_port = getattr (settings , "crisis_alert_smtp_port" , 587 ),
233+ sender = getattr (settings , "crisis_alert_email_from" , None ),
234+ recipients = getattr (settings , "crisis_alert_email_to" , ()),
235+ username = getattr (settings , "crisis_alert_smtp_username" , None ),
236+ password = getattr (settings , "crisis_alert_smtp_password" , None ),
237+ use_starttls = getattr (settings , "crisis_alert_smtp_starttls" , True ),
238+ use_ssl = getattr (settings , "crisis_alert_smtp_ssl" , False ),
239+ printer = lambda text , ** _kwargs : _call_log_message (log_message , text ),
240+ ** send_kwargs ,
241+ )
242+
243+
244+ def publish_strategy_plugin_alerts (
245+ signals ,
246+ * ,
247+ settings : PlatformRuntimeSettings ,
248+ translator : Callable [..., str ],
249+ log_message : Callable [..., Any ] = print ,
250+ ) -> int :
251+ sent_count = 0
252+ for alert_message in build_strategy_plugin_alert_messages (
253+ signals ,
254+ translator = translator ,
255+ strategy_label = settings .strategy_profile ,
256+ ):
257+ if send_crisis_alert_email (
258+ alert_message ,
259+ settings = settings ,
260+ log_message = log_message ,
261+ ):
262+ sent_count += 1
263+ if sent_count :
264+ _call_log_message (log_message , f"strategy_plugin_alert_email_sent count={ sent_count } " )
265+ return sent_count
266+
267+
165268def _runtime_metadata_with_execution_policy (
166269 metadata : Mapping [str , Any ] | None ,
167270 * ,
@@ -186,6 +289,11 @@ def run_strategy_cycle(
186289) -> dict [str , Any ]:
187290 now = _utcnow ()
188291 settings = runtime_settings or load_platform_runtime_settings (project_id_resolver = get_project_id )
292+ translator = build_translator (settings .notify_lang )
293+ strategy_plugin_signals , strategy_plugin_error = load_strategy_plugin_signals (
294+ settings .strategy_plugin_mounts_json ,
295+ strategy_profile = settings .strategy_profile ,
296+ )
189297 resolved_credentials = credentials or FirstradeCredentials .from_env (env_reader )
190298 store = state_store or build_gcs_state_store_from_env (env_reader )
191299 persist_strategy_runs = bool (settings .persist_strategy_runs and store is not None )
@@ -227,7 +335,7 @@ def run_strategy_cycle(
227335 available_inputs = available_inputs ,
228336 market_inputs = market_inputs ,
229337 portfolio_snapshot = snapshot ,
230- translator = build_translator ( settings . notify_lang ) ,
338+ translator = translator ,
231339 )
232340 evaluation = strategy_runtime .evaluate (** evaluation_inputs )
233341 plan = map_strategy_decision_to_plan (
@@ -258,7 +366,7 @@ def run_strategy_cycle(
258366 run_period = run_period ,
259367 )
260368 if is_duplicate_live_run (existing_run ):
261- return {
369+ result = {
262370 "ok" : True ,
263371 "api_kind" : "unofficial-reverse-engineered" ,
264372 "account" : masked_account ,
@@ -280,7 +388,24 @@ def run_strategy_cycle(
280388 }
281389 ],
282390 "action_done" : False ,
391+ "strategy_plugin_alert_email_sent_count" : 0 ,
283392 }
393+ return attach_strategy_plugin_result (
394+ result ,
395+ signals = strategy_plugin_signals ,
396+ error = strategy_plugin_error ,
397+ translator = translator ,
398+ )
399+ strategy_plugin_alert_email_sent_count = 0
400+ strategy_plugin_alert_email_error = None
401+ try :
402+ strategy_plugin_alert_email_sent_count = publish_strategy_plugin_alerts (
403+ strategy_plugin_signals ,
404+ settings = settings ,
405+ translator = translator ,
406+ )
407+ except Exception as exc :
408+ strategy_plugin_alert_email_error = f"{ type (exc ).__name__ } : { exc } "
284409 strategy_run_persisted = False
285410 strategy_run_persistence_error = None
286411 if persist_strategy_runs :
@@ -357,6 +482,15 @@ def run_strategy_cycle(
357482 result ["funding_blocked" ] = True
358483 if strategy_run_persistence_error :
359484 result ["strategy_run_persistence_error" ] = strategy_run_persistence_error
485+ result ["strategy_plugin_alert_email_sent_count" ] = strategy_plugin_alert_email_sent_count
486+ if strategy_plugin_alert_email_error :
487+ result ["strategy_plugin_alert_email_error" ] = strategy_plugin_alert_email_error
488+ attach_strategy_plugin_result (
489+ result ,
490+ signals = strategy_plugin_signals ,
491+ error = strategy_plugin_error ,
492+ translator = translator ,
493+ )
360494 if persist_strategy_runs :
361495 completed_state = build_strategy_run_state (
362496 stage = strategy_run_stage ,
0 commit comments