看股功能更新

This commit is contained in:
2026-08-15 15:36:11 +08:00
parent c1c43d2ff7
commit 9cce670b74
26 changed files with 957 additions and 427 deletions

View File

@@ -1,12 +1,15 @@
"""数据编排拉取Tushare 主 -> AKShare 兜底)+ 本地缓存。
真实行情落库到 candles 表timeframe='1d'),回测统一从库读。
真实行情落库到 candles 表timeframe='1d'**不复权底座**),回测统一从库读。
复权qfq/hfq在读取时按 adj_factor 表本地换算,见 api._adjust_bars。
"""
from __future__ import annotations
import asyncio
from sqlalchemy import delete, func, select
from sqlalchemy import func, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
from ..config import settings
@@ -40,6 +43,13 @@ async def is_cached(session: AsyncSession, symbol: str) -> bool:
return await count_cached(session, symbol) > 0
async def _last_cached_ts(session: AsyncSession, symbol: str):
res = await session.execute(
select(func.max(Candle.ts)).where(Candle.symbol == symbol, Candle.timeframe == "1d")
)
return res.scalar()
async def sync_symbol(
session: AsyncSession,
code: str,
@@ -48,34 +58,57 @@ async def sync_symbol(
source: str = "auto",
force: bool = False,
) -> dict:
"""拉取并缓存某标的日线。已缓存且非 force 时直接返回缓存计数。"""
if not force and await is_cached(session, code):
"""增量拉取并 upsert 某标的日线(**不复权**底座)。
- 永不删除已有行:按 (symbol, timeframe, ts) 主键 upsert
不会把 TDX 导入的 30 年历史冲掉;
- 已缓存时从最后一根的次日开始增量拉取force 仅跳过「有缓存就返回」
的短路,用于缓存落后于最新交易日时的刷新);
- 拉不到新行时保持原缓存不动。
"""
last_ts = await _last_cached_ts(session, code)
if last_ts is not None and not force and not start:
return {"symbol": code, "bars": await count_cached(session, code), "source": "cache"}
if last_ts is not None and not start:
# 增量:从缓存最后一根当天开始(重叠一天重新拉取,容忍数据源漏行/盘后修订)
start = last_ts.strftime("%Y%m%d")
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)
# tushare/akshare 是同步网络 IO丢到线程池避免阻塞事件循环
# adjust=None -> 不复权(复权在读取时按 adj_factor 换算)
bars = await asyncio.to_thread(fn, code, start, end, None)
used = name
break
except Exception as e: # noqa: BLE001
errors.append(f"{name}: {e}")
if not bars:
if last_ts is not None:
# 增量失败(如停牌/新股无新行):保留缓存,不算错误
return {"symbol": code, "bars": await count_cached(session, code), "source": "cache"}
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)
)
# upsert不 delete避免破坏既有底座TDX 全量历史
stmt = pg_insert(Candle).values([
{"symbol": code, "timeframe": "1d", "ts": b.ts, "open": b.open, "high": b.high,
"low": b.low, "close": b.close, "volume": b.volume,
"amount": b.amount, "turnover": b.turnover}
for b in bars
])
stmt = stmt.on_conflict_do_update(
index_elements=["symbol", "timeframe", "ts"],
set_={"open": stmt.excluded.open, "high": stmt.excluded.high, "low": stmt.excluded.low,
"close": stmt.excluded.close, "volume": stmt.excluded.volume,
# 增量源缺失额/换手时保留库里的旧值(如 TDX 已回补的 30 年成交额)
"amount": func.coalesce(stmt.excluded.amount, Candle.amount),
"turnover": func.coalesce(stmt.excluded.turnover, Candle.turnover)},
)
await session.execute(stmt)
await session.commit()
return {"symbol": code, "bars": len(bars), "source": used}