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作为美股大类代表 - 不添加同大类新标的(避免类内切换成本) - 新增标的应优先考虑新大类(增加跨类分散)
This commit is contained in:
187
tests/experiments/ab_test_ndx_vs_spx.py
Normal file
187
tests/experiments/ab_test_ndx_vs_spx.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user