Files
stock/backend/app/data/fetcher.py
2026-08-15 08:57:15 +08:00

82 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数据编排拉取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}