Files
etf/tests/verify_mode_b.py
aszerW 5212b004dc fix: 回测细节导出、交易日历测试和动量因子修复
修复项:
- export_backtest_detail.py: 统一回测导出脚本的数据源调用逻辑
- test_trading_calendar.py: 交易日历功能测试
- verify_fix_result.py: 修复结果验证
- verify_mode_b.py: 模式 B 验证

策略修复:
- momentum.py: 动量因子计算优化
- strategy.py: StrategyBase 数据获取修复(fetch_indices 返回 dict)
2026-05-24 14:26:35 +08:00

208 lines
6.6 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.

#!/usr/bin/env python3
"""
验证 Mode B: 指数信号 → ETF收益
文档预期结果:
CAGR: 28.07%, 最大回撤: -13.34%, 夏普: 1.685, Calmar: 2.104
"""
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from dotenv import load_dotenv
load_dotenv()
import pandas as pd
import numpy as np
import yaml
from datetime import datetime
from strategies.rotation.strategy import RotationStrategy
def calculate_metrics(nav: pd.Series) -> dict:
"""计算绩效指标"""
start_date = nav.index[0]
end_date = nav.index[-1]
days = (end_date - start_date).days
years = days / 365
total_return = nav.iloc[-1] - 1
cagr = (nav.iloc[-1] / nav.iloc[0]) ** (1/years) - 1
daily_ret = nav.pct_change().dropna()
sharpe = daily_ret.mean() / daily_ret.std() * np.sqrt(252) if daily_ret.std() > 0 else 0
peak = nav.cummax()
drawdown = (nav - peak) / peak
max_dd = drawdown.min()
calmar = cagr / abs(max_dd) if max_dd != 0 else 0
win_rate = (daily_ret > 0).sum() / len(daily_ret)
return {
'start_date': start_date.strftime('%Y-%m-%d'),
'end_date': end_date.strftime('%Y-%m-%d'),
'years': years,
'days': len(nav),
'total_return': total_return,
'cagr': cagr,
'max_dd': max_dd,
'sharpe': sharpe,
'calmar': calmar,
'win_rate': win_rate
}
def run_mode_b_backtest(data: dict, signals: pd.DataFrame, valid_codes: list,
etf_code_map: dict, a_share_dates: pd.DatetimeIndex,
trade_cost: float, select_num: int) -> dict:
"""
Mode B: 使用ETF价格计算收益
Args:
data: 包含 etf_data 的数据字典
signals: 指数生成的信号
valid_codes: 指数代码列表
etf_code_map: {指数代码: ETF代码} 映射
a_share_dates: A股交易日历
trade_cost: 交易成本
select_num: 选股数量
"""
from framework.execution import BacktestExecutor
etf_data = data.get('etf_data')
if etf_data is None:
print("❌ ETF数据不可用")
return {'result': None}
# 将信号对齐到 A 股日历
if a_share_dates is not signals.index:
signals = signals.reindex(a_share_dates, method='ffill').dropna(subset=[signals.columns[0]])
# 使用ETF收盘价计算收益率
returns_data = {}
for code in valid_codes:
etf_code = etf_code_map.get(code)
if etf_code and etf_code in etf_data.columns:
etf_close = etf_data[etf_code].dropna()
# 对齐到A股日历
etf_aligned = etf_close.reindex(a_share_dates, method='ffill')
returns_aligned = etf_aligned.pct_change(fill_method=None)
# 使用指数代码作为列名(与信号匹配)
returns_data[f'日收益率_{code}'] = returns_aligned
else:
# 没有ETF映射的标的回退使用指数数据
index_data = data.get('index_data', {})
if code in index_data and 'close' in index_data[code].columns:
close_series = index_data[code]['close'].dropna()
close_aligned = close_series.reindex(a_share_dates, method='ffill')
returns_data[f'日收益率_{code}'] = close_aligned.pct_change(fill_method=None)
returns_df = pd.DataFrame(returns_data)
# 对齐日期
common_dates = signals.index.intersection(returns_df.index)
signals = signals.loc[common_dates]
returns_df = returns_df.loc[common_dates]
print(f" Mode B 对齐后日期: {len(common_dates)}")
print(f" 使用ETF计算收益: {len([c for c in valid_codes if etf_code_map.get(c)])}")
executor = BacktestExecutor(
initial_capital=100000,
trade_cost=trade_cost,
select_num=select_num
)
portfolio = executor.execute(signals, returns_df)
if hasattr(portfolio, 'backtest_result'):
return {'result': portfolio.backtest_result, 'portfolio': portfolio}
return {'result': None}
def main():
# 加载配置
config_path = project_root / 'strategies/rotation/config.yaml'
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
# 设置回测区间
config['start_date'] = '2020-01-02'
config['end_date'] = '2026-05-19'
print('='*70)
print('Mode B 验证: 指数信号 → ETF收益')
print('='*70)
# 初始化策略
strategy = RotationStrategy(config)
# 获取数据
print('\n获取数据...')
data = strategy.get_data(use_flask_api=False)
# 计算因子(使用指数数据)
print('\n计算因子(指数信号)...')
factor_df = strategy.compute_factors(data)
# 生成信号
print('\n生成信号...')
signals = strategy.generate_signals(factor_df)
# 执行 Mode B 回测
print('\n执行 Mode B 回测ETF收益...')
result_b = run_mode_b_backtest(
data=data,
signals=signals,
valid_codes=data['valid_codes'],
etf_code_map=data['etf_code_map'],
a_share_dates=data.get('a_share_dates'),
trade_cost=config.get('trade_cost', 0.001),
select_num=config.get('select_num', 3)
)
if result_b.get('result') is None:
print('❌ Mode B 回测未生成结果')
return
# 计算指标
nav_b = result_b['result']['策略净值']
metrics_b = calculate_metrics(nav_b)
# 输出结果
print('\n' + '='*70)
print('Mode B 回测结果')
print('='*70)
print(f"回测区间: {metrics_b['start_date']} ~ {metrics_b['end_date']}")
print(f"回测年数: {metrics_b['years']:.2f}")
print(f"交易天数: {metrics_b['days']}")
print('-'*70)
print(f"CAGR: {metrics_b['cagr']:.2%}")
print(f"最大回撤: {metrics_b['max_dd']:.2%}")
print(f"夏普比率: {metrics_b['sharpe']:.3f}")
print(f"Calmar比率: {metrics_b['calmar']:.3f}")
print(f"日胜率: {metrics_b['win_rate']:.2%}")
print(f"累计收益: {metrics_b['total_return']:.2%}")
print('='*70)
# 文档预期对比
print('\n文档预期 (Mode B):')
print(' CAGR: 28.07%, MaxDD -13.34%, Sharpe 1.685, Calmar 2.104')
cagr_diff = abs(metrics_b['cagr'] - 0.2807)
print(f'\nCAGR差异: {cagr_diff:.2%}')
if cagr_diff < 0.05:
print('✓ 结果与文档预期基本一致')
else:
print('⚠ 结果与文档预期有差异')
return metrics_b
if __name__ == '__main__':
main()