From b7bf8c1eb49a2f71206ef73ee62f815e77bd64e6 Mon Sep 17 00:00:00 2001 From: aszerW Date: Thu, 26 Mar 2026 01:27:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(report):=20=E4=BF=AE=E6=AD=A3=E6=BA=A2?= =?UTF-8?q?=E4=BB=B7=E7=8E=87=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91=E4=B8=BA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8ETF=E6=94=B6=E7=9B=98=E4=BB=B7=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=E5=87=80=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修改生成性能报告时溢价率计算逻辑,改用信号日期的ETF收盘价 - 溢价率仅在当天净值数据存在时计算,避免使用前一日数据 - 更新打印最新调仓信号函数,支持显示ETF收盘价而非净值 - 修改报告图表部分,显示ETF收盘价和对应溢价率 - 优化时间基准日期计算,使用信号日期或前一交易日作为数据基准 - 保持对跨市场ETF映射的兼容性和显示一致性 --- strategies/rotation/report.py | 162 +++++++++++++++++++--------------- 1 file changed, 92 insertions(+), 70 deletions(-) diff --git a/strategies/rotation/report.py b/strategies/rotation/report.py index 435d361..6cceed1 100644 --- a/strategies/rotation/report.py +++ b/strategies/rotation/report.py @@ -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: