From 861e590441b97bdc023e72ae42755fd170200d34 Mon Sep 17 00:00:00 2001 From: aszerW Date: Fri, 8 May 2026 22:25:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(report):=20=E7=AD=96=E7=95=A5KPI=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=9F=E4=B8=80=E6=9D=A5=E6=BA=90=EF=BC=8C=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E9=87=8D=E5=A4=8D=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修改内容: 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报告指标与终端输出完全一致 --- strategies/rotation/report.py | 31 ++++++++++ .../report_generator/generate_report.py | 57 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/strategies/rotation/report.py b/strategies/rotation/report.py index ab9e245..a66cd24 100644 --- a/strategies/rotation/report.py +++ b/strategies/rotation/report.py @@ -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, diff --git a/visualization/report_generator/generate_report.py b/visualization/report_generator/generate_report.py index 4903204..0e7226c 100644 --- a/visualization/report_generator/generate_report.py +++ b/visualization/report_generator/generate_report.py @@ -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 # 使用净值计算真实收益