Initial commit: cc-cursor 全链路量化研究平台
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>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user