This commit is contained in:
2026-09-07 18:07:31 +08:00
parent 359f9ae2e4
commit bc1c72d558
27 changed files with 4532 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
"""A 股指数全量日线(上证指数日 K 图数据源)。
candles 表只有 6 位纯代码股票TDX 导入明确排除指数sh000001 与 sz000001 平安银行
无法区分),指数走 tushare index_daily 按需拉取分页拉全量1990 年至今 ~8900 根),
进程内 + Redis 两级缓存历史不可变、TTL 兜到当日更新。
"""
from __future__ import annotations
import asyncio
import json
import time
from datetime import datetime
from .. import cache
from ..domain import Bar
_PAGE = 8000 # index_daily 单次返回上限
_TTL = 7200 # 缓存 2h历史不可变只影响当日 bar 的新鲜度
_CALL_GAP = 0.12 # 分页请求间隔(秒),对 tushare 控频(与 market_overview 同款)
SH_INDEX = "000001.SH"
# 进程内缓存ts_code -> (bars, 过期时刻)。bars 为全量日线(升序)
_mem: dict[str, tuple[list[Bar], float]] = {}
def _fetch_all_sync(ts_code: str) -> list[Bar]:
"""分页拉全量日线。vol 单位手 -> 股amount 千元 -> 元(与 fetch_daily 同款换算)。"""
from .tushare_provider import get_pro
pro = get_pro()
frames = []
offset = 0
while True:
df = pro.index_daily(ts_code=ts_code, offset=offset, limit=_PAGE)
if df is None or df.empty:
break
frames.append(df)
if len(df) < _PAGE:
break
offset += _PAGE
time.sleep(_CALL_GAP)
if not frames:
raise RuntimeError(f"Tushare index_daily 无数据: {ts_code}")
import pandas as pd
df = pd.concat(frames).drop_duplicates(subset="trade_date").sort_values("trade_date")
bars: list[Bar] = []
for _, r in df.iterrows():
amt = r.get("amount")
bars.append(
Bar(
ts=datetime.strptime(str(r["trade_date"]), "%Y%m%d"),
open=float(r["open"]), high=float(r["high"]),
low=float(r["low"]), close=float(r["close"]),
volume=float(r["vol"]) * 100.0,
amount=float(amt) * 1000.0 if amt is not None and amt == amt else None,
)
)
return bars
def _to_raw(bars: list[Bar]) -> str:
"""紧凑 JSON[[ts_iso, open, high, low, close, volume, amount|null], ...]~600KB"""
return json.dumps(
[[b.ts.isoformat(), b.open, b.high, b.low, b.close, b.volume, b.amount] for b in bars],
ensure_ascii=False, separators=(",", ":"),
)
def _from_raw(raw: str) -> list[Bar]:
return [
Bar(ts=datetime.fromisoformat(row[0]), open=row[1], high=row[2], low=row[3],
close=row[4], volume=row[5], amount=row[6])
for row in json.loads(raw)
]
async def get_index_daily(ts_code: str = SH_INDEX) -> list[Bar]:
"""全量日线(升序):进程内 -> Redis -> tushare未命中层级回填上一级。"""
hit = _mem.get(ts_code)
if hit and hit[1] > time.monotonic():
return hit[0]
key = f"idxd:{ts_code}"
raw = await cache.cache_get(key)
if isinstance(raw, str):
bars = _from_raw(raw)
_mem[ts_code] = (bars, time.monotonic() + _TTL)
return bars
bars = await asyncio.to_thread(_fetch_all_sync, ts_code)
_mem[ts_code] = (bars, time.monotonic() + _TTL)
await cache.cache_set(key, _to_raw(bars), ttl=_TTL)
return bars