7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
140 lines
4.6 KiB
Python
140 lines
4.6 KiB
Python
"""
|
|
Tushare 数据源封装。
|
|
|
|
与 AkShareSource 保持相同接口,作为平替/备份数据源。
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
|
|
import pandas as pd
|
|
import tushare as ts
|
|
|
|
# 确保 .env 已加载(无论从哪个路径导入)
|
|
import config.settings # noqa: F401
|
|
|
|
|
|
class TushareSource:
|
|
"""Tushare 数据源。"""
|
|
|
|
def __init__(self, token: str | None = None):
|
|
token = token or os.getenv("TUSHARE_TOKEN", "")
|
|
if not token:
|
|
self._pro = None
|
|
else:
|
|
ts.set_token(token)
|
|
self._pro = ts.pro_api()
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return self._pro is not None
|
|
|
|
def _ensure_pro(self):
|
|
if self._pro is None:
|
|
raise RuntimeError("Tushare token 未配置,请在 .env 中设置 TUSHARE_TOKEN")
|
|
|
|
# ── 股票列表 ──────────────────────────────────────────
|
|
|
|
def fetch_stock_list(self) -> pd.DataFrame:
|
|
"""获取 A 股股票列表。"""
|
|
self._ensure_pro()
|
|
fields = "ts_code,name,area,industry,market,list_date,is_hs"
|
|
df = self._pro.stock_basic(
|
|
exchange="", list_status="L",
|
|
fields=fields,
|
|
)
|
|
if df is None or df.empty:
|
|
return pd.DataFrame()
|
|
return df[["ts_code", "name"]]
|
|
|
|
# ── 日线数据 ──────────────────────────────────────────
|
|
|
|
def fetch_daily(
|
|
self, ts_code: str, start: str, end: str | None = None
|
|
) -> pd.DataFrame:
|
|
"""
|
|
获取单只股票日线行情。
|
|
|
|
参数:
|
|
ts_code: 如 '000001.SZ'
|
|
start: 'YYYYMMDD'
|
|
end: 'YYYYMMDD'
|
|
"""
|
|
self._ensure_pro()
|
|
end = end or time.strftime("%Y%m%d")
|
|
|
|
df = self._pro.daily(
|
|
ts_code=ts_code,
|
|
start_date=start,
|
|
end_date=end,
|
|
fields="ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount,turnover_rate",
|
|
)
|
|
if df is None or df.empty:
|
|
return pd.DataFrame()
|
|
|
|
# 前复权因子
|
|
try:
|
|
adj = self._pro.adj_factor(ts_code=ts_code, start_date=start, end_date=end)
|
|
if adj is not None and not adj.empty:
|
|
df = df.merge(adj[["trade_date", "adj_factor"]], on="trade_date", how="left")
|
|
for col in ["open", "high", "low", "close", "pre_close"]:
|
|
if col in df.columns and "adj_factor" in df.columns:
|
|
df[col] = (df[col] * df["adj_factor"]).round(2)
|
|
df = df.drop(columns=["adj_factor"])
|
|
except Exception:
|
|
pass
|
|
|
|
df["trade_date"] = df["trade_date"].astype(str)
|
|
return df
|
|
|
|
# ── 指数日线 ──────────────────────────────────────────
|
|
|
|
def fetch_index_daily(
|
|
self, ts_code: str, start: str, end: str | None = None
|
|
) -> pd.DataFrame:
|
|
"""
|
|
获取指数日线行情。
|
|
|
|
Tushare index_daily 接口。
|
|
"""
|
|
self._ensure_pro()
|
|
end = end or time.strftime("%Y%m%d")
|
|
df = self._pro.index_daily(
|
|
ts_code=ts_code,
|
|
start_date=start, end_date=end,
|
|
fields="ts_code,trade_date,open,high,low,close,pre_close,change,pct_chg,vol,amount",
|
|
)
|
|
if df is None or df.empty:
|
|
return pd.DataFrame()
|
|
df["trade_date"] = df["trade_date"].astype(str)
|
|
return df
|
|
|
|
# ── 财务数据 ──────────────────────────────────────────
|
|
|
|
def fetch_financial(self, ts_code: str) -> pd.DataFrame:
|
|
"""获取单只股票核心财务指标。"""
|
|
self._ensure_pro()
|
|
fields = (
|
|
"ts_code,end_date,roe,roa,grossprofit_margin,netprofit_margin,"
|
|
"debt_to_assets,eps,dt_eps,bps,pe,pb,"
|
|
"total_revenue,revenue_yoy,n_income,n_income_yoy"
|
|
)
|
|
df = self._pro.fina_indicator(
|
|
ts_code=ts_code,
|
|
fields=fields,
|
|
)
|
|
if df is None or df.empty:
|
|
return pd.DataFrame()
|
|
|
|
df = df.rename(columns={
|
|
"grossprofit_margin": "gross_profit_margin",
|
|
"netprofit_margin": "net_profit_margin",
|
|
"dt_eps": "eps_diluted",
|
|
"bps": "bvps",
|
|
"revenue_yoy": "total_revenue_yoy",
|
|
"n_income": "net_profit",
|
|
"n_income_yoy": "net_profit_yoy",
|
|
})
|
|
df["end_date"] = df["end_date"].astype(str)
|
|
return df
|