"""数据编排:拉取(Tushare 主 -> AKShare 兜底)+ 本地缓存。 真实行情落库到 candles 表(timeframe='1d'),回测统一从库读。 """ from __future__ import annotations import asyncio from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from ..config import settings from ..domain import Bar from ..models import Candle from . import akshare_provider, tushare_provider DEFAULT_START = settings.data_default_start or "20200101" def _providers(source: str): """按优先级返回 (名称, 同步拉取函数) 列表。""" seq = [] if source in ("auto", "tushare") and settings.tushare_token: seq.append(("tushare", tushare_provider.fetch_daily)) if source in ("auto", "akshare"): seq.append(("akshare", akshare_provider.fetch_daily)) return seq async def count_cached(session: AsyncSession, symbol: str) -> int: res = await session.execute( select(func.count()).select_from(Candle).where( Candle.symbol == symbol, Candle.timeframe == "1d" ) ) return int(res.scalar() or 0) async def is_cached(session: AsyncSession, symbol: str) -> bool: return await count_cached(session, symbol) > 0 async def sync_symbol( session: AsyncSession, code: str, start: str | None = None, end: str | None = None, source: str = "auto", force: bool = False, ) -> dict: """拉取并缓存某标的日线。已缓存且非 force 时直接返回缓存计数。""" if not force and await is_cached(session, code): return {"symbol": code, "bars": await count_cached(session, code), "source": "cache"} start = start or DEFAULT_START adjust = settings.data_adjust errors: list[str] = [] bars: list[Bar] = [] used = None for name, fn in _providers(source): try: # tushare/akshare 是同步网络 IO,丢到线程池避免阻塞事件循环 bars = await asyncio.to_thread(fn, code, start, end, adjust) used = name break except Exception as e: # noqa: BLE001 errors.append(f"{name}: {e}") if not bars: raise RuntimeError("所有数据源均失败 -> " + " | ".join(errors) if errors else "无可用数据源") # 全量替换该标的日线(避免重复主键) await session.execute(delete(Candle).where(Candle.symbol == code, Candle.timeframe == "1d")) for b in bars: session.add( Candle(symbol=code, timeframe="1d", ts=b.ts, open=b.open, high=b.high, low=b.low, close=b.close, volume=b.volume) ) await session.commit() return {"symbol": code, "bars": len(bars), "source": used}