first commit
This commit is contained in:
81
backend/app/data/fetcher.py
Normal file
81
backend/app/data/fetcher.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""数据编排:拉取(Tushare 主 -> AKShare 兜底)+ 本地缓存。
|
||||
|
||||
真实行情落库到 candles 表(timeframe='1d'),回测统一从库读,与 DEMO 同路径。
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user