Archive legacy framework and utility modules that are no longer referenced by the active core (datasource/ and rotation/): - framework/ -> archive/framework/ - framework_v2/ -> archive/framework_v2/ - strategies/ -> archive/strategies/ - config/ -> archive/config/ - visualization/ -> archive/visualization/ - scripts/ -> archive/scripts/ - tests/ -> archive/tests/ - run_rotation.py, run_us_rotation.py -> archive/single_files/ - compare_*.py, test_api_dates.py -> archive/single_files/
208 lines
6.6 KiB
Python
208 lines
6.6 KiB
Python
#!/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() |