From 5c87bea4fcb53399a80cff91277c445d4c2827d1 Mon Sep 17 00:00:00 2001 From: aszerW Date: Fri, 8 May 2026 22:17:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(visualization):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=87=80=E5=80=BC=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E4=B8=8E=E8=BD=AE=E5=8A=A8=E7=AD=96=E7=95=A5=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题: - HTML报告错误地将所有品种持仓收益简单累加 - 没有考虑仓位占比权重 - 导致净值曲线和KPI指标与策略实际结果不一致 修复: - 使用出场净值和仓位占比计算每日组合净值 - 净值 = sum(出场净值 * 仓位占比) - 总收益 = (最终净值 - 初始净值) / 初始净值 * 100 - 月度收益使用净值变化率计算 - 最大回撤基于真实净值曲线计算 - 胜率基于每日净值涨跌计算 - 修复pandas FutureWarning警告 现在HTML报告的净值曲线、收益指标与轮动策略完全一致 --- .../report_generator/generate_report.py | 97 ++++++++++++------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/visualization/report_generator/generate_report.py b/visualization/report_generator/generate_report.py index 2408563..4903204 100644 --- a/visualization/report_generator/generate_report.py +++ b/visualization/report_generator/generate_report.py @@ -54,49 +54,58 @@ class ReportGenerator: """计算关键指标""" df = trades_filtered if trades_filtered is not None else self.trades_df - # 转换持仓收益为数值(去除百分号) - if df['持仓收益'].dtype == 'object': - df = df.copy() - df['持仓收益_num'] = df['持仓收益'].str.rstrip('%').astype(float) - else: - df = df.copy() - df['持仓收益_num'] = df['持仓收益'] + # 使用净值计算真实收益 + # 按日期分组计算每日组合净值 + daily_nav = df.groupby('出场日期').apply( + lambda x: (x['出场净值'].astype(float) * x['仓位占比'].str.rstrip('%').astype(float) / 100).sum(), + include_groups=False + ).reset_index() + daily_nav.columns = ['date', 'nav'] + daily_nav = daily_nav.sort_values('date') - # 总收益 - total_return = df['持仓收益_num'].sum() + # 总收益 = (最终净值 - 初始净值) / 初始净值 * 100 + initial_nav = daily_nav['nav'].iloc[0] + final_nav = daily_nav['nav'].iloc[-1] + total_return = (final_nav - initial_nav) / initial_nav * 100 # 年化收益 - days = (df['出场日期'].max() - df['出场日期'].min()).days + days = (daily_nav['date'].iloc[-1] - daily_nav['date'].iloc[0]).days if days > 0: annual_return = total_return / (days / 365.0) else: annual_return = 0 - # 胜率 - win_count = (df['持仓收益_num'] > 0).sum() - total_count = len(df) + # 胜率 - 使用净值变化 + daily_nav['nav_change'] = daily_nav['nav'].pct_change() + win_count = (daily_nav['nav_change'] > 0).sum() + total_count = len(daily_nav) - 1 # 减去第一天 win_rate = (win_count / total_count * 100) if total_count > 0 else 0 loss_count = total_count - win_count # 夏普比率 - daily_returns = df.groupby('出场日期')['持仓收益_num'].sum() - if daily_returns.std() > 0: - sharpe_ratio = daily_returns.mean() / daily_returns.std() * np.sqrt(252) + if daily_nav['nav_change'].std() > 0: + sharpe_ratio = daily_nav['nav_change'].mean() / daily_nav['nav_change'].std() * np.sqrt(252) else: sharpe_ratio = 0 - # 最大回撤(简化计算) - cum_returns = df['持仓收益_num'].cumsum() - running_max = cum_returns.cummax() - drawdown = (cum_returns - running_max) - max_drawdown = drawdown.min() if len(drawdown) > 0 else 0 + # 最大回撤 + running_max = daily_nav['nav'].cummax() + drawdown = (daily_nav['nav'] - running_max) / running_max * 100 + max_drawdown = drawdown.min() # 调仓次数 total_trades = len(df) - # 最佳品种 - symbol_returns = df.groupby('品种代码')['持仓收益_num'].sum() - best_symbol = symbol_returns.idxmax() if len(symbol_returns) > 0 else 'N/A' + # 最佳品种 - 从 summary 获取 + if self.summary_df is not None: + symbol_col = self.summary_df['累计收益'] + if symbol_col.dtype == 'object': + symbol_col_num = symbol_col.str.rstrip('%').astype(float) + else: + symbol_col_num = symbol_col + best_symbol = self.summary_df.loc[symbol_col_num.idxmax(), '品种代码'] + else: + best_symbol = 'N/A' # 平均持仓天数 avg_holding_days = df['持仓天数'].mean() if len(df) > 0 else 0 @@ -128,21 +137,39 @@ class ReportGenerator: df_sorted = df.sort_values('出场日期') - # 净值曲线数据 - df_sorted['累计收益'] = df_sorted['持仓收益_num'].cumsum() - nav_values = df_sorted['累计收益'].tolist() - nav_dates = df_sorted['出场日期'].dt.strftime('%Y-%m-%d').tolist() + # 净值曲线数据 - 使用出场净值(考虑仓位加权) + # 按日期分组,计算每日的加权平均净值 + daily_nav = df_sorted.groupby('出场日期').apply( + lambda x: (x['出场净值'].astype(float) * x['仓位占比'].str.rstrip('%').astype(float) / 100).sum(), + include_groups=False + ).reset_index() + daily_nav.columns = ['date', 'nav'] + daily_nav = daily_nav.sort_values('date') - # 月度收益数据 + nav_values = daily_nav['nav'].round(4).tolist() + nav_dates = daily_nav['date'].dt.strftime('%Y-%m-%d').tolist() + + # 月度收益数据 - 使用净值变化计算 df_copy = df_sorted.copy() df_copy['年月'] = df_copy['出场日期'].dt.to_period('M') - monthly = df_copy.groupby('年月')['持仓收益_num'].sum().reset_index() - monthly['年月_str'] = monthly['年月'].astype(str) - monthly_dates = monthly['年月_str'].tolist() - monthly_values = monthly['持仓收益_num'].round(2).tolist() + monthly_nav = df_copy.groupby('年月').apply( + lambda x: (x['出场净值'].astype(float) * x['仓位占比'].str.rstrip('%').astype(float) / 100).sum(), + include_groups=False + ).reset_index() + monthly_nav.columns = ['年月', 'nav'] + monthly_nav = monthly_nav.sort_values('年月') + monthly_nav['nav_change'] = monthly_nav['nav'].pct_change() * 100 + monthly_nav['nav_change'] = monthly_nav['nav_change'].fillna(0) - # 品种收益排行 - symbol_returns = df.groupby('品种代码')['持仓收益_num'].sum().sort_values() + monthly_nav['年月_str'] = monthly_nav['年月'].astype(str) + monthly_dates = monthly_nav['年月_str'].tolist() + monthly_values = monthly_nav['nav_change'].round(2).tolist() + + # 品种收益排行 - 使用累计收益列 + symbol_returns = self.summary_df.set_index('品种代码')['累计收益'] + if symbol_returns.dtype == 'object': + symbol_returns = symbol_returns.str.rstrip('%').astype(float) + symbol_returns = symbol_returns.sort_values() symbol_names = [] symbol_returns_list = []