diff --git a/strategies/rotation/report.py b/strategies/rotation/report.py index a66cd24..0c578dc 100644 --- a/strategies/rotation/report.py +++ b/strategies/rotation/report.py @@ -163,6 +163,21 @@ def generate_performance_report( with open(metrics_path, 'w', encoding='utf-8') as f: json.dump(metrics_dict, f, indent=2, ensure_ascii=False) print(f"策略指标已保存: {metrics_path}") + + # 保存净值曲线数据到CSV文件 + nav_df = pd.DataFrame({ + '日期': strategy_nav.index.strftime('%Y-%m-%d'), + '策略净值': strategy_nav.values, + '基准净值': benchmark_nav.values, + }) + # 添加各品种净值 + for code in code_list: + if f"净值_{code}" in backtest_result.columns: + nav_df[f"净值_{code}"] = backtest_result[f"净值_{code}"].values + + nav_path = f"{save_path}_nav.csv" + nav_df.to_csv(nav_path, index=False) + print(f"净值曲线已保存: {nav_path}") # 返回指标字典 return { diff --git a/visualization/report_generator/generate_report.py b/visualization/report_generator/generate_report.py index 0e7226c..fa5c352 100644 --- a/visualization/report_generator/generate_report.py +++ b/visualization/report_generator/generate_report.py @@ -26,6 +26,7 @@ class ReportGenerator: self.summary_df = None self.trades_df = None self.metrics = None # 从JSON加载的策略KPI + self.nav_df = None # 从CSV加载的净值曲线 def load_data(self): """加载回测数据""" @@ -59,6 +60,15 @@ class ReportGenerator: else: print(f"⚠️ 未找到策略指标文件: {metrics_path}") + # 加载净值曲线CSV文件(如果存在) + nav_path = os.path.join(self.results_dir, 'report_nav.csv') + if os.path.exists(nav_path): + self.nav_df = pd.read_csv(nav_path) + self.nav_df['日期'] = pd.to_datetime(self.nav_df['日期']) + print(f"✅ 加载净值曲线: {nav_path} ({len(self.nav_df)} 条记录)") + else: + print(f"⚠️ 未找到净值曲线文件: {nav_path}") + print(f"✅ 数据加载成功: {len(self.trades_df)} 条交易记录") def calculate_kpis(self, trades_filtered=None): @@ -179,7 +189,70 @@ class ReportGenerator: } def prepare_chart_data(self, trades_filtered=None): - """准备图表数据""" + """准备图表数据 - 优先使用轮动策略输出的净值曲线""" + + # 如果有从CSV加载的净值曲线,直接使用 + if self.nav_df is not None: + print("✅ 使用轮动策略输出的净值曲线") + + # 净值曲线数据 - 直接读取 + nav_dates = self.nav_df['日期'].dt.strftime('%Y-%m-%d').tolist() + nav_values = self.nav_df['策略净值'].round(4).tolist() + benchmark_values = self.nav_df['基准净值'].round(4).tolist() + + # 月度收益数据 - 从净值计算 + self.nav_df['年月'] = self.nav_df['日期'].dt.to_period('M') + monthly_nav = self.nav_df.groupby('年月').agg({ + '策略净值': 'last' + }).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) + monthly_nav['年月_str'] = monthly_nav['年月'].astype(str) + monthly_dates = monthly_nav['年月_str'].tolist() + monthly_values = monthly_nav['nav_change'].round(2).tolist() + + # 盈亏分布 - 从trades数据计算 + 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['持仓收益'] + + positive_returns = df[df['持仓收益_num'] > 0]['持仓收益_num'].tolist() + negative_returns = df[df['持仓收益_num'] <= 0]['持仓收益_num'].tolist() + + # 品种收益排行 - 使用累计收益列 + symbol_returns = self.summary_df.set_index('品种代码')['累计收益'] + symbol_returns = symbol_returns.sort_values() + symbol_names = [] + symbol_returns_list = [] + + for code, ret in symbol_returns.items(): + name = self.summary_df[self.summary_df['品种代码'] == code] + if len(name) > 0: + symbol_names.append(name.iloc[0]['品种名称']) + else: + symbol_names.append(code) + symbol_returns_list.append(ret) + + return { + 'nav_dates': nav_dates, + 'nav_values': nav_values, + 'benchmark_values': benchmark_values, + 'monthly_dates': monthly_dates, + 'monthly_values': monthly_values, + 'positive_returns': positive_returns, + 'negative_returns': negative_returns, + 'symbol_names': symbol_names, + 'symbol_returns': symbol_returns_list, + } + + # 否则重新计算(备用方案) + print("⚠️ 未找到净值曲线,重新计算...") df = trades_filtered if trades_filtered is not None else self.trades_df # 转换持仓收益为数值 @@ -203,6 +276,7 @@ class ReportGenerator: nav_values = daily_nav['nav'].round(4).tolist() nav_dates = daily_nav['date'].dt.strftime('%Y-%m-%d').tolist() + benchmark_values = [] # 备用方案无基准数据 # 月度收益数据 - 使用净值变化计算 df_copy = df_sorted.copy()