#!/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_data而非index_close # index_close可能对齐有问题,直接从index_data获取 index_data = data.get('index_data') valid_codes = data['valid_codes'] for code in valid_codes: if index_data is not None and code in index_data: # 从原始OHLCV数据获取close价格 price_df = index_data[code] if 'close' in price_df.columns: price_series = price_df['close'] else: price_series = price_df.iloc[:, 0] # 取第一列 # 对齐到回测日期 price_aligned = price_series.reindex(backtest_result.index, method='ffill') # 处理最后几天的NaN(用最后一个有效值填充) price_aligned = price_aligned.ffill() # 前向填充剩余NaN # 计算该标的的净值曲线 nav_series = (1 + price_aligned.pct_change()).cumprod() first_valid = nav_series.dropna().iloc[0] if len(nav_series.dropna()) > 0 else 1 nav_series = nav_series / first_valid # 归一化起点为1 backtest_result[f'净值_{code}'] = nav_series.values backtest_result[code] = price_aligned.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=backtest_result.index) for idx_code, etf_code in etf_code_map.items(): if etf_code in etf_data.columns: # 对齐ETF价格数据到回测日期 price_aligned = etf_data[etf_code].reindex(backtest_result.index, method='ffill') etf_price_data[idx_code] = price_aligned.values if etf_nav_data is not None: # ETF净值数据列名是ETF代码,需要用etf_code_map映射 # 并对齐到回测日期 etf_nav_data_raw = pd.DataFrame(index=backtest_result.index) for idx_code, etf_code in etf_code_map.items(): if etf_code in etf_nav_data.columns: # 对齐净值数据到回测日期(使用ffill处理日期差异) nav_aligned = etf_nav_data[etf_code].reindex(backtest_result.index, method='ffill') etf_nav_data_raw[idx_code] = nav_aligned.values # 生成原引擎格式的报告 print("\n" + "=" * 60) print(" 生成原引擎格式报告") print("=" * 60) save_path = 'results/rotation_legacy' os.makedirs('results', exist_ok=True) # 获取index_close用于报告图表绘制 index_close = data.get('index_close') 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()