test(experiments): add France CAC40 and SEA ETF experiments

- Add France CAC40 market test (004)
- Add SEA ETF limited test (005)
- Add France in EU category test (006)
- Update experiment README with new results
- Modify emerging market test description
This commit is contained in:
2026-05-06 22:23:12 +08:00
parent 17e806045f
commit 9776ae7de0
6 changed files with 812 additions and 1 deletions

View File

@@ -0,0 +1,182 @@
# 实验记录 004: 法国CAC40市场大类添加影响
## 实验信息
| 项目 | 内容 |
|------|------|
| 实验编号 | 004 |
| 实验日期 | 2026-05-06 |
| 实验类型 | A/B对比测试新大类添加 |
| 矧究问题 | 添加法国CAC40作为欧洲新大类对策略绩效的影响 |
---
## 1. 实验背景
### 与初衷:欧洲内部分散
当前配置已有德国DAX作为欧洲代表
```
添加法国CAC40奢侈品风格作为新大类
├─ 目的:验证欧洲内部分散是否有效
└─ 与德国工业风格形成互补
```
### 德国 vs 法国对比
| 维度 | 德国DAX | 法国CAC40 |
|-----|---------|---------|
| **成分股风格** | 工业制造(汽车、机械) | 奢侈品LV, 欧莱雅) |
| **权重行业** | 西门子、奔驰、宝马 | LVMH 欧莱雅 达尔 |
| **波动特性** | 工业周期性明显 | 奢侈品相对平缓 |
| **动量适应性** | ✓ 高(波动大) | 中(波动平缓) |
---
## 2. 实验设计
### A/B组配置
| 组别 | 欧洲配置 | 其他大类 |
|------|---------|---------|
| **A组对照组** | 仅德国DAX (513030.SH) | A股2、港股2美股、日本、 商品、 固收 |
| **B组实验组** | 德国DAX + 法国CAC40 (513080.SH) | 同A组 |
### 技术细节
```
指数代码: ^FCHIYFinance法国CAC40指数
ETF代码: 513080.SH华安法国ETF)
大类标记: FR法国 独立于德国的EU大类
```
---
## 3. 回测结果
### 绩效对比
| 指标 | A组仅德国 | B组有法国 | 差异 |
|------|-------------|-------------|------|
| **大类数量** | 7 | 8 | **+1** ✓ |
| **累计收益** | **1467.35%** | 1300.55% | **-166.80%** |
| **CAGR** | **48.10%** | 45.74% | **-2.36%** |
| **Sharpe** | **2.21** | 2.15 | **-0.06** |
| MaxDD | -17.33% | -17.33% | +0.00% |
| Calmar | 2.78 | 2.64 | -0.14 |
| **日胜率** | **56.45%** | 54.17% | **-2.28%** |
| 调仓次数 | 459 | 463 | +4 |
---
## 4. 关键发现
### 发现1大类数量增加但收益下降
```
跨类分散提升:
├─ 大类从7→8+1
└─ 但累计收益下降166.80%
```
### 发现2法国动量信号不如德国
```
德国工业风格更适合动量策略:
├─ 工业周期波动大 → 动量信号明显
├─ 奢侈品波动平缓 → 动量信号较弱
└─ 法国被选入Top3后 表现不如德国
```
### 发现3Sharpe和日胜率双降
```
风险调整指标全面下降:
├─ Sharpe: 2.21 → 2.15 (-0.06)
├─ 日胜率: 56.45% → 54.17% (-2.28%)
└─ 跻加法国降低整体风险收益比质量
```
### 发现4调仓次数略增
```
调仓增加4次
├─ 可能增加欧洲内切换(德国↔法国)
└─ 刯换成本略微增加
```
---
## 5. 实验结论
### 核心结论
| 儒设 | 实证结果 |
|-----|---------|
| 新大类增加跨类分散 | ✓ **验证通过** |
| 新大类提升收益 | ✗ **验证失败**收益下降167% |
| 奢侈品风格适合动量 | ✗ **验证失败**(不如工业) |
### 重要洞察
```
添加新大类 ≠ 必然提升收益
关键因素:
├─ 标的本身表现能力(动量信号强度)
├─ 标的风格与策略的匹配度
└─ 德国工业 > 法国奢侈品(对动量策略)
```
### 策略建议
```
建议暂不添加法国CAC40
原因:
1. 收益下降167%Sharpe下降0006
2. 奢侈品风格不如工业风格适合动量策略
3. 日胜率下降2.28%调仓增加4次
4. 保持德国DAX作为欧洲代表
德国 vs 法国:
├─ 德国DAX 工业周期明显 → 动量信号强 ✓
├─ 法国CAC 奢侈品波动平缓 → 动量信号弱 ✗
└─ 工业风格更适合动量策略
```
---
## 6. 与其他实验对比
| 实验 | 操作类型 | 大类变化 | 收益变化 |
|------|---------|---------|---------|
| 001 | 同大类添加标普500 | 0 | -291% |
| 002 | 纳指替换标普500 | 0 | -348% |
| 003 | 新大类添加印度 | +1 | -205% |
| 004 | 新大类添加法国 | +1 | **-167%** |
**统一结论**:标的质量比大类数量更重要
---
## 7. 相关文件
| 文件 | 说明 |
|-----|------|
| `tests/experiments/ab_test_france.py` | 法国市场A/B测试脚本 |
| `tests/experiments/ab_test_france_in_eu.py` | 法国EU大类内测试脚本 |
| `results/ab_test_france.csv` | 测试结果数据 |
---
## 8. 后续研究方向
1. 测试其他欧洲标的如英国富时100
2. 测试沙特ETF520830.SH 作为中东新大类
3. 線德国 vs 法国不同市场周期的表现差异(牛市/熊市分别测试)
---
*实验记录版本: v1.0*
*最后更新: 2026-05-06*

