feat(report): 净值曲线数据统一来源,直接读取轮动策略输出

修改内容:
1. strategies/rotation/report.py
   - 新增保存策略净值曲线到CSV文件的功能
   - 保存字段:日期、策略净值、基准净值、各品种净值
   - 输出路径: results/report_nav.csv
   - 包含1754条净值记录

2. visualization/report_generator/generate_report.py
   - 加载净值曲线CSV文件(优先)
   - 直接使用轮动策略输出的净值数据,不再重新计算
   - 保留备用计算逻辑(CSV不存在时)
   - 新增 benchmark_values 用于显示基准净值

3. visualization/report_generator/template.html
   - 净值曲线图表新增基准净值曲线(红色虚线)
   - 添加图例显示(策略净值、基准净值)
   - Tooltip 显示双线数据对比

效果:
- 净值曲线数据统一从轮动策略回测结果获取
- 避免重复计算导致的曲线不一致
- HTML报告显示策略vs基准对比曲线
- 数据来源清晰可追溯(1754条完整净值记录)
This commit is contained in:
2026-05-08 22:33:41 +08:00
parent 861e590441
commit 987cb38322
2 changed files with 90 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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()