"""Tushare 数据源(主)。日线 + 前复权。 token 从 settings.tushare_token 读取(.env)。免费版 pro.daily 与 ts.pro_bar 实测可用。 """ from __future__ import annotations from datetime import datetime from ..config import settings from ..domain import Bar from .symbols import to_ts_code def _parse(date_str: str) -> datetime: return datetime.strptime(str(date_str), "%Y%m%d") def fetch_daily(code: str, start: str = "20200101", end: str | None = None, adjust: str = "qfq") -> list[Bar]: import tushare as ts # 延迟导入:未装/无 token 时 DEMO 仍可用 if not settings.tushare_token: raise RuntimeError("未配置 TUSHARE_TOKEN") ts.set_token(settings.tushare_token) pro = ts.pro_api() ts_code = to_ts_code(code) end = end or datetime.now().strftime("%Y%m%d") # 优先 pro_bar(含复权);积分不足则退化为 pro.daily(不复权) df = None try: df = ts.pro_bar(ts_code=ts_code, adj=adjust, start_date=start, end_date=end, freq="D") except Exception: df = None if df is None or df.empty: df = pro.daily(ts_code=ts_code, start_date=start, end_date=end) if df is None or df.empty: raise RuntimeError(f"Tushare 无数据: {ts_code}") df = df.sort_values("trade_date") bars: list[Bar] = [] for _, r in df.iterrows(): bars.append( Bar( ts=_parse(r["trade_date"]), open=float(r["open"]), high=float(r["high"]), low=float(r["low"]), close=float(r["close"]), volume=float(r["vol"]) * 100.0, # Tushare vol 单位为手 -> 股 ) ) return bars