3939from app .utils .db import get_db_connection
4040from app .utils .logger import get_logger
4141from app .utils .strategy_runtime_logs import append_strategy_log
42+ from app .services .strategy_lifecycle import (
43+ auto_stop_live_strategy ,
44+ is_fatal_exchange_error ,
45+ should_skip_position_sync ,
46+ )
4247
4348# Lazy import IBKR to avoid ImportError if ib_insync not installed
4449IBKRClient = None
@@ -194,7 +199,9 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
194199 logger .debug (f"[PositionSync] Found { len (active_rows )} active live strategies in DB." )
195200 for _ar in active_rows :
196201 _sid = int (_ar .get ("id" ) or 0 )
197- if _sid > 0 and _sid not in sid_to_rows :
202+ if _sid <= 0 or should_skip_position_sync (_sid ):
203+ continue
204+ if _sid not in sid_to_rows :
198205 if target_strategy_id and target_strategy_id != _sid :
199206 continue
200207 sid_to_rows [_sid ] = []
@@ -205,6 +212,8 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
205212 for sid , plist in sid_to_rows .items ():
206213 if target_strategy_id and sid != target_strategy_id :
207214 continue
215+ if should_skip_position_sync (int (sid )):
216+ continue
208217 try :
209218 sc = load_strategy_configs (int (sid ))
210219 exec_mode = (sc .get ("execution_mode" ) or "" ).strip ().lower ()
@@ -272,7 +281,18 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
272281 try :
273282 client = create_client (exchange_config , market_type = market_type )
274283 except Exception as e :
275- logger .debug (f"[PositionSync] Strategy { sid } skipped: failed to create client (exchange_id={ exchange_id } ): { e } " )
284+ msg = str (e )
285+ if is_fatal_exchange_error (msg ):
286+ logger .error (
287+ "[PositionSync] Strategy %s fatal client error; auto-stopping. error=%s" ,
288+ sid ,
289+ msg ,
290+ )
291+ auto_stop_live_strategy (int (sid ), msg , source = "position_sync_client" )
292+ else :
293+ logger .debug (
294+ f"[PositionSync] Strategy { sid } skipped: failed to create client (exchange_id={ exchange_id } ): { e } "
295+ )
276296 continue
277297
278298 # Build an "exchange snapshot" per symbol+side
@@ -287,35 +307,9 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
287307 m = msg .lower ()
288308 # Fatal auth/config errors should auto-stop the strategy to avoid endless spam.
289309 # Typical Binance response: HTTP 401 {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action"}
290- is_fatal_auth = (
291- "binance http 401" in m
292- or "invalid api-key" in m
293- or "invalid api key" in m
294- or "permissions for action" in m
295- or '"code":-2015' in m
296- or "-2015" in m
297- or "unauthorized" in m
298- or "forbidden" in m
299- or "signature" in m
300- )
301- if is_fatal_auth :
310+ if is_fatal_exchange_error (msg ):
302311 logger .error (f"[PositionSync] Strategy { sid } fatal auth error; auto-stopping. error={ msg } " )
303- try :
304- from app .utils .strategy_runtime_logs import append_strategy_log
305- append_strategy_log (int (sid ), "error" , f"Auto-stopped: Binance auth failed: { msg } " )
306- except Exception :
307- pass
308- try :
309- with get_db_connection () as db :
310- cur = db .cursor ()
311- cur .execute (
312- "UPDATE qd_strategies_trading SET status = 'stopped' WHERE id = %s AND status = 'running'" ,
313- (int (sid ),),
314- )
315- db .commit ()
316- cur .close ()
317- except Exception :
318- pass
312+ auto_stop_live_strategy (int (sid ), msg , source = "position_sync_binance" )
319313 continue
320314 logger .error (f"[PositionSync] Strategy { sid } get_positions failed: { msg } " , exc_info = True )
321315 continue
@@ -350,33 +344,9 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
350344 # Typical OKX response: HTTP 401 {"msg":"Invalid OK-ACCESS-KEY","code":"50111"}
351345 msg = str (e )
352346 m = msg .lower ()
353- is_fatal_auth = (
354- "okx http 401" in m
355- or "invalid ok-access-key" in m
356- or '"code":"50111"' in m
357- or "50111" in m
358- or "invalid api" in m
359- or "unauthorized" in m
360- or "authentication" in m
361- )
362- if is_fatal_auth :
347+ if is_fatal_exchange_error (msg ):
363348 logger .error (f"[PositionSync] Strategy { sid } fatal auth error; auto-stopping. error={ msg } " )
364- try :
365- from app .utils .strategy_runtime_logs import append_strategy_log
366- append_strategy_log (int (sid ), "error" , f"Auto-stopped: OKX auth failed: { msg } " )
367- except Exception :
368- pass
369- try :
370- with get_db_connection () as db :
371- cur = db .cursor ()
372- cur .execute (
373- "UPDATE qd_strategies_trading SET status = 'stopped' WHERE id = %s AND status = 'running'" ,
374- (int (sid ),),
375- )
376- db .commit ()
377- cur .close ()
378- except Exception :
379- pass
349+ auto_stop_live_strategy (int (sid ), msg , source = "position_sync_okx" )
380350 continue
381351 # Non-fatal: keep syncing other strategies, but don't crash the worker loop.
382352 logger .error (f"[PositionSync] Strategy { sid } get_positions failed: { msg } " , exc_info = True )
@@ -603,7 +573,16 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
603573 try :
604574 positions = client .get_positions () or []
605575 except Exception as e :
606- logger .error (f"[PositionSync] Strategy { sid } IBKR get_positions failed: { e } " , exc_info = True )
576+ msg = str (e )
577+ if is_fatal_exchange_error (msg ):
578+ logger .error (
579+ "[PositionSync] Strategy %s IBKR fatal error; auto-stopping. error=%s" ,
580+ sid ,
581+ msg ,
582+ )
583+ auto_stop_live_strategy (int (sid ), msg , source = "position_sync_ibkr" )
584+ else :
585+ logger .error (f"[PositionSync] Strategy { sid } IBKR get_positions failed: { e } " , exc_info = True )
607586 continue
608587 if isinstance (positions , list ):
609588 for p in positions :
@@ -794,45 +773,10 @@ def _sync_positions_best_effort(self, target_strategy_id: Optional[int] = None)
794773 if to_insert :
795774 logger .debug (f"position sync: inserted { len (to_insert )} new positions for strategy_id={ sid } " )
796775 except Exception as e :
797- # Any exchange auth/config/connectivity fatal errors should stop the strategy to avoid endless spam.
798776 msg = str (e )
799- m = msg .lower ()
800- is_fatal = any (tok in m for tok in (
801- # Generic HTTP auth / permission failures
802- " http 401" ,
803- "unauthorized" ,
804- "forbidden" ,
805- "invalid api" ,
806- "invalid api-key" ,
807- "invalid api key" ,
808- "invalid_key" ,
809- "invalid key" ,
810- "signature mismatch" ,
811- "invalid_signature" ,
812- "permission" ,
813- # Common connection failures (IBKR/TWS, local gateways)
814- "connection refused" ,
815- "connect call failed" ,
816- "errno 111" ,
817- ))
818- if is_fatal :
777+ if is_fatal_exchange_error (msg ):
819778 logger .error (f"[PositionSync] Strategy { sid } fatal error; auto-stopping. error={ msg } " , exc_info = True )
820- try :
821- from app .utils .strategy_runtime_logs import append_strategy_log
822- append_strategy_log (int (sid ), "error" , f"Auto-stopped: position sync fatal error: { msg } " )
823- except Exception :
824- pass
825- try :
826- with get_db_connection () as db :
827- cur = db .cursor ()
828- cur .execute (
829- "UPDATE qd_strategies_trading SET status = 'stopped' WHERE id = %s AND status = 'running'" ,
830- (int (sid ),),
831- )
832- db .commit ()
833- cur .close ()
834- except Exception :
835- pass
779+ auto_stop_live_strategy (int (sid ), msg , source = "position_sync" )
836780 else :
837781 logger .error (f"position sync: strategy_id={ sid } failed: { e } " , exc_info = True )
838782
@@ -2424,34 +2368,8 @@ def _execute_ibkr_order(
24242368 _console_print (f"[worker] IBKR order exception: strategy_id={ strategy_id } pending_id={ order_id } err={ e } " )
24252369 _notify_live_best_effort (status = "failed" , error = str (e ))
24262370 append_strategy_log (strategy_id , "error" , f"IBKR order exception ({ symbol } { signal_type } ): { e } " )
2427- # Auto-stop strategy on fatal connectivity issues (e.g. TWS/IBG not reachable).
2428- try :
2429- msg = str (e )
2430- m = msg .lower ()
2431- is_fatal = any (tok in m for tok in (
2432- "connection refused" ,
2433- "connect call failed" ,
2434- "errno 111" ,
2435- "make sure api port on tws" ,
2436- ))
2437- if is_fatal :
2438- try :
2439- append_strategy_log (strategy_id , "error" , f"Auto-stopped: IBKR connection failed: { msg } " )
2440- except Exception :
2441- pass
2442- try :
2443- with get_db_connection () as db :
2444- cur = db .cursor ()
2445- cur .execute (
2446- "UPDATE qd_strategies_trading SET status = 'stopped' WHERE id = %s AND status = 'running'" ,
2447- (int (strategy_id ),),
2448- )
2449- db .commit ()
2450- cur .close ()
2451- except Exception :
2452- pass
2453- except Exception :
2454- pass
2371+ if is_fatal_exchange_error (str (e )):
2372+ auto_stop_live_strategy (int (strategy_id ), str (e ), source = "ibkr_order" )
24552373
24562374 def _execute_alpaca_order (
24572375 self ,
0 commit comments