""" A/B测试:添加标普500对轮动策略的影响 对比: - A组(对照组):当前11只标的配置 - B组(实验组):添加标普500后的12只标的配置 """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from strategies.rotation.engine import RotationStrategy import pandas as pd def create_config_with_spx(base_config: dict) -> dict: """在基础配置上添加标普500""" config = base_config.copy() config['code_list'] = base_config['code_list'].copy() # 添加标普500(美股大类内) config['code_list']['SPX'] = { 'name': '标普500', 'etf': '513500.SH', 'market': 'US' # 与纳指100同属美股大类 } return config def run_backtest(config: dict, label: str) -> dict: """运行回测并返回关键指标""" print(f"\n{'='*60}") print(f" {label}") print(f"{'='*60}") strategy = RotationStrategy(config) result = strategy.run() # result 是 DataFrame if result is None or len(result) == 0: return None # 从 DataFrame 中直接计算指标 strategy_nav = result['轮动策略净值'] strategy_ret = result['轮动策略日收益率'] benchmark_nav = result['基准净值'] benchmark_ret = result['基准日收益率'] # 累计收益 total_return = strategy_nav.iloc[-1] - 1 # CAGR (交易日口径) days = len(result) years = days / 250 cagr = (strategy_nav.iloc[-1] ** (1/years)) - 1 # Sharpe excess_ret = strategy_ret.mean() * 250 # 年化收益 vol = strategy_ret.std() * (250 ** 0.5) # 年化波动 sharpe = excess_ret / vol if vol > 0 else 0 # 最大回撤 rolling_max = strategy_nav.cummax() drawdown = (strategy_nav - rolling_max) / rolling_max max_dd = drawdown.min() # Calmar calmar = cagr / abs(max_dd) if max_dd < 0 else 0 # 日胜率 win_rate = (strategy_ret > 0).sum() / len(strategy_ret) # 提取关键指标 metrics = { 'label': label, '标的数': len(config['code_list']), '累计收益': total_return, 'CAGR': cagr, 'Sharpe': sharpe, 'MaxDD': max_dd, 'Calmar': calmar, '日胜率': win_rate, } print(f"\n标的池: {len(config['code_list'])}只") print(f"累计收益: {metrics['累计收益']:.2%}") print(f"CAGR: {metrics['CAGR']:.2%}") print(f"Sharpe: {metrics['Sharpe']:.2f}") print(f"MaxDD: {metrics['MaxDD']:.2%}") print(f"Calmar: {metrics['Calmar']:.2f}") print(f"日胜率: {metrics['日胜率']:.2%}") return metrics def compare_results(a_metrics: dict, b_metrics: dict): """对比两组结果""" print(f"\n{'='*60}") print(f" 对比结果") print(f"{'='*60}") print(f"\n{'指标':<12} {'A组(无SPX)':<15} {'B组(有SPX)':<15} {'差异':<15}") print("-" * 60) metrics_keys = ['标的数', '累计收益', 'CAGR', 'Sharpe', 'MaxDD', 'Calmar', '日胜率'] for key in metrics_keys: a_val = a_metrics.get(key, 0) b_val = b_metrics.get(key, 0) if key == '标的数': diff = b_val - a_val diff_str = f"+{diff}" if diff > 0 else str(diff) else: diff = b_val - a_val if key in ['累计收益', 'CAGR', 'MaxDD', '日胜率']: diff_str = f"{diff*100:+.2f}%" else: diff_str = f"{diff:+.2f}" if key in ['累计收益', 'CAGR', 'MaxDD', '日胜率']: a_str = f"{a_val:.2%}" b_str = f"{b_val:.2%}" else: a_str = str(a_val) b_str = str(b_val) print(f"{key:<12} {a_str:<15} {b_str:<15} {diff_str:<15}") print("-" * 60) # 分析美股大类内部切换情况 print(f"\n【关键发现】") print(f"添加标普500后:") print(f" - 美股大类从1只→2只(纳指100 + 标普500)") print(f" - 类内竞争:纳指100 vs 标普500,得分高者代表美股大类") print(f" - 跨类分散不变:美股大类还是只输出1只冠军进入Top3") if b_metrics['累计收益'] != a_metrics['累计收益']: print(f" - 累计收益变化:{a_metrics['累计收益']:.2%} → {b_metrics['累计收益']:.2%}") def main(): """主函数""" import yaml # 加载基础配置 config_path = Path(__file__).parent.parent / 'config' / 'strategies' / 'rotation.yaml' with open(config_path, 'r') as f: base_config = yaml.safe_load(f) # 添加缺失的 end_date(使用今天日期) from datetime import datetime base_config['end_date'] = datetime.now().strftime('%Y-%m-%d') print(f"\n{'='*60}") print(f" A/B测试:添加标普500对diversified模式的影响") print(f"{'='*60}") print(f"\n测试假设:") print(f" - diversified=true 模式下,每大类只选1只冠军") print(f" - 添加标普500(同属美股大类)不会增加跨类分散") print(f" - 但可能增加类内切换频率和换手率") # A组:当前配置(11只,无标普500) a_metrics = run_backtest(base_config, "A组: 当前配置(11只,无标普500)") # B组:添加标普500后的配置(12只) config_with_spx = create_config_with_spx(base_config) b_metrics = run_backtest(config_with_spx, "B组: 添加标普500(12只)") # 对比结果 if a_metrics and b_metrics: compare_results(a_metrics, b_metrics) # 保存对比结果 results_df = pd.DataFrame([a_metrics, b_metrics]) results_path = Path(__file__).parent.parent / 'results' / 'ab_test_spx.csv' results_df.to_csv(results_path, index=False) print(f"\n对比结果已保存: {results_path}") if __name__ == '__main__': main()