experiment(rotation): 添加新兴市场大类(印度)影响验证

实验设计:
- A组:当前7大类配置(无新兴市场)
- B组:添加印度作为第8大类(EM = Emerging Market)
- 标的:^NSEI → 164824.SZ(工银瑞信印度市场LOF)

实验结果:
├─ 大类数量: 7 → 8 (+1) ✓ 跨类分散提升
├─ 累计收益: 1467.35% → 1261.83% (-205.52%)
├─ CAGR: 48.10% → 45.16% (-2.94%)
├─ Sharpe: 2.21 → 2.09 (-0.11)
├─ 日胜率: 56.45% → 57.25% (+0.80%) ✓
└─ 调仓次数: 459 → 451 (-8)

核心发现:
1. 大类数量增加确实提升跨类分散
2. 但收益反而下降205%(与预期相反)
3. 印度LOF流动性不足(日均~3000万)
4. 印度动量信号不如主流市场强
5. Top3权重被印度占用,错过其他机会

重要结论:添加新大类 ≠ 必然提升收益
- 标的本身表现能力比大类归属更重要
- 流动性、动量信号强度是关键因素

与001实验对比:
- 001(同大类添加):大类不变 → 收益-291%
- 003(新大类添加):大类+1 → 收益-205%
→ 标的质量比大类数量更重要

策略建议:
- 暂不添加印度(LOF流动性不足)
- 可测试东南亚科技ETF(513730.SH)

新增文件:
- tests/experiments/ab_test_emerging_market.py
- docs/experiments/003_emerging_market_india.md
This commit is contained in:
2026-05-06 20:55:54 +08:00
parent 6b59855c28
commit 17e806045f
3 changed files with 424 additions and 0 deletions

View File

@@ -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 | 新上市 |
选择印度LOF164824.SZ进行测试
- 信号源:^NSEI印度Nifty50指数
- ETF164824.SZ工银瑞信印度市场LOF
- 大类标记EMEmerging 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权重被印度占用错过其他机会
替代方案:
├─ 测试东南亚科技ETF513730.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. **测试其他新兴市场标的**东南亚科技ETF513730.SH流动性更好
2. **印度LOF流动性改善后重新测试**:观察日均成交额提升后的表现
3. **标的质量评估机制**:在选择新大类前,先评估标的本身的表现能力
---
## 9. 技术记录
### YFinance印度指数代码
印度Nifty50指数在YFinance中需要使用 `^NSEI` 格式(带^前缀):
```python
# 错误404 Not Found
code = "NSEI"
# 正确
code = "^NSEI"
```
---
*实验记录版本: v1.0*
*最后更新: 2026-05-06*

View File

@@ -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% |
---

View File

@@ -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()