fix(report): 修正溢价率计算逻辑为使用ETF收盘价替代净值
- 修改生成性能报告时溢价率计算逻辑,改用信号日期的ETF收盘价 - 溢价率仅在当天净值数据存在时计算,避免使用前一日数据 - 更新打印最新调仓信号函数,支持显示ETF收盘价而非净值 - 修改报告图表部分,显示ETF收盘价和对应溢价率 - 优化时间基准日期计算,使用信号日期或前一交易日作为数据基准 - 保持对跨市场ETF映射的兼容性和显示一致性
This commit is contained in:
@@ -88,28 +88,33 @@ def generate_performance_report(
|
||||
|
||||
# 计算溢价率(需要ETF价格和ETF净值)
|
||||
# 溢价率 = (ETF价格 - ETF净值) / ETF净值
|
||||
# 注意:信号是基于前一日数据计算的,所以使用 signal_date - 1 的数据
|
||||
etf_nav_data = {}
|
||||
premium_data = {}
|
||||
# 使用信号日期的ETF收盘价,但只有当天有净值数据时才计算溢价率
|
||||
etf_close_data = {} # ETF收盘价
|
||||
premium_data = {} # 溢价率(仅当当天有净值时计算)
|
||||
|
||||
if etf_price_data is not None and etf_nav_data_raw is not None:
|
||||
# 使用信号前一天的数据(因为信号是基于前一天收盘数据计算的)
|
||||
if etf_price_data is not None:
|
||||
signal_date = backtest_result.index[-1]
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
|
||||
# 确保数据基准日期在数据范围内
|
||||
if data_base_date in etf_price_data.index and data_base_date in etf_nav_data_raw.index:
|
||||
# 获取信号日期的ETF收盘价
|
||||
if signal_date in etf_price_data.index:
|
||||
for code in code_list:
|
||||
if code in etf_price_data.columns and code in etf_nav_data_raw.columns:
|
||||
etf_price = etf_price_data.loc[data_base_date, code]
|
||||
etf_nav = etf_nav_data_raw.loc[data_base_date, code]
|
||||
if pd.notna(etf_price) and pd.notna(etf_nav) and etf_nav > 0:
|
||||
premium = (etf_price - etf_nav) / etf_nav
|
||||
if code in etf_price_data.columns:
|
||||
etf_close = etf_price_data.loc[signal_date, code]
|
||||
if pd.notna(etf_close):
|
||||
etf_close_data[code] = etf_close
|
||||
|
||||
# 计算溢价率:只有当天有净值数据时才计算
|
||||
if etf_nav_data_raw is not None and signal_date in etf_nav_data_raw.index:
|
||||
for code in code_list:
|
||||
if code in etf_close_data and code in etf_nav_data_raw.columns:
|
||||
etf_nav = etf_nav_data_raw.loc[signal_date, code]
|
||||
if pd.notna(etf_nav) and etf_nav > 0:
|
||||
etf_close = etf_close_data[code]
|
||||
premium = (etf_close - etf_nav) / etf_nav
|
||||
premium_data[code] = premium
|
||||
etf_nav_data[code] = etf_nav
|
||||
|
||||
# 打印最新调仓信号
|
||||
_print_latest_signal(backtest_result, code_list, code_name_map, select_num, code_config, etf_nav_data, premium_data)
|
||||
_print_latest_signal(backtest_result, code_list, code_name_map, select_num, code_config, etf_close_data, premium_data)
|
||||
|
||||
# 绘制图表
|
||||
_plot_report_chart(
|
||||
@@ -144,18 +149,21 @@ def generate_performance_report(
|
||||
}
|
||||
|
||||
|
||||
def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int, code_config: dict = None, etf_nav_data: dict = None, premium_data: dict = None):
|
||||
"""打印最新调仓信号(支持ETF映射、ETF净值和溢价率显示)"""
|
||||
def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int, code_config: dict = None, etf_close_data: dict = None, premium_data: dict = None):
|
||||
"""打印最新调仓信号(支持ETF映射、ETF收盘价和溢价率显示)"""
|
||||
code_config = code_config or {}
|
||||
etf_nav_data = etf_nav_data or {}
|
||||
etf_close_data = etf_close_data or {}
|
||||
premium_data = premium_data or {}
|
||||
latest = _extract_latest_positions(backtest_result, code_list, code_name_map, select_num)
|
||||
signal_date = latest["signal_date"]
|
||||
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
||||
|
||||
# 数据基准日期(信号是基于前一日数据计算的)
|
||||
# 根据跨市场ETF映射方案:T+1日09:00计算的信号基于T日数据
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
# 数据基准日期:使用信号日期的数据
|
||||
# 如果信号日期没有数据,则使用前一天
|
||||
if signal_date in backtest_result.index:
|
||||
data_base_date = signal_date
|
||||
else:
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
||||
|
||||
print("\n")
|
||||
@@ -165,8 +173,8 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
||||
print(f" 信号日期: {signal_date_str} (基于 {data_base_date_str} 收盘数据)")
|
||||
print()
|
||||
|
||||
# 表头 - 添加ETF净值和溢价率列
|
||||
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF净值":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
|
||||
# 表头 - 添加ETF收盘价和溢价率列
|
||||
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF收盘价":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
|
||||
print(" " + "-" * 155)
|
||||
|
||||
# 下期持仓(调入/维持)
|
||||
@@ -186,19 +194,19 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
||||
if etf_code is None:
|
||||
etf_code = '直接交易'
|
||||
|
||||
# 获取ETF净值和溢价率
|
||||
# 获取ETF收盘价和溢价率
|
||||
if market == 'CRYPTO':
|
||||
etf_nav_str = ' —'
|
||||
etf_close_str = ' —'
|
||||
premium_str = ' —'
|
||||
else:
|
||||
# ETF净值
|
||||
etf_nav = etf_nav_data.get(idx_code)
|
||||
if etf_nav is not None:
|
||||
etf_nav_str = f'{etf_nav:>10.3f}'
|
||||
# ETF收盘价
|
||||
etf_close = etf_close_data.get(idx_code)
|
||||
if etf_close is not None:
|
||||
etf_close_str = f'{etf_close:>10.3f}'
|
||||
else:
|
||||
etf_nav_str = ' —'
|
||||
etf_close_str = ' —'
|
||||
|
||||
# 溢价率
|
||||
# 溢价率(只有当天有净值数据时才显示)
|
||||
premium = premium_data.get(idx_code)
|
||||
if premium is not None:
|
||||
# 高溢价警告标记
|
||||
@@ -207,7 +215,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
||||
else:
|
||||
premium_str = ' —'
|
||||
|
||||
print(f' {pos["name"]:<10} {idx_code:>12} {etf_code:>12} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {etf_nav_str} {premium_str} {flag}{pos["action"]:>4} {days_str} {pnl_str}')
|
||||
print(f' {pos["name"]:<10} {idx_code:>12} {etf_code:>12} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {etf_close_str} {premium_str} {flag}{pos["action"]:>4} {days_str} {pnl_str}')
|
||||
|
||||
# 需调出的品种
|
||||
if latest["exit_positions"]:
|
||||
@@ -220,7 +228,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
||||
entry_date_str = pos["entry_date"].strftime("%Y-%m-%d") if pos.get("entry_date") else ' —'
|
||||
score_str = ' —' # 调出品种无得分
|
||||
|
||||
# 获取ETF代码、ETF净值和溢价率
|
||||
# 获取ETF代码、ETF收盘价和溢价率
|
||||
idx_code = pos["code"]
|
||||
cfg = code_config.get(idx_code, {})
|
||||
etf_code = cfg.get('etf', '—')
|
||||
@@ -228,19 +236,19 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
||||
if etf_code is None:
|
||||
etf_code = '直接交易'
|
||||
|
||||
# 获取ETF净值和溢价率
|
||||
# 获取ETF收盘价和溢价率
|
||||
if market == 'CRYPTO':
|
||||
etf_nav_str = ' —'
|
||||
etf_close_str = ' —'
|
||||
premium_str = ' —'
|
||||
else:
|
||||
# ETF净值
|
||||
etf_nav = etf_nav_data.get(idx_code)
|
||||
if etf_nav is not None:
|
||||
etf_nav_str = f'{etf_nav:>10.3f}'
|
||||
# ETF收盘价
|
||||
etf_close = etf_close_data.get(idx_code)
|
||||
if etf_close is not None:
|
||||
etf_close_str = f'{etf_close:>10.3f}'
|
||||
else:
|
||||
etf_nav_str = ' —'
|
||||
etf_close_str = ' —'
|
||||
|
||||
# 溢价率
|
||||
# 溢价率(只有当天有净值数据时才显示)
|
||||
premium = premium_data.get(idx_code)
|
||||
if premium is not None:
|
||||
warning = '⚠️' if premium > 0.02 else ''
|
||||
@@ -248,7 +256,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
||||
else:
|
||||
premium_str = ' —'
|
||||
|
||||
print(f' {pos["name"]:<10} {idx_code:>12} {etf_code:>12} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {etf_nav_str} {premium_str} ▼调出 {days_str} {pnl_str}')
|
||||
print(f' {pos["name"]:<10} {idx_code:>12} {etf_code:>12} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {etf_close_str} {premium_str} ▼调出 {days_str} {pnl_str}')
|
||||
|
||||
print("=" * 160)
|
||||
|
||||
@@ -428,22 +436,33 @@ def _plot_report_chart(
|
||||
# 准备配置数据
|
||||
code_config = code_config or {}
|
||||
signal_date = backtest_result.index[-1]
|
||||
# 数据基准日期(信号是基于前一日数据计算的)
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
# 数据基准日期:使用信号日期的数据,如果没有则使用前一天
|
||||
if signal_date in backtest_result.index:
|
||||
data_base_date = signal_date
|
||||
else:
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
|
||||
# 计算ETF净值和溢价率(使用数据基准日期的数据)
|
||||
etf_nav_dict = {}
|
||||
premium_dict = {}
|
||||
if etf_price_data is not None and etf_nav_data_raw is not None:
|
||||
# 确保数据基准日期在数据范围内
|
||||
if data_base_date in etf_price_data.index and data_base_date in etf_nav_data_raw.index:
|
||||
# 计算ETF收盘价和溢价率(使用信号日期的数据)
|
||||
etf_close_dict = {} # ETF收盘价
|
||||
premium_dict = {} # 溢价率(仅当当天有净值时计算)
|
||||
|
||||
if etf_price_data is not None:
|
||||
# 获取信号日期的ETF收盘价
|
||||
if signal_date in etf_price_data.index:
|
||||
for code in code_list:
|
||||
if code in etf_price_data.columns and code in etf_nav_data_raw.columns:
|
||||
etf_price = etf_price_data.loc[data_base_date, code]
|
||||
etf_nav = etf_nav_data_raw.loc[data_base_date, code]
|
||||
if pd.notna(etf_price) and pd.notna(etf_nav) and etf_nav > 0:
|
||||
premium = (etf_price - etf_nav) / etf_nav
|
||||
etf_nav_dict[code] = etf_nav
|
||||
if code in etf_price_data.columns:
|
||||
etf_close = etf_price_data.loc[signal_date, code]
|
||||
if pd.notna(etf_close):
|
||||
etf_close_dict[code] = etf_close
|
||||
|
||||
# 计算溢价率:只有当天有净值数据时才计算
|
||||
if etf_nav_data_raw is not None and signal_date in etf_nav_data_raw.index:
|
||||
for code in code_list:
|
||||
if code in etf_close_dict and code in etf_nav_data_raw.columns:
|
||||
etf_nav = etf_nav_data_raw.loc[signal_date, code]
|
||||
if pd.notna(etf_nav) and etf_nav > 0:
|
||||
etf_close = etf_close_dict[code]
|
||||
premium = (etf_close - etf_nav) / etf_nav
|
||||
premium_dict[code] = premium
|
||||
|
||||
# 计算表格行数
|
||||
@@ -460,14 +479,17 @@ def _plot_report_chart(
|
||||
|
||||
signal_date = latest["signal_date"]
|
||||
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
||||
# 数据基准日期(信号是基于前一日数据计算的)
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
# 数据基准日期:使用信号日期的数据,如果没有则使用前一天
|
||||
if signal_date in backtest_result.index:
|
||||
data_base_date = signal_date
|
||||
else:
|
||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
||||
ax0.set_title(f"最新调仓信号 (信号日期: {signal_date_str},基于 {data_base_date_str} 数据,下一交易日执行)", fontsize=14, fontweight="bold", loc="left", pad=15)
|
||||
|
||||
# 构建表格数据(添加ETF代码、ETF净值和溢价率列)
|
||||
# 构建表格数据(添加ETF代码、ETF收盘价和溢价率列)
|
||||
table_data = []
|
||||
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF净值", "溢价率", "操作", "持有天数", "盈亏"]
|
||||
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF收盘价", "溢价率", "操作", "持有天数", "盈亏"]
|
||||
|
||||
# 下期持仓(调入/维持)
|
||||
for pos in latest["positions"]:
|
||||
@@ -477,7 +499,7 @@ def _plot_report_chart(
|
||||
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
||||
score_str = f'{pos["score"]:.2f}' if pos["score"] is not None else "—"
|
||||
|
||||
# 获取ETF代码、ETF净值和溢价率
|
||||
# 获取ETF代码、ETF收盘价和溢价率
|
||||
idx_code = pos["code"]
|
||||
cfg = code_config.get(idx_code, {})
|
||||
market = cfg.get('market', 'A')
|
||||
@@ -486,12 +508,12 @@ def _plot_report_chart(
|
||||
etf_code = '直接交易'
|
||||
|
||||
if market == 'CRYPTO':
|
||||
etf_nav_str = "—"
|
||||
etf_close_str = "—"
|
||||
premium_str = "—"
|
||||
else:
|
||||
etf_nav = etf_nav_dict.get(idx_code)
|
||||
etf_close = etf_close_dict.get(idx_code)
|
||||
premium = premium_dict.get(idx_code)
|
||||
etf_nav_str = f"{etf_nav:.3f}" if etf_nav is not None else "—"
|
||||
etf_close_str = f"{etf_close:.3f}" if etf_close is not None else "—"
|
||||
if premium is not None:
|
||||
warning = "⚠️" if premium > 0.02 else ""
|
||||
premium_str = f"{premium:+.2%}{warning}"
|
||||
@@ -501,7 +523,7 @@ def _plot_report_chart(
|
||||
table_data.append([
|
||||
pos["name"], pos["code"], etf_code, f'{pos["weight"]:.0%}',
|
||||
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
|
||||
etf_nav_str, premium_str, pos["action"], days_str, pnl_str
|
||||
etf_close_str, premium_str, pos["action"], days_str, pnl_str
|
||||
])
|
||||
|
||||
# 需调出的品种
|
||||
@@ -512,7 +534,7 @@ def _plot_report_chart(
|
||||
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
||||
score_str = "—" # 调出品种无得分
|
||||
|
||||
# 获取ETF代码、ETF净值和溢价率
|
||||
# 获取ETF代码、ETF收盘价和溢价率
|
||||
idx_code = pos["code"]
|
||||
cfg = code_config.get(idx_code, {})
|
||||
market = cfg.get('market', 'A')
|
||||
@@ -521,12 +543,12 @@ def _plot_report_chart(
|
||||
etf_code = '直接交易'
|
||||
|
||||
if market == 'CRYPTO':
|
||||
etf_nav_str = "—"
|
||||
etf_close_str = "—"
|
||||
premium_str = "—"
|
||||
else:
|
||||
etf_nav = etf_nav_dict.get(idx_code)
|
||||
etf_close = etf_close_dict.get(idx_code)
|
||||
premium = premium_dict.get(idx_code)
|
||||
etf_nav_str = f"{etf_nav:.3f}" if etf_nav is not None else "—"
|
||||
etf_close_str = f"{etf_close:.3f}" if etf_close is not None else "—"
|
||||
if premium is not None:
|
||||
warning = "⚠️" if premium > 0.02 else ""
|
||||
premium_str = f"{premium:+.2%}{warning}"
|
||||
@@ -536,7 +558,7 @@ def _plot_report_chart(
|
||||
table_data.append([
|
||||
pos["name"], pos["code"], etf_code, f'{pos["weight"]:.0%}',
|
||||
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
|
||||
etf_nav_str, premium_str, "调出", days_str, pnl_str
|
||||
etf_close_str, premium_str, "调出", days_str, pnl_str
|
||||
])
|
||||
|
||||
if table_data:
|
||||
|
||||
Reference in New Issue
Block a user