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:
2026-06-07 15:59:05 +08:00
co-authored by Claude Opus 4.7
commit 271a9343a5
293 changed files with 59598 additions and 0 deletions
View File
+216
View File
@@ -0,0 +1,216 @@
"""
AkShare 数据源封装。
统一封装 AkShare 调用,返回标准化的 DataFrame。
所有网络请求都在这一层处理,包含重试和容错。
支持个股和指数两种数据接口。
"""
# 指数代码识别:.SH 后缀为主板指数,399xxx 为深证指数
# 注意区分:000001.SZ 是平安银行(个股)000001.SH 是上证指数
def is_index_code(ts_code: str) -> bool:
"""判断是否为指数代码。排除 000xxx.SZ 个股。"""
if not ts_code:
return False
code = ts_code.upper()
# .SH 开头 000 是指数
if code.endswith(".SH") and (code.startswith("000") or code.startswith("399")):
return True
# 深交所 399xxx 指数
if code.startswith("399"):
return True
# 无后缀的纯数字 000xxx(上证指数常见写法)
if code == "000001":
return True
return False
import time
import pandas as pd
import akshare as ak
from config.settings import AKSHARE_CONFIG
class AkShareSource:
"""AkShare 数据源。"""
def __init__(self):
self._timeout = AKSHARE_CONFIG["request_timeout"]
self._retry = AKSHARE_CONFIG["retry_times"]
self._delay = AKSHARE_CONFIG["retry_delay"]
def _retry_call(self, fn, name: str, **kwargs):
"""带重试的 API 调用包装。重试间隔递增。"""
last_err = None
for i in range(self._retry):
try:
return fn(**kwargs)
except Exception as e:
last_err = e
wait = self._delay * (i + 1)
print(f" [retry] {name} 失败 ({e}){wait}s 后重试 ({i + 1}/{self._retry})...")
if i < self._retry - 1:
time.sleep(wait)
raise last_err # type: ignore
# ── 股票列表 ──────────────────────────────────────────
def fetch_stock_list(self) -> pd.DataFrame:
"""获取 A 股股票列表。"""
df = self._retry_call(ak.stock_info_a_code_name, "stock_list")
df = df.rename(columns={
"code": "ts_code",
"name": "name",
})
return df[["ts_code", "name"]]
# ── 日线数据 ──────────────────────────────────────────
def fetch_daily(
self, ts_code: str, start: str, end: str | None = None
) -> pd.DataFrame:
"""
获取单只股票日线行情。
参数:
ts_code: 股票代码,如 '000001'(纯数字格式,AkShare 要求)
start: 起始日期 'YYYYMMDD'
end: 结束日期 'YYYYMMDD'None 表示今天
"""
symbol = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
end = end or time.strftime("%Y%m%d")
df = self._retry_call(
ak.stock_zh_a_hist,
"daily",
symbol=symbol,
period="daily",
start_date=start,
end_date=end,
adjust="qfq", # 前复权
)
if df.empty:
return df
df = df.rename(columns={
"日期": "trade_date",
"开盘": "open",
"收盘": "close",
"最高": "high",
"最低": "low",
"成交量": "vol",
"成交额": "amount",
"振幅": "amplitude",
"涨跌幅": "pct_chg",
"涨跌额": "change",
"换手率": "turnover_rate",
})
df["ts_code"] = ts_code
# AkShare 返回 'YYYY-MM-DD',统一转为 'YYYYMMDD'
df["trade_date"] = df["trade_date"].astype(str).str.replace("-", "")
return df
# ── 指数日线 ──────────────────────────────────────────
def fetch_index_daily(
self, ts_code: str, start: str, end: str | None = None
) -> pd.DataFrame:
"""
获取指数日线行情。
AkShare index_zh_a_hist 接口,symbol 为纯数字(如 '000001')。
"""
symbol = ts_code.replace(".SH", "").replace(".SZ", "").replace(".BJ", "")
end = end or time.strftime("%Y%m%d")
df = self._retry_call(
ak.index_zh_a_hist,
"index_daily",
symbol=symbol, period="daily",
start_date=start, end_date=end,
)
if df.empty:
return df
df = df.rename(columns={
"日期": "trade_date",
"开盘": "open",
"收盘": "close",
"最高": "high",
"最低": "low",
"成交量": "vol",
"成交额": "amount",
"涨跌幅": "pct_chg",
"涨跌额": "change",
})
df["ts_code"] = ts_code
df["trade_date"] = df["trade_date"].astype(str)
return df
# ── 财务数据 ──────────────────────────────────────────
def fetch_financial(self, ts_code: str) -> pd.DataFrame:
"""获取单只股票核心财务指标(同花顺接口)。"""
symbol = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
try:
df = ak.stock_financial_abstract_ths(symbol=symbol)
if df.empty:
return pd.DataFrame()
df = df.rename(columns={
"报告期": "end_date",
"净利润": "net_profit",
"净利润同比增长率": "net_profit_yoy",
"扣非净利润": "deducted_net_profit",
"扣非净利润同比增长率": "deducted_net_profit_yoy",
"营业总收入": "total_revenue",
"营业总收入同比增长率": "total_revenue_yoy",
"基本每股收益": "eps",
"每股净资产": "bvps",
"每股资本公积金": "capital_reserve_ps",
"每股未分配利润": "undistributed_profit_ps",
"每股经营现金流": "ocf_ps",
"销售净利率": "net_profit_margin",
"净资产收益率": "roe",
"净资产收益率-摊薄": "roe_diluted",
"营业周期": "operating_cycle",
"应收账款周转天数": "receivables_days",
"流动比率": "current_ratio",
"速动比率": "quick_ratio",
"保守速动比率": "conservative_quick_ratio",
"产权比率": "equity_ratio",
"资产负债率": "debt_to_assets",
})
df["ts_code"] = ts_code
# 日期格式统一
df["end_date"] = df["end_date"].astype(str).str.replace("-", "")
# 数值列清洗:去掉 万/亿/% 等单位
for col in df.columns:
if col in ("ts_code", "end_date"):
continue
df[col] = self._parse_financial_value(df[col])
return df
except Exception:
return pd.DataFrame()
@staticmethod
def _parse_financial_value(series: pd.Series) -> pd.Series:
"""解析财务数值字符串 '4302.00万', '64.75%', 'False' → float"""
def _parse(v):
if v is None or v == "False" or v == "":
return None
if isinstance(v, (int, float)):
return float(v)
s = str(v).strip()
if not s:
return None
try:
if s.endswith("%"):
return float(s[:-1])
if "" in s:
return float(s.replace("", "")) * 1e4
if "亿" in s:
return float(s.replace("亿", "")) * 1e8
return float(s)
except ValueError:
return None
return series.apply(_parse).astype("float64")
+139
View File
@@ -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