Files
etf/tests/experiments/ab_test_ndx_vs_spx.py
aszerW 6b59855c28 experiment(rotation): 同大类扩充与纳指vs标普替换对比实验
技术修复:
- SOCKS5代理IPv6问题:socks5:// → socks5h:// (hybrid_source.py, yfinance_source.py)

目录整理:
- scripts/ → 仅保留策略入口(daily_scheduler, run_rotation, run_cci_screener)
- 实验脚本移至 tests/experiments/
- 工具脚本移至 tests/utils/
- 实验记录新增 docs/experiments/
- results/ 添加到 gitignore

实验结果:

实验001 - 同大类扩充(添加标普500):
├─ 累计收益: 1467.35% → 1176.26% (-291%)
├─ CAGR: 48.10% → 43.82% (-4.28%)
├─ 调仓次数: 459 → 501 (+42次)
└─ 结论: 添加同大类标的不增加跨类分散,反而侵蚀收益

实验002 - 纳指vs标普替换对比:
├─ 累计收益: 1467.35% → 1118.77% (-348%)
├─ CAGR: 48.10% → 42.87% (-5.22%)
├─ Sharpe: 2.21 → 2.08 (-0.13)
├─ MaxDD: -17.33% → -15.14% (+2.18%)
└─ 结论: 纳指100优于标普500,成长风格更适合动量策略

策略建议:
- 保持纳指100作为美股大类代表
- 不添加同大类新标的(避免类内切换成本)
- 新增标的应优先考虑新大类(增加跨类分散)
2026-05-06 20:43:38 +08:00

187 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
A/B测试纳指100 vs 标普500 替换对比
对比:
- A组对照组纳指100作为美股大类代表
- B组实验组标普500替换纳指100作为美股大类代表
核心问题:替换后对策略绩效的影响(无类内竞争)
"""
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
import yaml
def create_config_replace_ndx_with_spx(base_config: dict) -> dict:
"""将纳指100替换为标普500"""
config = base_config.copy()
config['code_list'] = base_config['code_list'].copy()
# 移除纳指100
if 'NDX' in config['code_list']:
del config['code_list']['NDX']
# 添加标普500替换纳指100
config['code_list']['SPX'] = {
'name': '标普500',
'etf': '513500.SH',
'market': 'US'
}
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
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)
metrics = {
'label': label,
'美股标的': '纳指100' if 'NDX' in config['code_list'] else '标普500',
'累计收益': total_return,
'CAGR': cagr,
'Sharpe': sharpe,
'MaxDD': max_dd,
'Calmar': calmar,
'日胜率': win_rate,
}
print(f"\n美股代表: {metrics['美股标的']}")
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组(纳指100)':<15} {'B组(标普500)':<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 == '美股标的':
print(f"{key:<15} {a_val:<15} {b_val:<15} {'替换':<15}")
continue
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}%"
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"纳指100 → 标普500 替换效果:")
if b_metrics['CAGR'] < a_metrics['CAGR']:
print(f" - CAGR下降 {a_metrics['CAGR'] - b_metrics['CAGR']:.2%}")
print(f" → 标普500动量信号可能不如纳指强")
if b_metrics['MaxDD'] > a_metrics['MaxDD']: # 注意MaxDD是负数
print(f" - MaxDD改善 {b_metrics['MaxDD'] - a_metrics['MaxDD']:.2%}")
print(f" → 标普500更稳定回撤更小")
if b_metrics['Sharpe'] > a_metrics['Sharpe']:
print(f" - Sharpe改善 {b_metrics['Sharpe'] - a_metrics['Sharpe']:.2f}")
print(f" → 标普500风险调整后收益更优")
elif b_metrics['Sharpe'] < a_metrics['Sharpe']:
print(f" - Sharpe下降 {b_metrics['Sharpe'] - a_metrics['Sharpe']:.2f}")
print(f" → 纳指100风险调整后收益更优")
print(f"\n【策略建议】")
if b_metrics['累计收益'] < a_metrics['累计收益'] * 0.9:
print(f" 建议保持纳指100成长风格更适合动量策略")
elif b_metrics['Sharpe'] > a_metrics['Sharpe']:
print(f" 建议考虑标普500更稳定、风险调整收益更优")
else:
print(f" 建议保持纳指100累计收益更高")
def main():
"""主函数"""
# 加载基础配置
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测试纳指100 vs 标普500 替换对比")
print(f"{'='*60}")
print(f"\n研究问题:")
print(f" - 将美股大类代表从纳指100替换为标普500")
print(f" - 无类内竞争每大类还是1只")
print(f" - 评估标的特性变化对绩效的影响")
# A组纳指100当前配置
a_metrics = run_backtest(base_config, "A组: 纳指100作为美股代表")
# B组标普500替换纳指100
config_replace = create_config_replace_ndx_with_spx(base_config)
b_metrics = run_backtest(config_replace, "B组: 标普500替换纳指100")
# 对比
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_ndx_vs_spx.csv'
results_df.to_csv(results_path, index=False)
print(f"\n对比结果已保存: {results_path}")
if __name__ == '__main__':
main()