Spaces:
Running
Running
Thang6822 commited on
Commit Β·
c2c0884
1
Parent(s): 85d632d
Optimize requests: remove background prefetching, disable market peers, fix Binance NameError
Browse files- 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,
|
| 812 |
-
"limit": min(max(limit,
|
| 813 |
}
|
| 814 |
-
logger.info("[Binance] %s %s (%s)", symbol, interval, cfg.binance_type)
|
| 815 |
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
try:
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
|
| 831 |
-
cb.record_success()
|
| 832 |
-
return resp.json()
|
| 833 |
except Exception as ex:
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 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 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
|
|
|
|
|
|
|
|
|
| 864 |
try:
|
| 865 |
-
|
| 866 |
-
|
| 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 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
|
| 889 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
data = await _retry(_fetch)
|
| 905 |
-
if data.get("retCode", -1) != 0:
|
| 906 |
-
raise RuntimeError(f"Bybit: {data}")
|
| 907 |
|
| 908 |
-
|
| 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 |
-
|
| 3068 |
-
|
| 3069 |
-
|
| 3070 |
-
|
| 3071 |
-
|
| 3072 |
-
|
| 3073 |
-
|
| 3074 |
-
|
| 3075 |
-
|
| 3076 |
-
|
| 3077 |
-
|
| 3078 |
-
|
| 3079 |
-
|
| 3080 |
-
|
| 3081 |
-
|
| 3082 |
-
|
| 3083 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|