@@ -668,7 +668,14 @@ def ws_connection_reset(self):
668668
669669 async def _api_reload_markets (self , reload : bool = False ) -> None :
670670 try :
671- await self ._api_async .load_markets (reload = reload , params = {})
671+ # HACK: Use a short timeout (5s) so offline backtesting doesn't hang
672+ import asyncio as _asyncio
673+ await _asyncio .wait_for (
674+ self ._api_async .load_markets (reload = reload , params = {}),
675+ timeout = 5.0
676+ )
677+ except (TimeoutError , _asyncio .TimeoutError ) as e :
678+ raise TemporaryError (f"Market loading timed out: { e } " ) from e
672679 except ccxt .DDoSProtection as e :
673680 raise DDosProtection (e ) from e
674681 except (ccxt .OperationFailed , ccxt .ExchangeError ) as e :
@@ -704,21 +711,29 @@ def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = Tru
704711 ):
705712 return None
706713 logger .debug ("Performing scheduled market reload.." )
714+ exchange_loaded = False
707715 try :
708716 # on initial load, we retry 3 times to ensure we get the markets
709- retries : int = 3 if force else 0
717+ # HACK: Use 0 retries so backtesting works offline without long delays
718+ retries : int = 0
710719 # Reload async markets, then assign them to sync api
711720 retrier (self ._load_async_markets , retries = retries )(reload = True )
712721 self ._markets = self ._api_async .markets
722+ exchange_loaded = True
723+ except (ccxt .BaseError , TemporaryError ):
724+ logger .warning ("Could not load markets from exchange, will use locally injected pairs." )
713725
726+ try :
714727 # HACK: Allow stocks to get custom data
728+ # This also serves as a fallback when the exchange is unreachable:
729+ # inject ALL configured pairs so backtesting works with local data.
715730 whitelist = self ._config .get ('exchange' , {}).get ('pair_whitelist' , [])
716731 cli_pairs = self ._config .get ('pairs' , [])
717-
732+
718733 all_pairs = set (whitelist + cli_pairs )
719734 for pair in all_pairs :
720735 if pair not in self ._markets :
721- logger .info (f"Auto-injecting stock pair: { pair } " )
736+ logger .info (f"Auto-injecting pair: { pair } " )
722737 self ._markets [pair ] = {
723738 'symbol' : pair ,
724739 'base' : pair .split ('/' )[0 ],
@@ -741,21 +756,26 @@ def reload_markets(self, force: bool = False, *, load_leverage_tiers: bool = Tru
741756 'info' : {}
742757 }
743758
744- self ._api .set_markets_from_exchange (self ._api_async )
745-
746- # Assign options array, as it contains some temporary information from the exchange.
747- # ccxt does not implicitly copy options over in set_markets_from_exchange
748- self ._api .options = self ._api_async .options
749- if self ._exchange_ws :
750- # Set markets to avoid reloading on websocket api
751- self ._ws_async .set_markets_from_exchange (self ._api_async )
752- self ._ws_async .options = self ._api .options
759+ if exchange_loaded :
760+ self ._api .set_markets_from_exchange (self ._api_async )
761+
762+ # Assign options array, as it contains some temporary information from the exchange.
763+ # ccxt does not implicitly copy options over in set_markets_from_exchange
764+ self ._api .options = self ._api_async .options
765+ if self ._exchange_ws :
766+ # Set markets to avoid reloading on websocket api
767+ self ._ws_async .set_markets_from_exchange (self ._api_async )
768+ self ._ws_async .options = self ._api .options
769+
770+ # Also sync injected markets to the sync api
771+ self ._api .markets = self ._markets
772+
753773 self ._last_markets_refresh = dt_ts ()
754774
755- if is_initial and self ._ft_has ["needs_trading_fees" ]:
775+ if exchange_loaded and is_initial and self ._ft_has ["needs_trading_fees" ]:
756776 self ._trading_fees = self .fetch_trading_fees ()
757777
758- if load_leverage_tiers and self .trading_mode == TradingMode .FUTURES :
778+ if exchange_loaded and load_leverage_tiers and self .trading_mode == TradingMode .FUTURES :
759779 self .fill_leverage_tiers ()
760780 except (ccxt .BaseError , TemporaryError ):
761781 logger .exception ("Could not load markets." )
0 commit comments