1010import ccxt
1111
1212from app .data_sources .base import BaseDataSource , TIMEFRAME_SECONDS
13+ from app .data_sources .errors import MarketDataFailure , classify_market_data_failure
1314from app .utils .logger import get_logger
1415from app .config import CCXTConfig , APIKeys
1516
@@ -154,6 +155,7 @@ def __init__(self):
154155 self ._scoped_market_type = "spot"
155156 self ._preferred_public_exchange_id = ""
156157 self ._markets_load_lock = threading .Lock ()
158+ self ._failure_local = threading .local ()
157159 default_ex = (CCXTConfig .DEFAULT_EXCHANGE or "binance" ).strip ().lower ()
158160 if default_ex == "huobi" :
159161 default_ex = "htx"
@@ -178,6 +180,7 @@ def for_exchange(cls, exchange_id: str, market_type: str = "swap") -> "CryptoDat
178180 inst ._scoped_market_type = mt
179181 inst ._preferred_public_exchange_id = ""
180182 inst ._markets_load_lock = threading .Lock ()
183+ inst ._failure_local = threading .local ()
181184 inst ._init_ccxt_exchange (ccxt_id , options )
182185 _SCOPED_INSTANCES [cache_key ] = inst
183186 logger .info (
@@ -227,10 +230,43 @@ def for_public_market(
227230 inst ._scoped_market_type = mt
228231 inst ._preferred_public_exchange_id = ""
229232 inst ._markets_load_lock = threading .Lock ()
233+ inst ._failure_local = threading .local ()
230234 inst ._init_ccxt_exchange (ccxt_id , options )
231235 _PUBLIC_MARKET_INSTANCES [cache_key ] = inst
232236 return inst
233237
238+ def _clear_last_failure (self ) -> None :
239+ local = getattr (self , "_failure_local" , None )
240+ if local is None :
241+ local = threading .local ()
242+ self ._failure_local = local
243+ local .value = None
244+
245+ def _set_last_failure (
246+ self ,
247+ error : Any ,
248+ * ,
249+ symbol : str ,
250+ timeframe : str ,
251+ ) -> MarketDataFailure :
252+ failure = classify_market_data_failure (
253+ error ,
254+ exchange_id = getattr (self , "_scoped_exchange_id" , "" ) or getattr (self .exchange , "id" , "" ),
255+ market_type = getattr (self , "_scoped_market_type" , "" ) or "spot" ,
256+ symbol = symbol ,
257+ timeframe = timeframe ,
258+ )
259+ local = getattr (self , "_failure_local" , None )
260+ if local is None :
261+ local = threading .local ()
262+ self ._failure_local = local
263+ local .value = failure
264+ return failure
265+
266+ def get_last_failure (self ) -> Optional [MarketDataFailure ]:
267+ local = getattr (self , "_failure_local" , None )
268+ return getattr (local , "value" , None ) if local is not None else None
269+
234270 def _init_ccxt_exchange (self , ccxt_exchange_id : str , options : Optional [Dict [str , Any ]] = None ) -> None :
235271 config : Dict [str , Any ] = {
236272 "timeout" : CCXTConfig .TIMEOUT ,
@@ -503,6 +539,7 @@ def get_kline(
503539 after_time : Optional [int ] = None ,
504540 ) -> List [Dict [str , Any ]]:
505541 """获取加密货币K线数据"""
542+ self ._clear_last_failure ()
506543 klines = []
507544 symbol_pair = ""
508545
@@ -548,6 +585,11 @@ def get_kline(
548585 if exchange_timeframes and ccxt_timeframe not in exchange_timeframes :
549586 picked = self ._pick_resample_source (ccxt_timeframe , exchange_timeframes )
550587 if picked is None :
588+ self ._set_last_failure (
589+ f"Unsupported timeframe { ccxt_timeframe } on { self .exchange .id } " ,
590+ symbol = symbol ,
591+ timeframe = timeframe ,
592+ )
551593 logger .warning (
552594 f"Exchange '{ self .exchange .id } ' cannot serve timeframe '{ ccxt_timeframe } ' "
553595 f"and no finer supported granularity is available for resampling. "
@@ -568,10 +610,18 @@ def get_kline(
568610 symbol_pair = self ._symbol_for_scoped_market (symbol )
569611
570612 if not symbol_pair :
613+ self ._set_last_failure (
614+ f"Invalid symbol: { symbol } " , symbol = symbol , timeframe = timeframe
615+ )
571616 logger .warning (f"Failed to normalize symbol for K-line: { symbol } " )
572617 raise _PublicKlineUnavailable
573618
574619 if self ._is_invalid_symbol_cached (symbol_pair ):
620+ self ._set_last_failure (
621+ f"Symbol not found (cached): { symbol_pair } " ,
622+ symbol = symbol_pair ,
623+ timeframe = timeframe ,
624+ )
575625 raise _PublicKlineUnavailable
576626
577627 ohlcv = self ._fetch_ohlcv (
@@ -580,6 +630,12 @@ def get_kline(
580630 )
581631
582632 if not ohlcv :
633+ if self .get_last_failure () is None :
634+ self ._set_last_failure (
635+ "Exchange returned no K-line rows" ,
636+ symbol = symbol_pair ,
637+ timeframe = timeframe ,
638+ )
583639 logger .warning (f"CCXT returned no K-lines: { symbol_pair } " )
584640 raise _PublicKlineUnavailable
585641
@@ -628,8 +684,14 @@ def get_kline(
628684 pass
629685
630686 except _PublicKlineUnavailable :
631- pass
687+ if self .get_last_failure () is None :
688+ self ._set_last_failure (
689+ "No usable market data" ,
690+ symbol = symbol_pair or symbol ,
691+ timeframe = timeframe ,
692+ )
632693 except Exception as e :
694+ self ._set_last_failure (e , symbol = symbol_pair or symbol , timeframe = timeframe )
633695 logger .error (f"Failed to fetch crypto K-lines { symbol } : { str (e )} " )
634696 import traceback
635697 logger .error (traceback .format_exc ())
@@ -876,6 +938,7 @@ def _fetch_ohlcv(
876938 except Exception as e :
877939 if _is_symbol_not_found_error (e ):
878940 self ._mark_invalid_symbol (symbol_pair , e )
941+ self ._set_last_failure (e , symbol = symbol_pair , timeframe = timeframe )
879942 return []
880943 partial_rows = locals ().get ("all_ohlcv" ) or []
881944 if partial_rows :
@@ -889,6 +952,7 @@ def _fetch_ohlcv(
889952 by_ts = {int (row [0 ]): row for row in partial_rows if row and len (row ) >= 6 }
890953 return sorted (by_ts .values (), key = lambda row : row [0 ])
891954 logger .warning (f"CCXT fetch_ohlcv failed: { str (e )} ; trying fallback" )
955+ self ._set_last_failure (e , symbol = symbol_pair , timeframe = timeframe )
892956 return self ._fetch_ohlcv_fallback (
893957 symbol_pair , ccxt_timeframe , limit , before_time , timeframe , after_time
894958 )
@@ -935,7 +999,9 @@ def _fetch_ohlcv_fallback(
935999 except Exception as e :
9361000 if _is_symbol_not_found_error (e ):
9371001 self ._mark_invalid_symbol (symbol_pair , e )
1002+ self ._set_last_failure (e , symbol = symbol_pair , timeframe = timeframe )
9381003 return []
1004+ self ._set_last_failure (e , symbol = symbol_pair , timeframe = timeframe )
9391005 logger .warning ("Requested-window fallback failed for %s: %s" , symbol_pair , str (e ))
9401006
9411007 try :
@@ -957,4 +1023,5 @@ def _fetch_ohlcv_fallback(
9571023 self ._mark_invalid_symbol (symbol_pair , e )
9581024 else :
9591025 logger .error ("Recent-candle fallback also failed for %s: %s" , symbol_pair , str (e ))
1026+ self ._set_last_failure (e , symbol = symbol_pair , timeframe = timeframe )
9601027 return []
0 commit comments