View File

@@ -11,6 +11,9 @@
| [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% |
| [004](004_france_cac40_test.md) | 法国CAC40市场大类添加 | 2026-05-06 | A/B测试 | 奢侈品风格不适合动量策略,德国工业优于法国(收益-167% |
| [005](005_sea_etf_limited_test.md) | 东南亚科技ETF受限测试 | 2026-05-06 | A/B测试 | 测试失败:数据源逻辑限制 + QDII-ETF不能作为指数代码 |
| [006](006_france_in_eu_category.md) | 法国放入EU大类类内竞争 | 2026-05-06 | A/B测试 | 类内竞争比新大类损失更小(-149% vs -167%但仍选德国DAX |
---

View File

@@ -2,7 +2,7 @@
A/B测试添加新兴市场大类的影响
对比:
- A组对照组当前配置无新兴市场
- B组实验组添加印度作为新兴市场大类
- B组实验组添加印度/法国作为新兴/欧洲市场大类
核心问题:添加新大类是否增加跨类分散、提升绩效
"""

View File

@@ -0,0 +1,200 @@
"""
A/B测试添加法国CAC40市场大类的影响
对比:
- A组对照组当前配置无法国
- B组实验组添加法国CAC40作为新大类
核心问题:法国市场是否能有效补充欧洲分散
"""
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_france(base_config: dict) -> dict:
"""在基础配置上添加法国市场"""
config = base_config.copy()
config['code_list'] = base_config['code_list'].copy()
# 添加法国CAC40新大类
# 当前已有德国DAX(EU),法国可以增加欧洲内部的多样性
# 注意德国和法国同属EU大类但这是不同的指数
config['code_list']['^FCHI'] = {
'name': '法国CAC40',
'etf': '513080.SH', # 法国ETF华安
'market': 'FR' # 法国大类(独立于德国)
}
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 if years > 0 else 0
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)
# 统计大类数量
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,
}
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组(无法国)':<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 == '大类数量':
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"添加法国CAC40大类效果")
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}")
print(f"\n【与德国DAX对比分析】")
print(f" 当前配置德国DAX (513030.SH) → EU大类")
print(f" 新增配置法国CAC40 (513080.SH) → FR大类")
print(f" ")
print(f" 德国 vs 法国特点:")
print(f" ├─ 德国DAX工业权重高汽车、机械")
print(f" ├─ 法国CAC奢侈品权重高LV、欧莱雅")
print(f" └─ 两者风格不同,可能互补")
print(f"\n【策略建议】")
if b_metrics['累计收益'] > a_metrics['累计收益'] and b_metrics['Sharpe'] >= a_metrics['Sharpe'] * 0.95:
print(f" 建议添加法国CAC40欧洲分散有效")
elif b_metrics['累计收益'] < a_metrics['累计收益'] * 0.95:
print(f" 建议:暂不添加法国(收益损失较大)")
print(f" 原因法国动量信号可能不如德国DAX强")
else:
print(f" 建议:保持观察,欧洲分散效果有限")
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测试添加法国CAC40市场大类")
print(f"{'='*60}")
print(f"\n研究问题:")
print(f" - 添加法国CAC40作为新大类FR")
print(f" - 与德国DAXEU形成欧洲内部分散")
print(f" - 德国工业风格 vs 法国奢侈品风格")
print(f" - 验证欧洲分散是否有效")
# A组当前配置
a_metrics = run_backtest(base_config, "A组: 当前配置仅德国DAX")
# B组添加法国CAC40
config_with_france = create_config_with_france(base_config)
b_metrics = run_backtest(config_with_france, "B组: 添加法国CAC40")
# 对比
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_france.csv'
results_df.to_csv(results_path, index=False)
print(f"\n对比结果已保存: {results_path}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,218 @@
"""
A/B测试法国CAC40与德国DAX类内竞争对比
对比:
- A组对照组仅德国DAX代表欧洲(EU)
- B组实验组德国DAX + 法国CAC40 同属EU大类类内竞争
核心问题:
- 法国放在欧洲大类下market='EU'),与德国竞争
- diversified模式下EU大类只输出1个冠军
- 验证:类内竞争选择最优,而非盲目增加大类
对比003/004实验
- 003/004法国/印度作为独立大类 → 收益下降
- 本次法国放EU大类内 → 类内竞争,不增加大类数量
"""
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_france_in_eu(base_config: dict) -> dict:
"""在基础配置上添加法国但放入EU大类"""
config = base_config.copy()
config['code_list'] = base_config['code_list'].copy()
# 添加法国CAC40但market='EU'与德国DAX同大类
# diversified模式下德国 vs 法国 → 选动量最强的1个进入Top3
config['code_list']['^FCHI'] = {
'name': '法国CAC40',
'etf': '513080.SH',
'market': 'EU' # 与德国DAX同属欧洲大类
}
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 if years > 0 else 0
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)
# 统计大类数量
markets = set()
for code_info in config['code_list'].values():
markets.add(code_info.get('market', 'A'))
# 统计EU大类标的数量
eu_count = sum(1 for c in config['code_list'].values() if c.get('market') == 'EU')
metrics = {
'label': label,
'大类数量': len(markets),
'EU大类标的数': eu_count,
'累计收益': total_return,
'CAGR': cagr,
'Sharpe': sharpe,
'MaxDD': max_dd,
'Calmar': calmar,
'日胜率': win_rate,
}
print(f"\n大类数量: {metrics['大类数量']}")
print(f"EU大类标的数: {metrics['EU大类标的数']}")
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组(仅德国)':<15} {'B组(德国+法国)':<15} {'差异':<15}")
print("-" * 60)
metrics_keys = ['大类数量', 'EU大类标的数', '累计收益', '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 ['大类数量', 'EU大类标的数']:
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"法国放入EU大类与德国类内竞争")
if b_metrics['大类数量'] == a_metrics['大类数量']:
print(f" ✓ 大类数量不变({b_metrics['大类数量']}大类)")
print(f" → 不增加跨类分散避免占用Top3权重")
if b_metrics['EU大类标的数'] > a_metrics['EU大类标的数']:
print(f" ✓ EU大类标的增加 {b_metrics['EU大类标的数'] - a_metrics['EU大类标的数']}")
print(f" → 德国 vs 法国 类内竞争,选动量最强")
if b_metrics['累计收益'] > a_metrics['累计收益']:
print(f" ✓ 累计收益提升 {b_metrics['累计收益'] - a_metrics['累计收益']:.2%}")
print(f" → 类内竞争选择最优,比新大类添加更有效")
elif b_metrics['累计收益'] < a_metrics['累计收益']:
loss = a_metrics['累计收益'] - b_metrics['累计收益']
print(f" ✗ 累计收益下降 {loss:.2%}")
if loss < 0.05: # 下降5%以内
print(f" → 收益损失较小vs 004实验下降167%")
else:
print(f" → 法国动量信号不如德国,类内竞争仍选德国")
print(f"\n【与004实验对比】")
print(f" 004实验法国作为独立FR大类")
print(f" ├─ 大类数量: 7 → 8 (+1)")
print(f" └─ 累计收益: -166.80%")
print(f" ")
print(f" 本次实验法国放入EU大类")
print(f" ├─ 大类数量: 保持{b_metrics['大类数量']}不变")
print(f" ├─ EU类内竞争德国DAX vs 法国CAC40")
print(f" └─ 累计收益变化: {b_metrics['累计收益'] - a_metrics['累计收益']:.2%}")
print(f"\n【策略建议】")
if b_metrics['累计收益'] >= a_metrics['累计收益'] * 0.98:
print(f" 建议法国放入EU大类可行")
print(f" 原因:类内竞争自动选最优,不增加大类数量")
elif abs(b_metrics['累计收益'] - a_metrics['累计收益']) < 0.05:
print(f" 建议法国放入EU大类可考虑")
print(f" 原因收益损失小于5%,可接受")
else:
print(f" 建议保持仅德国DAX")
print(f" 原因:法国动量信号不如德国")
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测试法国CAC40放入EU大类类内竞争")
print(f"{'='*60}")
print(f"\n研究问题:")
print(f" - 法国CAC40放入EU大类market='EU'")
print(f" - 与德国DAX类内竞争选动量最强的1个")
print(f" - 不增加大类数量避免占用Top3权重")
print(f" - 对比004实验法国独立大类的差异")
# A组仅德国DAX
a_metrics = run_backtest(base_config, "A组: 仅德国DAX代表欧洲")
# B组德国+法国同属EU大类
config_with_france = create_config_with_france_in_eu(base_config)
b_metrics = run_backtest(config_with_france, "B组: 德国+法国EU类内竞争")
# 对比
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_france_in_eu.csv'
results_df.to_csv(results_path, index=False)
print(f"\n对比结果已保存: {results_path}")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,208 @@
"""
A/B测试添加东南亚科技ETF的影响受限测试
对比:
- A组对照组当前配置无东南亚
- B组实验组添加东南亚科技ETF
限制说明:
- 东南亚科技ETF513730.SH2023年12月上市数据仅约2年
- 新交所泛东南亚科技指数在YFinance中暂无数据
- 本次测试使用ETF价格作为信号源非最佳实践仅作参考
- 回测时间范围将被缩短
核心问题新兴市场ETF流动性是否优于印度LOF
"""
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_sea(base_config: dict) -> dict:
"""在基础配置上添加东南亚科技"""
config = base_config.copy()
config['code_list'] = base_config['code_list'].copy()
# 添加东南亚科技(新大类)
# 注意由于指数数据不可用使用ETF价格作为信号源
# 513730.SH 同时作为指数代码和ETF代码
config['code_list']['513730.SH'] = {
'name': '东南亚科技',
'etf': '513730.SH', # 华泰柏瑞东南亚科技ETF
'market': 'SEA' # 东南亚大类
}
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 if years > 0 else 0
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)
# 统计大类数量
markets = set()
for code_info in config['code_list'].values():
markets.add(code_info.get('market', 'A'))
metrics = {
'label': label,
'大类数量': len(markets),
'回测天数': days,
'回测年数': years,
'累计收益': total_return,
'CAGR': cagr,
'Sharpe': sharpe,
'MaxDD': max_dd,
'Calmar': calmar,
'日胜率': win_rate,
}
print(f"\n大类数量: {metrics['大类数量']}")
print(f"回测天数: {metrics['回测天数']}")
print(f"回测年数: {metrics['回测年数']:.2f}")
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组(无东南亚)':<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(int(a_val))
b_str = str(int(b_val))
diff_str = f"+{int(diff)}" if diff > 0 else str(int(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" ⚠ 本次测试数据量受限东南亚ETF仅2年数据")
print(f" ⚠ 使用ETF价格作为信号源指数数据暂不可用")
print(f" ⚠ 结果仅供参考,不建议直接用于决策")
print(f"\n【关键发现】")
if b_metrics['大类数量'] > a_metrics['大类数量']:
print(f" ✓ 大类数量增加 {b_metrics['大类数量'] - a_metrics['大类数量']}")
if b_metrics['累计收益'] > a_metrics['累计收益']:
print(f" ✓ 累计收益提升 {b_metrics['累计收益'] - a_metrics['累计收益']:.2%}")
else:
print(f" ✗ 累计收益下降 {a_metrics['累计收益'] - b_metrics['累计收益']:.2%}")
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}")
print(f"\n【策略建议】")
print(f" 建议等待东南亚科技ETF积累更多数据后再测试")
print(f" 原因:")
print(f" 1. 数据量不足(仅{b_metrics['回测年数']:.1f}年)")
print(f" 2. 指数信号源暂不可用")
print(f" 3. 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)
# 设置回测结束日期
from datetime import datetime
base_config['end_date'] = datetime.now().strftime('%Y-%m-%d')
# ⚠ 重要由于东南亚ETF数据从2023年12月开始
# 需要调整start_date以匹配数据可用范围
# 本次测试将使用较短的时间窗口
print(f"\n{'='*60}")
print(f" A/B测试添加东南亚科技ETF受限测试")
print(f"{'='*60}")
print(f"\n⚠ 限制说明:")
print(f" - 东南亚科技ETF513730.SH2023年12月上市")
print(f" - 数据仅约2年回测时间范围受限")
print(f" - 指数数据暂不可用使用ETF价格作为信号源")
print(f" - 结果仅供参考,不建议直接用于决策")
# A组当前配置使用较短时间窗口
config_a = base_config.copy()
config_a['start_date'] = '2024-01-01' # 调整为东南亚ETF有数据的起始时间
a_metrics = run_backtest(config_a, "A组: 当前配置2024年起")
# B组添加东南亚科技
config_b = create_config_with_sea(base_config)
config_b['start_date'] = '2024-01-01'
b_metrics = run_backtest(config_b, "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_sea_etf.csv'
results_df.to_csv(results_path, index=False)
print(f"\n对比结果已保存: {results_path}")
if __name__ == '__main__':
main()