Files
etf/tests/experiments/ab_test_emerging_market.py
aszerW 9776ae7de0 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
2026-05-06 22:23:12 +08:00

198 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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