Files
etf/tests/experiments/ab_test_france.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

200 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测试添加法国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()