""" 因子类型对比实验 测试 4 种动量因子的回测表现: 1. weighted_momentum: 加权线性回归动量 (当前默认) 2. vol_adjusted_momentum: 波动率调整动量 (Moskowitz TSMOM) 3. slope_r2: 斜率 × R² (未加权) 4. momentum: 简单收益率 运行方式: cd /Users/aszer/code/etf python3 rotation/experiments/factor_comparison.py """ import os import sys import json import yaml import copy from pathlib import Path from datetime import datetime PROJECT_ROOT = Path(__file__).parent.parent.parent sys.path.insert(0, str(PROJECT_ROOT)) from rotation.simple_rotation import SimpleRotationStrategy from rotation.config_loader import FactorType FACTOR_TYPES = [ ("weighted_momentum", "加权线性回归动量 (年化收益×R²)"), ("vol_adjusted_momentum", "波动率调整动量 (Sharpe-like×R²)"), ("slope_r2", "斜率×R² (未加权归一化)"), ("momentum", "简单收益率 (last/first-1)"), ] def load_config(): """Load base config""" config_path = PROJECT_ROOT / "rotation" / "config_simple.yaml" with open(config_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) def run_factor_experiment(factor_type: str): """Run backtest with a specific factor type""" print(f"\n{'='*60}") print(f" Testing: {factor_type}") print(f"{'='*60}") # Modify config to use this factor type config_path = PROJECT_ROOT / "rotation" / "config_simple.yaml" with open(config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) original_type = config['factor']['type'] config['factor']['type'] = factor_type # Write temporary config with open(config_path, 'w', encoding='utf-8') as f: yaml.dump(config, f, allow_unicode=True, default_flow_style=False) try: # Run strategy strategy = SimpleRotationStrategy() result = strategy.run() if result: metrics = result.get('metrics', {}) return { 'factor_type': factor_type, 'annual_return': metrics.get('annual_return', 0), 'total_return': metrics.get('total_return', 0), 'sharpe_ratio': metrics.get('sharpe_ratio', 0), 'max_drawdown': metrics.get('max_drawdown', 0), 'win_rate': metrics.get('win_rate', 0), 'rebalance_count': metrics.get('rebalance_count', 0), 'calmar_ratio': metrics.get('calmar_ratio', 0), } finally: # Restore original config config['factor']['type'] = original_type with open(config_path, 'w', encoding='utf-8') as f: yaml.dump(config, f, allow_unicode=True, default_flow_style=False) return None def main(): if 'FLASK_API_URL' not in os.environ: os.environ['FLASK_API_URL'] = 'https://k3s.tokenpluse.xyz' print("="*60) print(" ETF轮动策略 - 因子类型对比实验") print("="*60) results = [] for factor_type, description in FACTOR_TYPES: print(f"\n>>> {description}") result = run_factor_experiment(factor_type) if result: results.append(result) print(f" ✓ {factor_type}: 年化={result['annual_return']:.2%}, " f"夏普={result['sharpe_ratio']:.2f}, 回撤={result['max_drawdown']:.2%}") else: print(f" ✗ {factor_type}: 运行失败") # Summary table print(f"\n{'='*60}") print(" 对比结果汇总") print(f"{'='*60}") print(f"{'因子类型':<25} {'年化收益':>10} {'夏普比率':>8} {'最大回撤':>10} {'调仓次数':>8} {'胜率':>6}") print("-"*60) for r in results: print(f"{r['factor_type']:<25} " f"{r['annual_return']:>9.2%} " f"{r['sharpe_ratio']:>8.2f} " f"{r['max_drawdown']:>9.2%} " f"{r['rebalance_count']:>8d} " f"{r['win_rate']:>5.1%}") # Find best if results: best = max(results, key=lambda x: x['sharpe_ratio']) print(f"\n★ 最优因子 (按夏普): {best['factor_type']}") print(f" 年化收益: {best['annual_return']:.2%}") print(f" 夏普比率: {best['sharpe_ratio']:.2f}") print(f" 最大回撤: {best['max_drawdown']:.2%}") # Save results output_dir = PROJECT_ROOT / "rotation" / "experiments" / "output" output_dir.mkdir(exist_ok=True) output_path = output_dir / "factor_comparison_results.json" with open(output_path, 'w', encoding='utf-8') as f: json.dump({ 'timestamp': datetime.now().isoformat(), 'results': results }, f, ensure_ascii=False, indent=2) print(f"\n结果已保存: {output_path}") if __name__ == "__main__": main()