1111from flask import Flask
1212
1313import google .auth
14+ import requests
1415from application .runtime_broker_adapters import build_runtime_broker_adapters
1516from application .runtime_composer import build_runtime_composer
1617from application .rebalance_service import run_strategy as run_rebalance_cycle
@@ -110,6 +111,14 @@ def t(key, **kwargs):
110111 return build_translator (NOTIFY_LANG )(key , ** kwargs )
111112
112113
114+ def _split_env_list (value : str | None ) -> tuple [str , ...]:
115+ return tuple (
116+ item .strip ()
117+ for item in str (value or "" ).replace (";" , "," ).split ("," )
118+ if item .strip ()
119+ )
120+
121+
113122signal_text = build_signal_text (t )
114123strategy_display_name = build_strategy_display_name (t )(
115124 STRATEGY_PROFILE ,
@@ -235,6 +244,79 @@ def build_strategy_plugin_alert_messages(signals):
235244 return STRATEGY_ADAPTERS .build_strategy_plugin_alert_messages (signals )
236245
237246
247+ def _runtime_error_notification_targets () -> tuple [tuple [str , str ], ...]:
248+ targets : list [tuple [str , str ]] = []
249+ if TG_TOKEN and TG_CHAT_ID :
250+ targets .append ((TG_TOKEN , TG_CHAT_ID ))
251+ crisis_token = os .getenv ("CRISIS_ALERT_TELEGRAM_BOT_TOKEN" )
252+ for chat_id in _split_env_list (os .getenv ("CRISIS_ALERT_TELEGRAM_CHAT_IDS" )):
253+ if crisis_token and chat_id :
254+ targets .append ((crisis_token , chat_id ))
255+
256+ seen : set [tuple [str , str ]] = set ()
257+ unique_targets : list [tuple [str , str ]] = []
258+ for target in targets :
259+ if target in seen :
260+ continue
261+ seen .add (target )
262+ unique_targets .append (target )
263+ return tuple (unique_targets )
264+
265+
266+ def _runtime_error_notification_message (exc : Exception , * , route_label : str ) -> str :
267+ error_text = f"{ type (exc ).__name__ } : { exc } "
268+ if len (error_text ) > 1200 :
269+ error_text = error_text [:1197 ] + "..."
270+ return "\n " .join (
271+ (
272+ "LongBridge strategy run failed" ,
273+ f"service: { os .getenv ('K_SERVICE' ) or SECRET_NAME or 'longbridge-platform' } " ,
274+ f"revision: { os .getenv ('K_REVISION' ) or '<unknown>' } " ,
275+ f"route: { route_label } " ,
276+ f"strategy: { STRATEGY_PROFILE } " ,
277+ f"account_scope: { ACCOUNT_REGION } " ,
278+ f"error: { error_text } " ,
279+ )
280+ )
281+
282+
283+ def _notify_runtime_error (exc : Exception , * , route_label : str ) -> bool :
284+ targets = _runtime_error_notification_targets ()
285+ if not targets :
286+ print ("LongBridge runtime error notification skipped: no Telegram target configured." , flush = True )
287+ return False
288+ message = _runtime_error_notification_message (exc , route_label = route_label )
289+ for token , chat_id in targets :
290+ try :
291+ requests .post (
292+ f"https://api.telegram.org/bot{ token } /sendMessage" ,
293+ json = {"chat_id" : chat_id , "text" : message },
294+ timeout = 10 ,
295+ )
296+ except Exception as send_exc :
297+ print (f"LongBridge runtime error Telegram send failed: { send_exc } " , flush = True )
298+ return True
299+
300+
301+ def _handle_route_runtime_error (exc : Exception , * , route_label : str ):
302+ print (f"LongBridge route failed before strategy-cycle handling: { type (exc ).__name__ } : { exc } " , flush = True )
303+ traceback .print_exc ()
304+ _notify_runtime_error (exc , route_label = route_label )
305+ return "Error" , 500
306+
307+
308+ def _route_with_runtime_error_fallback (handler , * , success_body : str , route_label : str , ** kwargs ):
309+ try :
310+ result = handler (** kwargs )
311+ except Exception as exc :
312+ return _handle_route_runtime_error (exc , route_label = route_label )
313+ if result is False :
314+ return "Error" , 500
315+ if isinstance (result , tuple ):
316+ return result
317+ return success_body , 200
318+
319+
238320def build_strategy_plugin_alert_state_settings ():
239321 return StrategyPluginAlertStateSettings .from_env (
240322 gcp_project_id = PROJECT_ID ,
@@ -321,7 +403,7 @@ def run_strategy(*, force_run: bool = False, validation_only: bool = False, vali
321403 },
322404 )
323405 print (composer .with_prefix ("Outside market hours; skip." ), flush = True )
324- return
406+ return True
325407 if force_run and not market_open :
326408 reporting_adapters .log_event (
327409 log_context ,
@@ -366,6 +448,7 @@ def run_strategy(*, force_run: bool = False, validation_only: bool = False, vali
366448 "strategy_cycle_completed" ,
367449 message = "Strategy execution completed" ,
368450 )
451+ return True
369452
370453 except Exception as exc :
371454 append_runtime_report_error (
@@ -388,6 +471,7 @@ def run_strategy(*, force_run: bool = False, validation_only: bool = False, vali
388471 detailed_text = f"Strategy error:\n { err } " ,
389472 compact_text = f"{ t ('error_title' )} \n { err } " ,
390473 )
474+ return False
391475 finally :
392476 try :
393477 report_path = reporting_adapters .persist_execution_report (report )
@@ -487,28 +571,46 @@ def run_probe(*, response_body: str = "Probe OK"):
487571@app .route ("/" , methods = ["POST" , "GET" ])
488572def handle_trigger ():
489573 """Entrypoint for Cloud Run / scheduler: run strategy and return 200."""
490- run_strategy ()
491- return "OK" , 200
574+ return _route_with_runtime_error_fallback (
575+ run_strategy ,
576+ success_body = "OK" ,
577+ route_label = "POST /" ,
578+ )
492579
493580
494581@app .route ("/backfill" , methods = ["POST" , "GET" ])
495582def handle_backfill ():
496583 """Manual backfill entrypoint for verification-only execution."""
497- run_strategy (force_run = True , validation_only = True )
498- return "OK" , 200
584+ return _route_with_runtime_error_fallback (
585+ run_strategy ,
586+ force_run = True ,
587+ validation_only = True ,
588+ success_body = "OK" ,
589+ route_label = "POST /backfill" ,
590+ )
499591
500592
501593@app .route ("/precheck" , methods = ["POST" , "GET" ])
502594def handle_precheck ():
503595 """Pre-market / post-open verification entrypoint for dry-run only execution."""
504- run_strategy (force_run = True , validation_only = True , validation_label = "precheck" )
505- return "Precheck OK" , 200
596+ return _route_with_runtime_error_fallback (
597+ run_strategy ,
598+ force_run = True ,
599+ validation_only = True ,
600+ validation_label = "precheck" ,
601+ success_body = "Precheck OK" ,
602+ route_label = "POST /precheck" ,
603+ )
506604
507605
508606@app .route ("/probe" , methods = ["POST" , "GET" ])
509607def handle_probe ():
510608 """Post-open broker/account health probe; notify only on failure."""
511- return run_probe ()
609+ return _route_with_runtime_error_fallback (
610+ run_probe ,
611+ success_body = "Probe OK" ,
612+ route_label = "POST /probe" ,
613+ )
512614
513615
514616if __name__ == "__main__" :
0 commit comments