Files
etf/archive/legacy_tests/tests/experiments/ab_test_sea_etf.py
aszerW 1fca536c95 refactor: 归档旧代码,保留新框架结构
归档内容:
- core/ (数据源、因子计算、通用工具) → archive/legacy_core/
- strategies/rotation/engine.py, portfolio.py, report.py → archive/legacy_core/
- scripts/ (run_rotation, daily_scheduler) → archive/legacy_scripts/
- examples/ → archive/legacy_examples/
- tests/ (实验、对比测试) → archive/legacy_tests/
- 单独文件 (fetch_*.py, 动量.py, 全球市场.py等) → archive/single_files/

保留新结构:
- framework/ (抽象接口)
- strategies/shared/ (定制组件)
- strategies/rotation/strategy.py (新策略)
- 外层配置: .env, .dockerignore, build-and-push.sh, hk_ecs.pem, README.md, requirements.txt
- Docker相关: Dockerfile, Dockerfile_base, docker-compose.yml

更新README反映新框架架构
2026-05-11 23:34:23 +08:00

208 lines
7.4 KiB
Python
Raw Permalink 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测试添加东南亚科技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()