feat(report): 策略KPI数据统一来源,避免重复计算
修改内容: 1. strategies/rotation/report.py - 新增保存策略KPI到JSON文件的功能 - 保存指标:累计收益、年化收益、夏普比率、最大回撤、Calmar比率、日胜率 - 同时保存基准指标和回测区间信息 - 输出路径: results/report_metrics.json 2. visualization/report_generator/generate_report.py - 加载策略KPI JSON文件(优先) - 直接使用轮动策略输出的指标,不再重复计算 - 保留备用计算逻辑(JSON不存在时) - 确保HTML报告与轮动策略结果完全一致 效果: - KPI指标统一从轮动策略回测结果获取 - 避免重复计算导致的数据不一致 - 数据来源清晰可追溯 - HTML报告指标与终端输出完全一致
This commit is contained in:
@@ -133,6 +133,37 @@ def generate_performance_report(
|
||||
etf_nav_data_raw=etf_nav_data_raw,
|
||||
)
|
||||
|
||||
# 保存整体策略KPI到JSON文件
|
||||
import json
|
||||
metrics_dict = {
|
||||
"策略": {
|
||||
"累计收益": float(s_total_return),
|
||||
"年化收益(自然日)": float(s_cagr_nat),
|
||||
"年化收益(交易日)": float(s_cagr_trd),
|
||||
"夏普比率": float(s_sharpe),
|
||||
"最大回撤": float(s_max_dd),
|
||||
"Calmar比率": float(s_calmar),
|
||||
"日胜率": float(s_win_rate),
|
||||
"回测区间": {
|
||||
"开始": strategy_nav.index.min().strftime("%Y-%m-%d"),
|
||||
"结束": strategy_nav.index.max().strftime("%Y-%m-%d"),
|
||||
"交易天数": len(strategy_nav)
|
||||
}
|
||||
},
|
||||
"基准": {
|
||||
"累计收益": float(b_total_return),
|
||||
"年化收益(自然日)": float(b_cagr_nat),
|
||||
"夏普比率": float(b_sharpe),
|
||||
"最大回撤": float(b_max_dd),
|
||||
"名称": benchmark_name
|
||||
}
|
||||
}
|
||||
|
||||
metrics_path = f"{save_path}_metrics.json"
|
||||
with open(metrics_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(metrics_dict, f, indent=2, ensure_ascii=False)
|
||||
print(f"策略指标已保存: {metrics_path}")
|
||||
|
||||
# 返回指标字典
|
||||
return {
|
||||
"累计收益": s_total_return,
|
||||
|
||||
@@ -15,6 +15,7 @@ from datetime import datetime
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
|
||||
class ReportGenerator:
|
||||
@@ -24,6 +25,7 @@ class ReportGenerator:
|
||||
self.results_dir = results_dir
|
||||
self.summary_df = None
|
||||
self.trades_df = None
|
||||
self.metrics = None # 从JSON加载的策略KPI
|
||||
|
||||
def load_data(self):
|
||||
"""加载回测数据"""
|
||||
@@ -48,10 +50,63 @@ class ReportGenerator:
|
||||
self.trades_df['进场日期'] = pd.to_datetime(self.trades_df['进场日期'])
|
||||
self.trades_df['出场日期'] = pd.to_datetime(self.trades_df['出场日期'])
|
||||
|
||||
# 加载策略KPI JSON文件(如果存在)
|
||||
metrics_path = os.path.join(self.results_dir, 'report_metrics.json')
|
||||
if os.path.exists(metrics_path):
|
||||
with open(metrics_path, 'r', encoding='utf-8') as f:
|
||||
self.metrics = json.load(f)
|
||||
print(f"✅ 加载策略指标: {metrics_path}")
|
||||
else:
|
||||
print(f"⚠️ 未找到策略指标文件: {metrics_path}")
|
||||
|
||||
print(f"✅ 数据加载成功: {len(self.trades_df)} 条交易记录")
|
||||
|
||||
def calculate_kpis(self, trades_filtered=None):
|
||||
"""计算关键指标"""
|
||||
"""计算关键指标 - 优先使用轮动策略输出的指标"""
|
||||
|
||||
# 如果有从JSON加载的策略KPI,直接使用
|
||||
if self.metrics is not None and '策略' in self.metrics:
|
||||
strategy_metrics = self.metrics['策略']
|
||||
print("✅ 使用轮动策略输出的KPI指标")
|
||||
|
||||
# 计算调仓次数(需要从trades数据获取)
|
||||
df = trades_filtered if trades_filtered is not None else self.trades_df
|
||||
total_trades = len(df)
|
||||
|
||||
# 最佳品种(从summary获取)
|
||||
if self.summary_df is not None:
|
||||
symbol_col = self.summary_df['累计收益']
|
||||
best_symbol = self.summary_df.loc[symbol_col.idxmax(), '品种代码']
|
||||
else:
|
||||
best_symbol = 'N/A'
|
||||
|
||||
# 平均持仓天数
|
||||
avg_holding_days = df['持仓天数'].mean() if len(df) > 0 else 0
|
||||
|
||||
# 盈亏次数(基于trades数据)
|
||||
# 转换持仓收益为数值
|
||||
if df['持仓收益'].dtype == 'object':
|
||||
returns_num = df['持仓收益'].str.rstrip('%').astype(float)
|
||||
else:
|
||||
returns_num = df['持仓收益']
|
||||
win_count = (returns_num > 0).sum()
|
||||
loss_count = len(df) - win_count
|
||||
|
||||
return {
|
||||
'total_return': f"{strategy_metrics['累计收益'] * 100:.2f}",
|
||||
'annual_return': f"{strategy_metrics['年化收益(自然日)'] * 100:.2f}",
|
||||
'win_rate': f"{strategy_metrics['日胜率'] * 100:.2f}",
|
||||
'max_drawdown': f"{strategy_metrics['最大回撤'] * 100:.2f}",
|
||||
'sharpe_ratio': f"{strategy_metrics['夏普比率']:.2f}",
|
||||
'total_trades': str(total_trades),
|
||||
'best_symbol': best_symbol,
|
||||
'avg_holding_days': f"{avg_holding_days:.1f}",
|
||||
'win_count': int(win_count),
|
||||
'loss_count': int(loss_count)
|
||||
}
|
||||
|
||||
# 否则重新计算(备用方案)
|
||||
print("⚠️ 未找到策略KPI,重新计算...")
|
||||
df = trades_filtered if trades_filtered is not None else self.trades_df
|
||||
|
||||
# 使用净值计算真实收益
|
||||
|
||||
Reference in New Issue
Block a user