Files
stock/backend/app/data/index_series.py
2026-09-07 18:07:31 +08:00

97 lines
3.3 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.
"""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