Files
simonandClaude Opus 4.7 271a9343a5 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>
2026-06-07 15:59:05 +08:00

145 lines
4.7 KiB
Python

"""
标准化回测报告。
与回测引擎解耦,后续换引擎只需改构造函数。
"""
from dataclasses import dataclass, field
import numpy as np
import pandas as pd
@dataclass
class BacktestReport:
"""回测报告"""
# 核心收益指标
total_return: float = 0.0
cagr: float = 0.0
max_drawdown: float = 0.0
sharpe_ratio: float = 0.0
calmar_ratio: float = 0.0
annual_volatility: float = 0.0
# 交易统计
win_rate: float = 0.0
profit_factor: float = 0.0
total_trades: int = 0
avg_hold_days: float = 0.0
best_trade_pct: float = 0.0
worst_trade_pct: float = 0.0
# 序列数据
equity_curve: pd.Series = field(default_factory=pd.Series)
drawdown_curve: pd.Series = field(default_factory=pd.Series)
monthly_returns: pd.Series = field(default_factory=pd.Series)
trades_df: pd.DataFrame = field(default_factory=pd.DataFrame)
# 原始 stats
stats_dict: dict = field(default_factory=dict)
@classmethod
def from_vbt_result(cls, pf, close: pd.Series) -> "BacktestReport":
"""从 VectorBT Portfolio 结果构建报告。"""
stats = pf.stats()
def _pct(v):
"""VectorBT stats 值已为百分比 float,直接返回。"""
if v is None:
return 0.0
try:
return float(v)
except (ValueError, TypeError):
return 0.0
def _duration_days(v):
"""Timedelta → 天数。"""
if v is None:
return 0.0
try:
return v.total_seconds() / 86400
except AttributeError:
return float(v) if v is not None else 0.0
equity = pf.value()
equity = pd.Series(equity.values, index=close.index[: len(equity)])
if not isinstance(equity.index, pd.DatetimeIndex):
equity.index = pd.to_datetime(equity.index, format="%Y%m%d")
dd = equity / equity.cummax() - 1
daily_ret = equity.pct_change().dropna()
years = len(daily_ret) / 252 if len(daily_ret) > 0 else 1
total_ret = (equity.iloc[-1] / equity.iloc[0] - 1) * 100 if len(equity) > 1 else 0
cagr = ((total_ret / 100 + 1) ** (1 / years) - 1) * 100 if years > 0 else 0
mdd = dd.min() * 100
ann_vol = daily_ret.std() * np.sqrt(252) * 100 if len(daily_ret) > 0 else 0
mean_ret = daily_ret.mean() * 252
std_ret = daily_ret.std() * np.sqrt(252)
sharpe = mean_ret / std_ret if std_ret > 0 else 0
calmar = cagr / abs(mdd) if mdd != 0 else 0
trades = pf.trades.records_readable if hasattr(pf, "trades") else pd.DataFrame()
try:
monthly = equity.resample("ME").last().pct_change()
except Exception:
monthly = pd.Series(dtype=float)
pf_factor = stats.get("Profit Factor", 0)
if pf_factor is None or np.isinf(float(pf_factor)):
pf_factor = 0.0
else:
pf_factor = float(pf_factor)
return cls(
total_return=round(total_ret, 2),
cagr=round(cagr, 2),
max_drawdown=round(mdd, 2),
sharpe_ratio=round(sharpe, 2),
calmar_ratio=round(calmar, 2),
annual_volatility=round(ann_vol, 2),
win_rate=_pct(stats.get("Win Rate [%]", 0)),
profit_factor=pf_factor,
total_trades=int(stats.get("Total Trades", 0)),
avg_hold_days=_duration_days(stats.get("Avg Winning Trade Duration", None)),
best_trade_pct=_pct(stats.get("Best Trade [%]", 0)),
worst_trade_pct=_pct(stats.get("Worst Trade [%]", 0)),
equity_curve=equity,
drawdown_curve=dd,
monthly_returns=monthly,
trades_df=trades,
stats_dict={k: str(v) for k, v in stats.items()},
)
def to_dict(self) -> dict:
"""核心指标转字典。"""
return {
"total_return": self.total_return,
"cagr": self.cagr,
"max_drawdown": self.max_drawdown,
"sharpe_ratio": self.sharpe_ratio,
"calmar_ratio": self.calmar_ratio,
"annual_volatility": self.annual_volatility,
"win_rate": self.win_rate,
"profit_factor": self.profit_factor,
"total_trades": self.total_trades,
}
def summary(self) -> str:
"""一行摘要。"""
return (
f"收益={self.total_return:.1f}% "
f"年化={self.cagr:.1f}% "
f"回撤={self.max_drawdown:.1f}% "
f"夏普={self.sharpe_ratio:.2f} "
f"胜率={self.win_rate:.1f}% "
f"交易={self.total_trades}笔"
)
def __repr__(self) -> str:
return self.summary()