#!/usr/bin/env python3 """ 验证 Mode B: 指数信号 → ETF收益 文档预期结果: CAGR: 28.07%, 最大回撤: -13.34%, 夏普: 1.685, Calmar: 2.104 """ import sys from pathlib import Path project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root)) from dotenv import load_dotenv load_dotenv() import pandas as pd import numpy as np import yaml from datetime import datetime from strategies.rotation.strategy import RotationStrategy def calculate_metrics(nav: pd.Series) -> dict: """计算绩效指标""" start_date = nav.index[0] end_date = nav.index[-1] days = (end_date - start_date).days years = days / 365 total_return = nav.iloc[-1] - 1 cagr = (nav.iloc[-1] / nav.iloc[0]) ** (1/years) - 1 daily_ret = nav.pct_change().dropna() sharpe = daily_ret.mean() / daily_ret.std() * np.sqrt(252) if daily_ret.std() > 0 else 0 peak = nav.cummax() drawdown = (nav - peak) / peak max_dd = drawdown.min() calmar = cagr / abs(max_dd) if max_dd != 0 else 0 win_rate = (daily_ret > 0).sum() / len(daily_ret) return { 'start_date': start_date.strftime('%Y-%m-%d'), 'end_date': end_date.strftime('%Y-%m-%d'), 'years': years, 'days': len(nav), 'total_return': total_return, 'cagr': cagr, 'max_dd': max_dd, 'sharpe': sharpe, 'calmar': calmar, 'win_rate': win_rate } def run_mode_b_backtest(data: dict, signals: pd.DataFrame, valid_codes: list, etf_code_map: dict, a_share_dates: pd.DatetimeIndex, trade_cost: float, select_num: int) -> dict: """ Mode B: 使用ETF价格计算收益 Args: data: 包含 etf_data 的数据字典 signals: 指数生成的信号 valid_codes: 指数代码列表 etf_code_map: {指数代码: ETF代码} 映射 a_share_dates: A股交易日历 trade_cost: 交易成本 select_num: 选股数量 """ from framework.execution import BacktestExecutor etf_data = data.get('etf_data') if etf_data is None: print("❌ ETF数据不可用") return {'result': None} # 将信号对齐到 A 股日历 if a_share_dates is not signals.index: signals = signals.reindex(a_share_dates, method='ffill').dropna(subset=[signals.columns[0]]) # 使用ETF收盘价计算收益率 returns_data = {} for code in valid_codes: etf_code = etf_code_map.get(code) if etf_code and etf_code in etf_data.columns: etf_close = etf_data[etf_code].dropna() # 对齐到A股日历 etf_aligned = etf_close.reindex(a_share_dates, method='ffill') returns_aligned = etf_aligned.pct_change(fill_method=None) # 使用指数代码作为列名(与信号匹配) returns_data[f'日收益率_{code}'] = returns_aligned else: # 没有ETF映射的标的,回退使用指数数据 index_data = data.get('index_data', {}) if code in index_data and 'close' in index_data[code].columns: close_series = index_data[code]['close'].dropna() close_aligned = close_series.reindex(a_share_dates, method='ffill') returns_data[f'日收益率_{code}'] = close_aligned.pct_change(fill_method=None) returns_df = pd.DataFrame(returns_data) # 对齐日期 common_dates = signals.index.intersection(returns_df.index) signals = signals.loc[common_dates] returns_df = returns_df.loc[common_dates] print(f" Mode B 对齐后日期: {len(common_dates)} 天") print(f" 使用ETF计算收益: {len([c for c in valid_codes if etf_code_map.get(c)])} 只") executor = BacktestExecutor( initial_capital=100000, trade_cost=trade_cost, select_num=select_num ) portfolio = executor.execute(signals, returns_df) if hasattr(portfolio, 'backtest_result'): return {'result': portfolio.backtest_result, 'portfolio': portfolio} return {'result': None} def main(): # 加载配置 config_path = project_root / 'strategies/rotation/config.yaml' with open(config_path, 'r') as f: config = yaml.safe_load(f) # 设置回测区间 config['start_date'] = '2020-01-02' config['end_date'] = '2026-05-19' print('='*70) print('Mode B 验证: 指数信号 → ETF收益') print('='*70) # 初始化策略 strategy = RotationStrategy(config) # 获取数据 print('\n获取数据...') data = strategy.get_data(use_flask_api=False) # 计算因子(使用指数数据) print('\n计算因子(指数信号)...') factor_df = strategy.compute_factors(data) # 生成信号 print('\n生成信号...') signals = strategy.generate_signals(factor_df) # 执行 Mode B 回测 print('\n执行 Mode B 回测(ETF收益)...') result_b = run_mode_b_backtest( data=data, signals=signals, valid_codes=data['valid_codes'], etf_code_map=data['etf_code_map'], a_share_dates=data.get('a_share_dates'), trade_cost=config.get('trade_cost', 0.001), select_num=config.get('select_num', 3) ) if result_b.get('result') is None: print('❌ Mode B 回测未生成结果') return # 计算指标 nav_b = result_b['result']['策略净值'] metrics_b = calculate_metrics(nav_b) # 输出结果 print('\n' + '='*70) print('Mode B 回测结果') print('='*70) print(f"回测区间: {metrics_b['start_date']} ~ {metrics_b['end_date']}") print(f"回测年数: {metrics_b['years']:.2f} 年") print(f"交易天数: {metrics_b['days']} 天") print('-'*70) print(f"CAGR: {metrics_b['cagr']:.2%}") print(f"最大回撤: {metrics_b['max_dd']:.2%}") print(f"夏普比率: {metrics_b['sharpe']:.3f}") print(f"Calmar比率: {metrics_b['calmar']:.3f}") print(f"日胜率: {metrics_b['win_rate']:.2%}") print(f"累计收益: {metrics_b['total_return']:.2%}") print('='*70) # 文档预期对比 print('\n文档预期 (Mode B):') print(' CAGR: 28.07%, MaxDD -13.34%, Sharpe 1.685, Calmar 2.104') cagr_diff = abs(metrics_b['cagr'] - 0.2807) print(f'\nCAGR差异: {cagr_diff:.2%}') if cagr_diff < 0.05: print('✓ 结果与文档预期基本一致') else: print('⚠ 结果与文档预期有差异') return metrics_b if __name__ == '__main__': main()