fix(visualization): 修复净值计算逻辑,与轮动策略结果对齐
问题: - HTML报告错误地将所有品种持仓收益简单累加 - 没有考虑仓位占比权重 - 导致净值曲线和KPI指标与策略实际结果不一致 修复: - 使用出场净值和仓位占比计算每日组合净值 - 净值 = sum(出场净值 * 仓位占比) - 总收益 = (最终净值 - 初始净值) / 初始净值 * 100 - 月度收益使用净值变化率计算 - 最大回撤基于真实净值曲线计算 - 胜率基于每日净值涨跌计算 - 修复pandas FutureWarning警告 现在HTML报告的净值曲线、收益指标与轮动策略完全一致
This commit is contained in:
@@ -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 = []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user