"""多周期 K 线:优先 SQLite,不足或过期再请求币安。""" import logging from datetime import datetime from .binance import binance_client from .chart_intervals import ( CHART_INTERVALS, cache_minutes_for_interval, limit_for_interval, validate_interval, ) from .db import get_kline_meta, get_klines_from_db, save_klines from .exceptions import BinanceRateLimitedError logger = logging.getLogger(__name__) def _is_db_fresh(symbol: str, interval: str, min_bars: int) -> bool: meta = get_kline_meta(symbol, interval) if not meta or meta.get("bar_count", 0) < min_bars: return False try: last_fetch = datetime.fromisoformat(meta["last_fetch_at"]) except ValueError: return False age = (datetime.now() - last_fetch).total_seconds() return age < cache_minutes_for_interval(interval) * 60 async def sync_klines( symbol: str, interval: str, limit: int | None = None ) -> list[dict]: """从币安拉取指定周期并写入本地库。""" sym = symbol.upper() iv = validate_interval(interval) n = min(limit or limit_for_interval(iv), 1500) candles = await binance_client.get_klines_limit(sym, iv, n) save_klines(sym, iv, candles) logger.info("Saved %d %s klines for %s to DB", len(candles), iv, sym) return candles async def sync_daily_klines(symbol: str, limit: int | None = None) -> list[dict]: return await sync_klines(symbol, "1d", limit) async def get_candles( symbol: str, interval: str = "1d", limit: int | None = None, force_refresh: bool = False, ) -> tuple[list[dict], str]: """ 返回 (candles, source)。 source: db | db_stale | binance """ sym = symbol.upper().strip() iv = validate_interval(interval) n = min(limit or limit_for_interval(iv), 1500) min_bars = min(n, 50) if not force_refresh and _is_db_fresh(sym, iv, min_bars): candles = get_klines_from_db(sym, iv, n) if len(candles) >= min_bars: return candles, "db" stored = get_klines_from_db(sym, iv, n) if binance_client.is_rate_limited(): if stored: logger.warning("Rate limited, serve stale DB klines for %s %s", sym, iv) return stored, "db_stale" raise BinanceRateLimitedError(binance_client.rate_limit_remaining_sec(), sym) try: candles = await sync_klines(sym, iv, n) return candles, "binance" except BinanceRateLimitedError: if stored: return stored, "db_stale" raise except Exception: if stored: return stored, "db_stale" raise async def get_daily_candles( symbol: str, limit: int | None = None, force_refresh: bool = False, ) -> tuple[list[dict], str]: return await get_candles(symbol, "1d", limit, force_refresh) async def prefetch_symbols(symbols: list[str]) -> None: """后台预拉 Top 币种全周期 K 线入库(串行,避免 418)。""" seen: set[str] = set() for raw in symbols: sym = raw.upper().strip() if not sym or sym in seen or not sym.endswith("USDT"): continue seen.add(sym) for interval in CHART_INTERVALS: n = limit_for_interval(interval) if _is_db_fresh(sym, interval, min(50, n)): continue if binance_client.is_rate_limited(): logger.warning("Prefetch stopped — rate limited") return try: await sync_klines(sym, interval, n) except BinanceRateLimitedError: logger.warning("Prefetch rate limited at %s %s", sym, interval) return except Exception as e: logger.warning("Prefetch %s %s failed: %s", sym, interval, e)