Thang6822 commited on
Commit
c2c0884
Β·
1 Parent(s): 85d632d

Optimize requests: remove background prefetching, disable market peers, fix Binance NameError

Browse files
Files changed (1) hide show
  1. backend/main.py +150 -150
backend/main.py CHANGED
@@ -100,6 +100,9 @@ class Settings(BaseModel):
100
  alphavantage_api_key: Optional[str] = os.getenv("ALPHAVANTAGE_API_KEY")
101
  admin_token: str = os.getenv("ADMIN_TOKEN", "kronos_v6_default_secret")
102
 
 
 
 
103
  # App Config
104
  host: str = os.getenv("HOST", "0.0.0.0")
105
  port: int = int(os.getenv("PORT", 8000))
@@ -803,44 +806,60 @@ async def fetch_binance(symbol: str, interval: str, limit: int) -> List[Dict[str
803
  await _rate_limit("binance")
804
  cfg = SYMBOLS[symbol]
805
  endpoint_symbol = cfg.mappings["binance"]
806
- base_url = "https://fapi.binance.com" if cfg.binance_type == "futures" else "https://api.binance.com"
807
- endpoint = "/fapi/v1/klines" if cfg.binance_type == "futures" else "/api/v3/klines"
808
 
 
 
809
  params = {
810
  "symbol": endpoint_symbol,
811
- "interval": BINANCE_INTERVAL_MAP.get(interval, interval),
812
- "limit": min(max(limit, 30), 1000),
813
  }
814
- logger.info("[Binance] %s %s (%s)", symbol, interval, cfg.binance_type)
815
 
816
- async def _fetch():
817
- cb = source_breakers["binance"]
818
- if not cb.allow_request():
819
- raise HTTPException(status_code=503, detail="Binance circuit is OPEN")
820
-
 
 
 
 
 
 
 
 
821
  try:
822
- client = await GlobalHTTPClient.get_client()
823
- resp = await client.get(f"{base_url}{endpoint}", params=params)
824
- if resp.status_code == 429:
825
- cb.record_failure()
826
- raise HTTPException(status_code=429, detail="Binance rate limit")
827
- if resp.status_code >= 500:
828
- cb.record_failure()
829
- raise HTTPException(status_code=resp.status_code, detail="Binance server error")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830
 
831
- cb.record_success()
832
- return resp.json()
833
  except Exception as ex:
834
- cb.record_failure()
835
- raise ex
836
-
837
- payload = await _retry(_fetch)
838
- parsed = [
839
- {"time": int(k[0])//1000, "open": k[1], "high": k[2],
840
- "low": k[3], "close": k[4], "volume": k[5]}
841
- for k in payload
842
- ]
843
- return _normalize_ohlcv(parsed, interval)[-limit:]
844
 
845
 
846
  async def fetch_bybit(symbol: str, interval: str, limit: int) -> List[Dict[str, Any]]:
@@ -856,64 +875,69 @@ async def fetch_bybit(symbol: str, interval: str, limit: int) -> List[Dict[str,
856
  }
857
  logger.info("[Bybit] %s %s (cat=%s)", symbol, interval, bybit_cat)
858
 
859
- async def _fetch():
860
- cb = source_breakers["bybit"]
861
- if not cb.allow_request():
862
- raise HTTPException(status_code=503, detail="Bybit circuit is OPEN")
863
-
 
 
 
864
  try:
865
- client = await GlobalHTTPClient.get_client()
866
- url = "https://api.bybit.com/v5/market/kline"
867
-
868
- # Authenticate if keys are available
869
- headers = {}
870
- query_params = params.copy()
871
-
872
- if settings.bybit_api_key and settings.bybit_api_secret:
873
- timestamp = str(int(time.time() * 1000))
874
- recv_window = "5000"
875
- # For GET, sort params alphabetically and join
876
- sorted_params = "&".join([f"{k}={v}" for k, v in sorted(query_params.items())])
877
- raw_str = timestamp + settings.bybit_api_key + recv_window + sorted_params
878
- signature = hmac.new(settings.bybit_api_secret.encode('utf-8'),
879
- raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
880
 
881
- headers = {
882
- 'X-BAPI-API-KEY': settings.bybit_api_key,
883
- 'X-BAPI-TIMESTAMP': timestamp,
884
- 'X-BAPI-SIGN-TYPE': '2',
885
- 'X-BAPI-RECV-WINDOW': recv_window,
886
- 'X-BAPI-SIGN': signature
887
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
888
 
889
- resp = await client.get(url, params=query_params, headers=headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
 
891
- if resp.status_code == 429:
892
- cb.record_failure()
893
- raise HTTPException(status_code=429, detail="Bybit rate limit")
894
- if resp.status_code >= 500:
895
- cb.record_failure()
896
- raise HTTPException(status_code=resp.status_code, detail="Bybit server error")
897
-
898
- cb.record_success()
899
- return resp.json()
900
  except Exception as ex:
901
- cb.record_failure()
902
- raise ex
903
-
904
- data = await _retry(_fetch)
905
- if data.get("retCode", -1) != 0:
906
- raise RuntimeError(f"Bybit: {data}")
907
 
908
- rows = data.get("result", {}).get("list", [])
909
- # Bybit returns: [startTime, open, high, low, close, volume, turnover]
910
- parsed = [
911
- {"time": int(r[0])//1000, "open": r[1], "high": r[2],
912
- "low": r[3], "close": r[4], "volume": r[5]}
913
- for r in rows
914
- ]
915
- parsed.reverse() # Bybit: newest first
916
- return _normalize_ohlcv(parsed, interval)[-limit:]
917
 
918
 
919
  async def fetch_coingecko(symbol: str, interval: str, limit: int) -> List[Dict[str, Any]]:
@@ -2560,7 +2584,6 @@ async def lifespan(app: FastAPI):
2560
  asyncio.create_task(ws_manager.heartbeat())
2561
  asyncio.create_task(_background_cleanup())
2562
  asyncio.create_task(_periodic_health_check())
2563
- asyncio.create_task(_prefetch_popular_symbols()) # D-3: Prefetcher
2564
 
2565
  # F-2: Rate limit cleanup task
2566
  async def _ip_cleanup_loop():
@@ -2948,26 +2971,6 @@ async def rate_limit_middleware(request: Request, call_next):
2948
  return await call_next(request)
2949
 
2950
 
2951
- RECENT_SYMBOLS: List[str] = []
2952
-
2953
- async def _prefetch_popular_symbols():
2954
- """D-3: Prefetch data for major symbols and active session symbols (P2)."""
2955
- static_popular = ["XAUUSD", "BTCUSD", "ETHUSD", "DXY", "SP500"]
2956
- while True:
2957
- # Merge static favorites with recently viewed symbols
2958
- targets = list(dict.fromkeys(RECENT_SYMBOLS + static_popular))[:10]
2959
-
2960
- logger.info("[Prefetch] Refreshing symbols: %s", targets)
2961
- for sym in targets:
2962
- try:
2963
- # Fetch 1h and 1d to warm up both indicator contexts
2964
- await fetch_historical(sym, "1h", 300)
2965
- await asyncio.sleep(0.5)
2966
- await fetch_historical(sym, "1d", 200)
2967
- await asyncio.sleep(1.0) # Gentle throttling
2968
- except Exception:
2969
- pass
2970
- await asyncio.sleep(300) # Every 5 mins
2971
 
2972
  async def _source_selftest():
2973
  """Ping data sources at startup to confirm reachability (1 attempt each)."""
@@ -3062,49 +3065,51 @@ async def list_symbols(
3062
 
3063
 
3064
  # ── Market Peers ──────────────────────────────────────────────────────────────
3065
- @app.get("/api/market-peers")
3066
- async def get_market_peers(symbol: str = Query("BTCUSD")) -> Dict[str, Any]:
3067
- # Fix: Ensure symbol is canonical to avoid category mismatches
3068
- symbol = _get_canonical_symbol(symbol.upper())
3069
-
3070
- if symbol not in SYMBOLS:
3071
- # Fallback to Crypto if unknown
3072
- return await crypto_market(top=10)
3073
-
3074
- cfg = SYMBOLS[symbol]
3075
- category = cfg.category
3076
-
3077
- # Find all peers in the same category
3078
- peers_list = [s for s in SYMBOLS.values() if s.category == category and s.symbol != symbol]
3079
-
3080
- # If too few peers in category, mix with others
3081
- if len(peers_list) < 3:
3082
- all_others = [s for s in SYMBOLS.values() if s.symbol != symbol]
3083
- peers_list.extend(all_others[:5])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3084
 
3085
- # Format result with actual price data
3086
- result_peers = []
3087
- # Batch fetch ticker data for peers
3088
- for p in peers_list[:12]:
3089
- try:
3090
- ticker = await fetch_ticker(p.symbol)
3091
- result_peers.append({
3092
- "symbol": p.symbol,
3093
- "label": p.label,
3094
- "category": p.category,
3095
- "price": ticker.get("price", 0),
3096
- "change_24h": ticker.get("change_pct", 0)
3097
- })
3098
- except:
3099
- result_peers.append({
3100
- "symbol": p.symbol, "label": p.label, "category": p.category,
3101
- "price": 0, "change_24h": 0
3102
- })
3103
-
3104
- return {
3105
- "category": category,
3106
- "peers": result_peers
3107
- }
3108
 
3109
 
3110
  # ── Symbol search ──────���──────────────────────────────────────────────────────
@@ -3500,11 +3505,6 @@ async def switch_symbol_interval(body: SwitchRequest) -> Dict[str, Any]:
3500
  h_cleared = historical_cache.delete_by_prefix(f"hist_{prefix}")
3501
  f_cleared = forecast_cache.delete_by_prefix(f"forecast_{prefix}")
3502
 
3503
- # P2: Track recent symbols for pre-fetching
3504
- global RECENT_SYMBOLS
3505
- if symbol not in RECENT_SYMBOLS:
3506
- RECENT_SYMBOLS.insert(0, symbol)
3507
- RECENT_SYMBOLS = RECENT_SYMBOLS[:5] # Keep top 5
3508
 
3509
  logger.info("[switch] %s %s β†’ hist=%d forecast=%d", symbol, interval, h_cleared, f_cleared)
3510
 
 
100
  alphavantage_api_key: Optional[str] = os.getenv("ALPHAVANTAGE_API_KEY")
101
  admin_token: str = os.getenv("ADMIN_TOKEN", "kronos_v6_default_secret")
102
 
103
+ # Environment detection
104
+ is_hf: bool = os.getenv("SPACE_ID") is not None
105
+
106
  # App Config
107
  host: str = os.getenv("HOST", "0.0.0.0")
108
  port: int = int(os.getenv("PORT", 8000))
 
806
  await _rate_limit("binance")
807
  cfg = SYMBOLS[symbol]
808
  endpoint_symbol = cfg.mappings["binance"]
 
 
809
 
810
+ # Define request details
811
+ endpoint = "/fapi/v1/klines" if cfg.binance_type == "futures" else "/api/v3/klines"
812
  params = {
813
  "symbol": endpoint_symbol,
814
+ "interval": BINANCE_INTERVAL_MAP.get(interval, "1h"),
815
+ "limit": min(max(limit, 1), 1000),
816
  }
 
817
 
818
+ # B-2: Endpoint Rotation for HF/Cloud environments
819
+ endpoints_spot = ["https://api.binance.com", "https://api1.binance.com", "https://api2.binance.com", "https://api3.binance.com", "https://data-api.binance.com"]
820
+ endpoints_fapi = ["https://fapi.binance.com"] # fapi usually more restricted, but try first
821
+
822
+ selected_endpoints = endpoints_fapi if cfg.binance_type == "futures" else endpoints_spot
823
+
824
+ # If on HF, we know api.binance.com is likely blocked, so we can try data-api or alternates faster
825
+ if settings.is_hf and cfg.binance_type == "spot":
826
+ # Move data-api to the front for HF spot
827
+ selected_endpoints = ["https://data-api.binance.com", "https://api1.binance.com", "https://api2.binance.com", "https://api3.binance.com", "https://api.binance.com"]
828
+
829
+ last_error = None
830
+ for base_url in selected_endpoints:
831
  try:
832
+ cb = source_breakers["binance"]
833
+ if not cb.allow_request():
834
+ continue # Try next endpoint or fall through
835
+
836
+ async def _do_fetch():
837
+ client = await GlobalHTTPClient.get_client()
838
+ resp = await client.get(f"{base_url}{endpoint}", params=params, timeout=10.0)
839
+ if resp.status_code == 451:
840
+ logger.warning("[Binance] Endpoint %s blocked (451). Trying next...", base_url)
841
+ raise RuntimeError("IP Blocked")
842
+ if resp.status_code == 429:
843
+ raise HTTPException(status_code=429, detail="Binance rate limit")
844
+ resp.raise_for_status()
845
+ cb.record_success()
846
+ return resp.json()
847
+
848
+ payload = await _retry(_do_fetch)
849
+ # If successful, parse and return
850
+ parsed = [
851
+ {"time": int(k[0])//1000, "open": k[1], "high": k[2],
852
+ "low": k[3], "close": k[4], "volume": k[5]}
853
+ for k in payload
854
+ ]
855
+ return _normalize_ohlcv(parsed, interval)[-limit:]
856
 
 
 
857
  except Exception as ex:
858
+ last_error = ex
859
+ logger.error("[Binance] Failed with %s: %s", base_url, ex)
860
+ continue
861
+
862
+ raise last_error or HTTPException(status_code=503, detail="Binance all endpoints failed")
 
 
 
 
 
863
 
864
 
865
  async def fetch_bybit(symbol: str, interval: str, limit: int) -> List[Dict[str, Any]]:
 
875
  }
876
  logger.info("[Bybit] %s %s (cat=%s)", symbol, interval, bybit_cat)
877
 
878
+ # B-3: Endpoint Rotation for Bybit
879
+ bybit_endpoints = ["https://api.bybit.com", "https://api.bytick.com", "https://api.bybit.nl"]
880
+ if settings.is_hf:
881
+ # Prefer bytick on HF
882
+ bybit_endpoints = ["https://api.bytick.com", "https://api.bybit.com", "https://api.bybit.nl"]
883
+
884
+ last_error = None
885
+ for base_url in bybit_endpoints:
886
  try:
887
+ cb = source_breakers["bybit"]
888
+ if not cb.allow_request():
889
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
890
 
891
+ async def _do_fetch():
892
+ client = await GlobalHTTPClient.get_client()
893
+ url = f"{base_url}/v5/market/kline"
894
+
895
+ headers = {}
896
+ query_params = params.copy()
897
+
898
+ if settings.bybit_api_key and settings.bybit_api_secret:
899
+ timestamp = str(int(time.time() * 1000))
900
+ recv_window = "5000"
901
+ sorted_params = "&".join([f"{k}={v}" for k, v in sorted(query_params.items())])
902
+ raw_str = timestamp + settings.bybit_api_key + recv_window + sorted_params
903
+ signature = hmac.new(settings.bybit_api_secret.encode('utf-8'),
904
+ raw_str.encode('utf-8'), hashlib.sha256).hexdigest()
905
+
906
+ headers = {
907
+ 'X-BAPI-API-KEY': settings.bybit_api_key,
908
+ 'X-BAPI-TIMESTAMP': timestamp,
909
+ 'X-BAPI-SIGN-TYPE': '2',
910
+ 'X-BAPI-RECV-WINDOW': recv_window,
911
+ 'X-BAPI-SIGN': signature
912
+ }
913
 
914
+ resp = await client.get(url, params=query_params, headers=headers, timeout=10.0)
915
+ if resp.status_code == 403:
916
+ logger.warning("[Bybit] Endpoint %s forbidden (403). Trying next...", base_url)
917
+ raise RuntimeError("IP Blocked")
918
+ resp.raise_for_status()
919
+ cb.record_success()
920
+ return resp.json()
921
+
922
+ data = await _retry(_do_fetch)
923
+ if data.get("retCode", -1) != 0:
924
+ raise RuntimeError(f"Bybit Error: {data}")
925
+
926
+ rows = data.get("result", {}).get("list", [])
927
+ parsed = [
928
+ {"time": int(r[0])//1000, "open": r[1], "high": r[2],
929
+ "low": r[3], "close": r[4], "volume": r[5]}
930
+ for r in rows
931
+ ]
932
+ parsed.reverse()
933
+ return _normalize_ohlcv(parsed, interval)[-limit:]
934
 
 
 
 
 
 
 
 
 
 
935
  except Exception as ex:
936
+ last_error = ex
937
+ logger.error("[Bybit] Failed with %s: %s", base_url, ex)
938
+ continue
 
 
 
939
 
940
+ raise last_error or HTTPException(status_code=503, detail="Bybit all endpoints failed")
 
 
 
 
 
 
 
 
941
 
942
 
943
  async def fetch_coingecko(symbol: str, interval: str, limit: int) -> List[Dict[str, Any]]:
 
2584
  asyncio.create_task(ws_manager.heartbeat())
2585
  asyncio.create_task(_background_cleanup())
2586
  asyncio.create_task(_periodic_health_check())
 
2587
 
2588
  # F-2: Rate limit cleanup task
2589
  async def _ip_cleanup_loop():
 
2971
  return await call_next(request)
2972
 
2973
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2974
 
2975
  async def _source_selftest():
2976
  """Ping data sources at startup to confirm reachability (1 attempt each)."""
 
3065
 
3066
 
3067
  # ── Market Peers ──────────────────────────────────────────────────────────────
3068
+ # @app.get("/api/market-peers")
3069
+ # async def get_market_peers(symbol: str = Query("BTCUSD")) -> Dict[str, Any]:
3070
+ # # Fix: Ensure symbol is canonical to avoid category mismatches
3071
+ # symbol = _get_canonical_symbol(symbol.upper())
3072
+ #
3073
+ # if symbol not in SYMBOLS:
3074
+ # # Fallback to Crypto if unknown
3075
+ # # return await crypto_market(top=10)
3076
+ # return {"category": "Unknown", "peers": []}
3077
+ #
3078
+ # cfg = SYMBOLS[symbol]
3079
+ # category = cfg.category
3080
+ #
3081
+ # # Find all peers in the same category
3082
+ # peers_list = [s for s in SYMBOLS.values() if s.category == category and s.symbol != symbol]
3083
+ #
3084
+ # # If too few peers in category, mix with others
3085
+ # if len(peers_list) < 3:
3086
+ # all_others = [s for s in SYMBOLS.values() if s.symbol != symbol]
3087
+ # peers_list.extend(all_others[:5])
3088
+ #
3089
+ # # Format result with actual price data
3090
+ # result_peers = []
3091
+ # # Batch fetch ticker data for peers
3092
+ # for p in peers_list[:12]:
3093
+ # try:
3094
+ # ticker = await fetch_ticker(p.symbol)
3095
+ # result_peers.append({
3096
+ # "symbol": p.symbol,
3097
+ # "label": p.label,
3098
+ # "category": p.category,
3099
+ # "price": ticker.get("price", 0),
3100
+ # "change_24h": ticker.get("change_pct", 0)
3101
+ # })
3102
+ # except:
3103
+ # result_peers.append({
3104
+ # "symbol": p.symbol, "label": p.label, "category": p.category,
3105
+ # "price": 0, "change_24h": 0
3106
+ # })
3107
+ #
3108
+ # return {
3109
+ # "category": category,
3110
+ # "peers": result_peers
3111
+ # }
3112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3113
 
3114
 
3115
  # ── Symbol search ──────���──────────────────────────────────────────────────────
 
3505
  h_cleared = historical_cache.delete_by_prefix(f"hist_{prefix}")
3506
  f_cleared = forecast_cache.delete_by_prefix(f"forecast_{prefix}")
3507
 
 
 
 
 
 
3508
 
3509
  logger.info("[switch] %s %s β†’ hist=%d forecast=%d", symbol, interval, h_cleared, f_cleared)
3510