""" A/B测试:添加东南亚科技ETF的影响(受限测试) 对比: - A组(对照组):当前配置(无东南亚) - B组(实验组):添加东南亚科技ETF 限制说明: - 东南亚科技ETF(513730.SH)2023年12月上市,数据仅约2年 - 新交所泛东南亚科技指数在YFinance中暂无数据 - 本次测试使用ETF价格作为信号源(非最佳实践,仅作参考) - 回测时间范围将被缩短 核心问题:新兴市场ETF流动性是否优于印度LOF """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from strategies.rotation.engine import RotationStrategy import pandas as pd import yaml def create_config_with_sea(base_config: dict) -> dict: """在基础配置上添加东南亚科技""" config = base_config.copy() config['code_list'] = base_config['code_list'].copy() # 添加东南亚科技(新大类) # 注意:由于指数数据不可用,使用ETF价格作为信号源 # 513730.SH 同时作为指数代码和ETF代码 config['code_list']['513730.SH'] = { 'name': '东南亚科技', 'etf': '513730.SH', # 华泰柏瑞东南亚科技ETF 'market': 'SEA' # 东南亚大类 } 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() if result is None or len(result) == 0: return None # 计算指标 strategy_nav = result['轮动策略净值'] strategy_ret = result['轮动策略日收益率'] total_return = strategy_nav.iloc[-1] - 1 days = len(result) years = days / 250 cagr = (strategy_nav.iloc[-1] ** (1/years)) - 1 if years > 0 else 0 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 = cagr / abs(max_dd) if max_dd < 0 else 0 win_rate = (strategy_ret > 0).sum() / len(strategy_ret) # 统计大类数量 markets = set() for code_info in config['code_list'].values(): markets.add(code_info.get('market', 'A')) metrics = { 'label': label, '大类数量': len(markets), '回测天数': days, '回测年数': years, '累计收益': total_return, 'CAGR': cagr, 'Sharpe': sharpe, 'MaxDD': max_dd, 'Calmar': calmar, '日胜率': win_rate, } print(f"\n大类数量: {metrics['大类数量']}") print(f"回测天数: {metrics['回测天数']}") print(f"回测年数: {metrics['回测年数']:.2f}") 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{'指标':<15} {'A组(无东南亚)':<15} {'B组(有东南亚)':<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) diff = b_val - a_val if key in ['累计收益', 'CAGR', 'MaxDD', '日胜率']: a_str = f"{a_val:.2%}" b_str = f"{b_val:.2%}" diff_str = f"{diff*100:+.2f}%" elif key in ['大类数量', '回测天数']: a_str = str(int(a_val)) b_str = str(int(b_val)) diff_str = f"+{int(diff)}" if diff > 0 else str(int(diff)) else: a_str = f"{a_val:.2f}" b_str = f"{b_val:.2f}" diff_str = f"{diff:+.2f}" print(f"{key:<15} {a_str:<15} {b_str:<15} {diff_str:<15}") print("-" * 60) print(f"\n【限制说明】") print(f" ⚠ 本次测试数据量受限(东南亚ETF仅2年数据)") print(f" ⚠ 使用ETF价格作为信号源(指数数据暂不可用)") print(f" ⚠ 结果仅供参考,不建议直接用于决策") print(f"\n【关键发现】") if b_metrics['大类数量'] > a_metrics['大类数量']: print(f" ✓ 大类数量增加 {b_metrics['大类数量'] - a_metrics['大类数量']}") if b_metrics['累计收益'] > a_metrics['累计收益']: print(f" ✓ 累计收益提升 {b_metrics['累计收益'] - a_metrics['累计收益']:.2%}") else: print(f" ✗ 累计收益下降 {a_metrics['累计收益'] - b_metrics['累计收益']:.2%}") if b_metrics['Sharpe'] > a_metrics['Sharpe']: print(f" ✓ Sharpe改善 {b_metrics['Sharpe'] - a_metrics['Sharpe']:.2f}") else: print(f" ✗ Sharpe下降 {a_metrics['Sharpe'] - b_metrics['Sharpe']:.2f}") print(f"\n【策略建议】") print(f" 建议:等待东南亚科技ETF积累更多数据后再测试") print(f" 原因:") print(f" 1. 数据量不足(仅{b_metrics['回测年数']:.1f}年)") print(f" 2. 指数信号源暂不可用") print(f" 3. ETF价格作为信号源存在溢价干扰") def main(): """主函数""" config_path = Path(__file__).parent.parent.parent / 'config' / 'strategies' / 'rotation.yaml' with open(config_path, 'r') as f: base_config = yaml.safe_load(f) # 设置回测结束日期 from datetime import datetime base_config['end_date'] = datetime.now().strftime('%Y-%m-%d') # ⚠ 重要:由于东南亚ETF数据从2023年12月开始 # 需要调整start_date以匹配数据可用范围 # 本次测试将使用较短的时间窗口 print(f"\n{'='*60}") print(f" A/B测试:添加东南亚科技ETF(受限测试)") print(f"{'='*60}") print(f"\n⚠ 限制说明:") print(f" - 东南亚科技ETF(513730.SH)2023年12月上市") print(f" - 数据仅约2年,回测时间范围受限") print(f" - 指数数据暂不可用,使用ETF价格作为信号源") print(f" - 结果仅供参考,不建议直接用于决策") # A组:当前配置(使用较短时间窗口) config_a = base_config.copy() config_a['start_date'] = '2024-01-01' # 调整为东南亚ETF有数据的起始时间 a_metrics = run_backtest(config_a, "A组: 当前配置(2024年起)") # B组:添加东南亚科技 config_b = create_config_with_sea(base_config) config_b['start_date'] = '2024-01-01' b_metrics = run_backtest(config_b, "B组: 添加东南亚科技") # 对比 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.parent / 'results' / 'ab_test_sea_etf.csv' results_df.to_csv(results_path, index=False) print(f"\n对比结果已保存: {results_path}") if __name__ == '__main__': main()