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>
145 lines
4.7 KiB
Python
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()
|