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
+223
View File
@@ -0,0 +1,223 @@
"""
VectorBT 回测引擎封装。
统一接口:engine.run(strategy, price_df, factor_df) → BacktestReport
"""
import numpy as np
import pandas as pd
import vectorbt as vbt
from backtest.base import BaseStrategy
from backtest.report import BacktestReport
class VectorBTEngine:
"""
VectorBT 回测引擎。
只做多,不做空。
"""
def __init__(
self,
initial_capital: float = 100_000,
commission: float = 0.0003, # 万三
freq: str = "D",
):
self.initial_capital = initial_capital
self.commission = commission
self.freq = freq
# ── 单股票回测 ────────────────────────────────────────
def run(
self,
strategy: BaseStrategy,
price_df: pd.DataFrame,
factor_df: pd.DataFrame | None = None,
) -> BacktestReport:
"""
单股票回测。
参数:
strategy: 策略实例
price_df: 价格数据,index=trade_date,必须有 'close'
factor_df: 因子数据,index=trade_date。
None 时使用 price_df 作为因子数据源。
返回:
BacktestReport
"""
if factor_df is None:
factor_df = price_df
# 1. 对齐日期
common_idx = price_df.index.intersection(factor_df.index)
if len(common_idx) < 2:
return BacktestReport()
price_df = price_df.loc[common_idx].sort_index()
factor_df = factor_df.loc[common_idx].sort_index()
# 2. 合并 close 到 factor_df(策略可能需要)
if "close" not in factor_df.columns:
factor_df = factor_df.copy()
factor_df["close"] = price_df["close"]
# 3. 生成信号
raw_signals = strategy.generate_signals(factor_df)
# 4. 信号 → VectorBT entries/exits
entries, exits = self._signals_to_entries(raw_signals, price_df.index)
# 5. 运行回测
close = price_df["close"]
pf = vbt.Portfolio.from_signals(
close,
entries=entries,
exits=exits,
init_cash=self.initial_capital,
fees=self.commission,
freq=self.freq,
direction="longonly",
)
return BacktestReport.from_vbt_result(pf, close)
# ── 截面回测(多股票) ──────────────────────────────────
def run_cross_section(
self,
strategy: BaseStrategy,
price_universe: dict[str, pd.DataFrame],
factor_universe: dict[str, pd.DataFrame] | None = None,
rebalance_freq: str = "M",
) -> BacktestReport:
"""
截面策略回测(多股票 + 定期调仓)。
对每只股票独立回测,合并权益曲线。
参数:
strategy: 策略实例
price_universe: {ts_code: price_df}
factor_universe: {ts_code: factor_df}
rebalance_freq: 调仓频率 'D'/'W'/'M',用于合并时对齐
返回:
BacktestReport
"""
if factor_universe is None:
factor_universe = price_universe
stock_equities = {}
stock_reports = {}
# 逐股票回测
for ts_code in price_universe:
price_df = price_universe[ts_code]
if "close" not in price_df.columns or price_df.empty:
continue
factor_df = factor_universe.get(ts_code, price_df)
report = self.run(strategy, price_df, factor_df)
if report is not None and len(report.equity_curve) > 0:
stock_equities[ts_code] = report.equity_curve
stock_reports[ts_code] = report
if not stock_equities:
return BacktestReport()
# 合并:等权分配资金到各股票
return self._merge_equities(stock_equities)
# ── 信号转换 ──────────────────────────────────────────
@staticmethod
def _signals_to_entries(
raw_signals: pd.Series,
target_index: pd.Index,
) -> tuple[pd.Series, pd.Series]:
"""
将策略信号转为 VectorBT entries/exits。
信号格式:
1 → 买入
0 → 平仓
-1 → 继续持有/不操作
entries: True 时开仓
exits: True 时平仓
"""
# 对齐到目标 index
aligned = pd.Series(-1, index=target_index)
common = target_index.intersection(raw_signals.index)
aligned.loc[common] = raw_signals.loc[common].values
entries = pd.Series(False, index=target_index)
exits = pd.Series(False, index=target_index)
in_position = False
for i in range(len(aligned)):
sig = aligned.iloc[i]
if not in_position and sig == 1:
entries.iloc[i] = True
in_position = True
elif in_position and sig == 0:
exits.iloc[i] = True
in_position = False
return entries, exits
# ── 合并多股票权益 ─────────────────────────────────────
def _merge_equities(
self, stock_equities: dict[str, pd.Series]
) -> BacktestReport:
"""等权合并多股票权益曲线,构建组合级报告。"""
equity_df = pd.DataFrame(stock_equities)
equity_df = equity_df.ffill().fillna(0)
# 转为 DatetimeIndex
if not isinstance(equity_df.index, pd.DatetimeIndex):
equity_df.index = pd.to_datetime(equity_df.index, format="%Y%m%d")
n_stocks = len(stock_equities)
weight = 1.0 / n_stocks if n_stocks > 0 else 1.0
# 加权组合收益
returns_df = equity_df.pct_change().fillna(0)
portfolio_ret = returns_df.mean(axis=1) # 等权 = 逐行平均
# 组合净值
portfolio_equity = self.initial_capital * (1 + portfolio_ret).cumprod()
dd = portfolio_equity / portfolio_equity.cummax() - 1
years = max(len(portfolio_ret) / 252, 0.02)
total_return = (portfolio_equity.iloc[-1] / portfolio_equity.iloc[0] - 1) * 100
cagr = ((total_return / 100 + 1) ** (1 / years) - 1) * 100
mdd = dd.min() * 100
mean_ret = portfolio_ret.mean() * 252
std_ret = portfolio_ret.std() * np.sqrt(252)
sharpe = mean_ret / std_ret if std_ret > 0 else 0
calmar = cagr / abs(mdd) if abs(mdd) > 0 else 0
try:
monthly = portfolio_equity.resample("ME").last().pct_change()
except Exception:
monthly = pd.Series(dtype=float)
return BacktestReport(
total_return=round(total_return, 2),
cagr=round(cagr, 2),
max_drawdown=round(mdd, 2),
sharpe_ratio=round(sharpe, 2),
calmar_ratio=round(calmar, 2),
annual_volatility=round(std_ret * 100 if std_ret != 0 else 0, 2),
total_trades=0,
equity_curve=portfolio_equity,
drawdown_curve=dd,
monthly_returns=monthly,
)