diff --git a/docs/experiments/003_emerging_market_india.md b/docs/experiments/003_emerging_market_india.md new file mode 100644 index 0000000..c836c76 --- /dev/null +++ b/docs/experiments/003_emerging_market_india.md @@ -0,0 +1,225 @@ +# 实验记录 003: 添加新兴市场大类(印度)的影响 + +## 实验信息 + +| 项目 | 内容 | +|------|------| +| 实验编号 | 003 | +| 实验日期 | 2026-05-06 | +| 实验类型 | A/B对比测试(新大类添加) | +| 研究问题 | 添加印度作为新兴市场新大类对策略绩效的影响 | + +--- + +## 1. 实验背景 + +### 与001、002实验的关系 + +| 实验 | 操作类型 | 大类变化 | 标的数量变化 | +|------|---------|---------|-------------| +| 001 | 同大类添加(标普500) | 0(美股还是1只) | 11→12 | +| 002 | 同大类替换(标普换纳指) | 0(美股还是1只) | 11→11 | +| 003 | **新大类添加(印度)** | **+1(新增EM大类)** | 11→12 | + +**003实验核心问题**:验证添加新大类是否真正提升跨类分散效果 + +### 理论预期 + +``` +添加新大类的预期效果: +├─ 跨类分散提升(大类数量从7→8) +├─ Top3候选池扩大(更多大类冠军可选) +├─ 收益可能提升或保持稳定 +└─ Sharpe可能改善(分散降低风险) +``` + +--- + +## 2. 实验设计 + +### 新兴市场标的选择 + +A股场内可交易的新兴市场标的: + +| 代码 | 名称 | 类型 | 流动性 | +|-----|------|-----|--------| +| 164824.SZ | 工银瑞信印度市场LOF | LOF | 日均~3000万 | +| 520580.SH | 新兴亚洲ETF招商 | ETF | 日均~7000万 | +| 513730.SH | 东南亚科技ETF华泰柏瑞 | ETF | 新上市 | + +选择印度LOF(164824.SZ)进行测试: +- 信号源:^NSEI(印度Nifty50指数) +- ETF:164824.SZ(工银瑞信印度市场LOF) +- 大类标记:EM(Emerging Market) + +### A/B组配置 + +| 组别 | 大类数量 | 新兴市场 | +|------|---------|---------| +| **A组(对照组)** | 7大类 | 无 | +| **B组(实验组)** | 8大类 | 印度(^NSEI → 164824.SZ) | + +--- + +## 3. 回测结果 + +### 绩效对比 + +| 指标 | A组(无新兴) | B组(有印度) | 差异 | +|------|-------------|-------------|------| +| **大类数量** | 7 | 8 | **+1** ✓ | +| **累计收益** | **1467.35%** | 1261.83% | **-205.52%** | +| **CAGR** | **48.10%** | 45.16% | **-2.94%** | +| **Sharpe** | **2.21** | 2.09 | **-0.11** | +| MaxDD | -17.33% | -17.33% | +0.00% | +| Calmar | 2.78 | 2.61 | -0.17 | +| **日胜率** | 56.45% | **57.25%** | **+0.80%** ✓ | +| 调仓次数 | 459次 | 451次 | -8 | + +--- + +## 4. 关键发现 + +### 发现1:大类数量确实增加 + +``` +大类变化: +├─ A组:A(2)、HK(2)、US(1)、JP(1)、EU(1)、COMMODITY(3)、BOND(1) = 7大类 +├─ B组:新增EM(1) = 8大类 +└─ 跨类分散确实提升 ✓ +``` + +### 发现2:但收益反而下降 + +``` +收益变化: +├─ 累计收益下降205.52% +├─ CAGR下降2.94% +├─ Sharpe下降0.11 +└─ 与预期相反! +``` + +### 发现3:日胜率略有提升 + +``` +正面指标: +├─ 日胜率提升0.80% +├─ 调仓次数减少8次 +└─ 说明:印度可能降低了激进调仓频率 +``` + +### 发现4:问题根因分析 + +``` +收益下降的可能原因: + +1. LOF流动性问题 + ├─ 164824.SZ日均成交额仅~3000万 + ├─ 买卖价差较大,实际执行成本高 + └─ 溢价/折价导致价格偏离指数 + +2. 印度动量信号较弱 + ├─ 印度Nifty50走势相对平稳 + ├─ 动量因子得分不如纳指、日经等主流市场 + └─ 选入Top3后反而拖累组合收益 + +3. Top3权重被占用 + ├─ 印度成为大类冠军后进入Top3候选池 + ├─ 占用了本应属于其他强动量标的的权重 + └─ 导致错过其他市场的机会 +``` + +--- + +## 5. 实验结论 + +### 核心结论 + +| 假设 | 实证结果 | +|-----|---------| +| 新大类增加跨类分散 | ✓ **验证通过**(+1大类) | +| 新大类提升收益 | ✗ **验证失败**(-205%) | +| 新大类改善Sharpe | ✗ **验证失败**(-0.11) | + +### 重要发现 + +``` +添加新大类 ≠ 必然提升绩效 + +关键因素: +├─ 标的本身的表现能力(动量信号强度) +├─ 标的流动性(实际执行成本) +├─ 新大类是否与现有大类低相关 +└─ 新大类是否有机会成为Top3候选 +``` + +### 策略建议 + +``` +当前建议:暂不添加印度 + +原因: +1. LOF流动性不足(日均仅~3000万) +2. 印度动量信号不如主流市场强 +3. 虽然跨类分散提升了,但收益下降205% +4. Top3权重被印度占用,错过其他机会 + +替代方案: +├─ 测试东南亚科技ETF(513730.SH) +│ → 真正的场内ETF,流动性更好 +├─ 等待印度主题ETF上市后再测试 +└─ 测试其他新兴市场(如越南、沙特) +``` + +--- + +## 6. 与001实验对比 + +| 实验 | 操作 | 大类变化 | 收益变化 | 核心结论 | +|------|------|---------|---------|---------| +| 001 | 同大类添加标普500 | 0 | -291% | 同大类添加不增加分散 | +| 003 | 新大类添加印度 | +1 | -205% | 新大类添加 ≠ 必然提升收益 | + +**关键洞察**: +- 001:大类不变 → 分散不变 → 收益下降(切换成本) +- 003:大类增加 → 分散提升 → 但收益仍下降(标的本身问题) + +**共同结论**:标的本身的表现能力比大类归属更重要 + +--- + +## 7. 相关文件 + +| 文件 | 说明 | +|-----|------| +| `tests/experiments/ab_test_emerging_market.py` | A/B测试脚本 | +| `results/ab_test_emerging_market.csv` | 测试结果数据 | + +--- + +## 8. 后续研究方向 + +1. **测试其他新兴市场标的**:东南亚科技ETF(513730.SH)流动性更好 +2. **印度LOF流动性改善后重新测试**:观察日均成交额提升后的表现 +3. **标的质量评估机制**:在选择新大类前,先评估标的本身的表现能力 + +--- + +## 9. 技术记录 + +### YFinance印度指数代码 + +印度Nifty50指数在YFinance中需要使用 `^NSEI` 格式(带^前缀): + +```python +# 错误(404 Not Found) +code = "NSEI" + +# 正确 +code = "^NSEI" +``` + +--- + +*实验记录版本: v1.0* +*最后更新: 2026-05-06* \ No newline at end of file diff --git a/docs/experiments/README.md b/docs/experiments/README.md index 1378459..74ac1a6 100644 --- a/docs/experiments/README.md +++ b/docs/experiments/README.md @@ -10,6 +10,7 @@ |------|---------|------|------|---------| | [001](001_same_category_expansion_ab_test.md) | 同大类扩充对轮动策略的影响 | 2026-05-06 | A/B测试 | 添加同大类标的不增加跨类分散,反而因切换成本侵蚀收益 | | [002](002_ndx_vs_spx_replacement.md) | 纳指100 vs 标普500替换对比 | 2026-05-06 | A/B测试 | 纳指100优于标普500(收益+348%,Sharpe+0.13),成长风格更适合动量 | +| [003](003_emerging_market_india.md) | 添加新兴市场大类(印度) | 2026-05-06 | A/B测试 | 新大类≠必然提升收益,标的本身表现能力更重要(收益-205%) | --- diff --git a/tests/experiments/ab_test_emerging_market.py b/tests/experiments/ab_test_emerging_market.py new file mode 100644 index 0000000..8a2192f --- /dev/null +++ b/tests/experiments/ab_test_emerging_market.py @@ -0,0 +1,198 @@ +""" +A/B测试:添加新兴市场大类的影响 +对比: +- A组(对照组):当前配置(无新兴市场) +- B组(实验组):添加印度作为新兴市场大类 + +核心问题:添加新大类是否增加跨类分散、提升绩效 +""" + +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_india(base_config: dict) -> dict: + """在基础配置上添加印度市场""" + config = base_config.copy() + config['code_list'] = base_config['code_list'].copy() + + # 添加印度市场(新大类) + # YFinance印度指数需要用^NSEI格式 + config['code_list']['^NSEI'] = { + 'name': '印度Nifty50', + 'etf': '164824.SZ', # 工银瑞信印度市场LOF + 'market': 'EM' # 新兴市场大类 + } + + 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) + + # 计算调仓次数 + trades = result.get('调仓记录', []) + rebalance_count = len(trades) if trades else 0 + + # 统计大类数量 + markets = set() + for code_info in config['code_list'].values(): + markets.add(code_info.get('market', 'A')) + + metrics = { + 'label': label, + '大类数量': len(markets), + '累计收益': total_return, + 'CAGR': cagr, + 'Sharpe': sharpe, + 'MaxDD': max_dd, + 'Calmar': calmar, + '日胜率': win_rate, + '调仓次数': rebalance_count, + } + + 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%}") + print(f"调仓次数: {metrics['调仓次数']}") + + 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(a_val) + b_str = str(b_val) + diff_str = f"+{diff}" if diff > 0 else str(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"添加印度新兴市场大类效果:") + + if b_metrics['大类数量'] > a_metrics['大类数量']: + print(f" ✓ 大类数量增加 {b_metrics['大类数量'] - a_metrics['大类数量']}(跨类分散提升)") + + if b_metrics['累计收益'] > a_metrics['累计收益']: + print(f" ✓ 累计收益提升 {b_metrics['累计收益'] - a_metrics['累计收益']:.2%}") + print(f" → 新大类确实带来收益增益") + elif b_metrics['累计收益'] < a_metrics['累计收益']: + print(f" ✗ 累计收益下降 {a_metrics['累计收益'] - b_metrics['累计收益']:.2%}") + print(f" → 印度市场可能动量信号不够强或流动性问题") + + 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}") + + if b_metrics['调仓次数'] > a_metrics['调仓次数'] * 1.1: + print(f" ⚠ 调仓次数增加 {b_metrics['调仓次数'] - a_metrics['调仓次数']}(可能增加切换成本)") + + print(f"\n【策略建议】") + if b_metrics['累计收益'] > a_metrics['累计收益'] and b_metrics['Sharpe'] >= a_metrics['Sharpe'] * 0.95: + print(f" 建议:添加印度新兴市场大类(跨类分散有效)") + elif b_metrics['累计收益'] < a_metrics['累计收益'] * 0.95: + print(f" 建议:暂不添加印度(收益损失较大)") + print(f" 原因:LOF流动性可能不足、印度动量信号可能较弱") + else: + print(f" 建议:进一步测试其他新兴市场标的(如东南亚科技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) + + # 添加 end_date + from datetime import datetime + base_config['end_date'] = datetime.now().strftime('%Y-%m-%d') + + print(f"\n{'='*60}") + print(f" A/B测试:添加新兴市场大类(印度)") + print(f"{'='*60}") + print(f"\n研究问题:") + print(f" - 添加印度作为新大类(EM = Emerging Market)") + print(f" - 跨类分散是否真正提升") + print(f" - 对比001实验(同大类添加),验证新大类添加效果") + + # A组:当前配置 + a_metrics = run_backtest(base_config, "A组: 当前配置(无新兴市场)") + + # B组:添加印度 + config_with_india = create_config_with_india(base_config) + b_metrics = run_backtest(config_with_india, "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_emerging_market.csv' + results_df.to_csv(results_path, index=False) + print(f"\n对比结果已保存: {results_path}") + + +if __name__ == '__main__': + main() \ No newline at end of file