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
+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")