"""日 K 线:优先 SQLite 本地库,不足或过期再请求币安。""" import logging from datetime import datetime from .binance import binance_client from .config import settings from .db import get_daily_klines_from_db, get_kline_meta, save_daily_klines from .exceptions import BinanceRateLimitedError logger = logging.getLogger(__name__) def _is_db_fresh(symbol: str, min_bars: int) -> bool: meta = get_kline_meta(symbol) 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 < settings.chart_cache_minutes * 60 async def sync_daily_klines(symbol: str, limit: int | None = None) -> list[dict]: """从币安拉取并写入本地库。""" sym = symbol.upper() n = min(limit or settings.chart_kline_limit, 1500) candles = await binance_client.get_daily_klines(sym, n) save_daily_klines(sym, candles) logger.info("Saved %d daily klines for %s to DB", len(candles), sym) return candles async def get_daily_candles( symbol: str, limit: int | None = None, force_refresh: bool = False, ) -> tuple[list[dict], str]: """ 返回 (candles, source)。 source: db | db_stale | binance """ sym = symbol.upper().strip() n = min(limit or settings.chart_kline_limit, 1500) min_bars = min(n, 50) if not force_refresh and _is_db_fresh(sym, min_bars): candles = get_daily_klines_from_db(sym, n) if len(candles) >= min_bars: return candles, "db" stored = get_daily_klines_from_db(sym, n) if binance_client.is_rate_limited(): if stored: logger.warning("Rate limited, serve stale DB klines for %s", sym) return stored, "db_stale" raise BinanceRateLimitedError(binance_client.rate_limit_remaining_sec(), sym) try: candles = await sync_daily_klines(sym, n) return candles, "binance" except BinanceRateLimitedError: if stored: return stored, "db_stale" raise except Exception: if stored: return stored, "db_stale" raise 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) if _is_db_fresh(sym, min(50, settings.chart_kline_limit)): continue if binance_client.is_rate_limited(): logger.warning("Prefetch stopped — rate limited") break try: await sync_daily_klines(sym) except BinanceRateLimitedError: logger.warning("Prefetch rate limited at %s", sym) break except Exception as e: logger.warning("Prefetch %s failed: %s", sym, e)