From 38a31357d1671679a2f63ed91abd12106be22cff Mon Sep 17 00:00:00 2001 From: aszerW Date: Tue, 12 May 2026 01:42:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E6=A1=86=E6=9E=B6=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=8E=9F=E5=BC=95=E6=93=8E=E6=8A=A5=E5=91=8A=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 scripts/generate_legacy_report.py: - 使用新框架运行回测 - 将数据格式转换为原引擎格式 - 调用原引擎 generate_performance_report 生成报告 输出文件: - rotation_legacy_chart.png (净值曲线+回撤+持仓分布) - rotation_legacy_metrics.json (策略指标JSON) - rotation_legacy_nav.csv (净值曲线数据) 用法:python scripts/generate_legacy_report.py --- scripts/generate_legacy_report.py | 164 ++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 scripts/generate_legacy_report.py diff --git a/scripts/generate_legacy_report.py b/scripts/generate_legacy_report.py new file mode 100644 index 0000000..2df9704 --- /dev/null +++ b/scripts/generate_legacy_report.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +使用新框架数据生成原引擎格式的报告 + +用法: + python scripts/generate_legacy_report.py +""" + +import os +import sys +import yaml +import pandas as pd +import numpy as np +from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() + +# 添加项目根目录到 sys.path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root)) + +# 导入新框架 +from strategies.rotation.strategy import RotationStrategy + +# 导入原引擎报告生成模块 +archive_path = project_root / 'archive' / 'legacy_core' +sys.path.insert(0, str(archive_path)) +from report import generate_performance_report +from core.common.utils import calculate_cagr, calculate_max_drawdown, calculate_sharpe + + +def run_with_legacy_report(): + """运行新框架回测并生成原引擎格式报告""" + + # 加载配置 + config_path = 'config/strategies/rotation.yaml' + with open(config_path, 'r', encoding='utf-8') as f: + config = yaml.safe_load(f) + + # 新框架回测 + print("=" * 60) + print(" ETF轮动策略 回测系统 (新框架)") + print("=" * 60) + + strategy = RotationStrategy.from_yaml(config_path) + data = strategy.get_data() + + # 计算因子 + print("\n计算因子...") + factor_df = strategy.compute_factors(data) + + # 生成信号 + print("\n生成信号...") + signals = strategy.generate_signals(factor_df) + + # 执行回测 + print("\n执行回测...") + result = strategy.run_backtest(data=data) + + # 准备原引擎格式的数据 + backtest_result = result['result'].copy() + + if backtest_result is None: + print("回测失败,无法生成报告") + return + + # 重命名列以匹配原引擎格式 + backtest_result['轮动策略净值'] = backtest_result['策略净值'] + backtest_result['轮动策略日收益率'] = backtest_result['策略日收益率'] + + # 1. 基准净值和基准日收益率 + benchmark_data = data.get('benchmark_data') + if benchmark_data is not None and not benchmark_data.empty: + # 对齐基准数据到回测日期 + benchmark_close = benchmark_data['close'] if 'close' in benchmark_data.columns else benchmark_data.iloc[:, 0] + benchmark_close_aligned = benchmark_close.reindex(backtest_result.index, method='ffill') + + # 计算基准净值 + benchmark_nav = (1 + benchmark_close_aligned.pct_change()).cumprod() + benchmark_nav = benchmark_nav / benchmark_nav.dropna().iloc[0] # 归一化起点为1 + + backtest_result['基准净值'] = benchmark_nav.values + backtest_result['基准日收益率'] = benchmark_close_aligned.pct_change().values + + # 2. 各标的净值(指数价格) + index_close = data.get('index_close') + valid_codes = data['valid_codes'] + + for code in valid_codes: + if index_close is not None and code in index_close.columns: + # 计算该标的的净值曲线 + price_series = index_close[code].loc[backtest_result.index] + nav_series = (1 + price_series.pct_change()).cumprod() + nav_series = nav_series / nav_series.iloc[0] if nav_series.iloc[0] > 0 else nav_series + backtest_result[f'净值_{code}'] = nav_series.values + backtest_result[code] = price_series.values # 当前价格 + + # 3. 得分列(从factor_df获取) + for code in valid_codes: + if code in factor_df.columns: + scores_aligned = factor_df[code].reindex(backtest_result.index, method='ffill') + backtest_result[f'得分_{code}'] = scores_aligned.values + + # 4. 信号列(中文名) + backtest_result['信号'] = backtest_result['signal'] + + # 构建code_name_map和code_config + code_config = config.get('code_list', {}) + code_name_map = {code: cfg.get('name', code) for code, cfg in code_config.items()} + + # 准备ETF价格和净值数据(用于溢价率计算) + etf_data = data.get('etf_data') + etf_nav_data = data.get('etf_nav_data') + + # ETF数据需要用ETF代码作为列名 + etf_price_data = None + etf_nav_data_raw = None + + if etf_data is not None: + # 转换列名:指数代码 -> ETF代码(通过etf_code_map) + etf_code_map = data.get('etf_code_map', {}) + etf_price_data = pd.DataFrame(index=etf_data.index) + for idx_code, etf_code in etf_code_map.items(): + if etf_code in etf_data.columns: + etf_price_data[idx_code] = etf_data[etf_code] + + if etf_nav_data is not None: + etf_nav_data_raw = pd.DataFrame(index=etf_nav_data.index) + for idx_code, etf_code in etf_code_map.items(): + if etf_code in etf_nav_data.columns: + etf_nav_data_raw[idx_code] = etf_nav_data[etf_code] + + # 生成原引擎格式的报告 + print("\n" + "=" * 60) + print(" 生成原引擎格式报告") + print("=" * 60) + + save_path = 'results/rotation_legacy' + os.makedirs('results', exist_ok=True) + + metrics = generate_performance_report( + backtest_result=backtest_result, + code_list=valid_codes, + code_name_map=code_name_map, + benchmark_name=config.get('benchmark_name', '沪深300指数'), + save_path=save_path, + select_num=config.get('select_num', 3), + code_config=code_config, + index_data=index_close, + etf_price_data=etf_price_data, + etf_nav_data_raw=etf_nav_data_raw, + ) + + print(f"\n报告文件已生成:") + print(f" - {save_path}_chart.png") + print(f" - {save_path}_metrics.json") + print(f" - {save_path}_nav.csv") + + return metrics + + +if __name__ == '__main__': + run_with_legacy_report() \ No newline at end of file