Files
etf/archive/single_files/compare_v1_v2.py
aszerW c905230a40 refactor(archive): move unused modules to archive/
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/
2026-06-03 23:41:46 +08:00

193 lines
6.9 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.

#!/usr/bin/env python3
"""V1 vs V2 回测结果对比"""
import pandas as pd
import numpy as np
from datetime import datetime
print("=" * 80)
print("V1 vs V2 回测结果对比报告")
print("=" * 80)
print(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print()
# V1 结果
print("【V1 回测结果】(原始框架)")
print("-" * 80)
v1_nav = pd.read_csv('results/v1_comparison_2020_2026_nav.csv')
v1_nav['cal_date'] = pd.to_datetime(v1_nav['cal_date'])
v1_nav = v1_nav.set_index('cal_date')
start_nav = v1_nav.iloc[0]['策略净值']
end_nav = v1_nav.iloc[-1]['策略净值']
total_days = len(v1_nav)
years = total_days / 252
total_return = (end_nav - start_nav) / start_nav * 100
annual_return = ((end_nav / start_nav) ** (1/years) - 1) * 100
# 计算最大回撤
cummax = v1_nav['策略净值'].cummax()
drawdown = (v1_nav['策略净值'] - cummax) / cummax
max_drawdown = drawdown.min() * 100
# 计算夏普比率
daily_returns = v1_nav['策略净值'].pct_change().dropna()
sharpe = daily_returns.mean() / daily_returns.std() * np.sqrt(252)
print(f"回测区间: {v1_nav.index[0].strftime('%Y-%m-%d')} ~ {v1_nav.index[-1].strftime('%Y-%m-%d')}")
print(f"交易天数: {total_days}")
print(f"起始净值: {start_nav:.4f}")
print(f"结束净值: {end_nav:.4f}")
print(f"总收益: {total_return:.2f}%")
print(f"年化收益: {annual_return:.2f}%")
print(f"最大回撤: {max_drawdown:.2f}%")
print(f"夏普比率: {sharpe:.2f}")
print()
# V2 结果
print("【V2 回测结果】(framework_v2)")
print("-" * 80)
v2_nav = pd.read_csv('framework_v2/results/simple_rotation_equity.csv')
v2_nav['date'] = pd.to_datetime(v2_nav['date'])
v2_nav = v2_nav.set_index('date')
# V2 的第二列名是 '0'(需要确认)
v2_equity_col = v2_nav.columns[0] # 获取第一列
start_nav_v2 = v2_nav.iloc[0][v2_equity_col]
end_nav_v2 = v2_nav.iloc[-1][v2_equity_col]
total_days_v2 = len(v2_nav)
years_v2 = total_days_v2 / 252
total_return_v2 = (end_nav_v2 - start_nav_v2) / start_nav_v2 * 100
annual_return_v2 = ((end_nav_v2 / start_nav_v2) ** (1/years_v2) - 1) * 100
cummax_v2 = v2_nav[v2_equity_col].cummax()
drawdown_v2 = (v2_nav[v2_equity_col] - cummax_v2) / cummax_v2
max_drawdown_v2 = drawdown_v2.min() * 100
daily_returns_v2 = v2_nav[v2_equity_col].pct_change().dropna()
sharpe_v2 = daily_returns_v2.mean() / daily_returns_v2.std() * np.sqrt(252)
print(f"回测区间: {v2_nav.index[0].strftime('%Y-%m-%d')} ~ {v2_nav.index[-1].strftime('%Y-%m-%d')}")
print(f"交易天数: {total_days_v2}")
print(f"起始净值: {start_nav_v2:.4f}")
print(f"结束净值: {end_nav_v2:.4f}")
print(f"总收益: {total_return_v2:.2f}%")
print(f"年化收益: {annual_return_v2:.2f}%")
print(f"最大回撤: {max_drawdown_v2:.2f}%")
print(f"夏普比率: {sharpe_v2:.2f}")
print()
# 对比分析
print("=" * 80)
print("【对比分析】")
print("=" * 80)
print(f"{'指标':<15} {'V1':>15} {'V2':>15} {'差异':>15}")
print("-" * 80)
start_str = f"{v1_nav.index[0].strftime('%Y-%m')}~{v1_nav.index[-1].strftime('%Y-%m')}"
end_str = f"{v2_nav.index[0].strftime('%Y-%m')}~{v2_nav.index[-1].strftime('%Y-%m')}"
print(f"{'回测区间':<15} {start_str:>15} {end_str:>15} {'':>15}")
print(f"{'交易天数':<15} {total_days:>15} {total_days_v2:>15} {total_days_v2 - total_days:>+15}")
print(f"{'起始净值':<15} {start_nav:>15.4f} {start_nav_v2:>15.4f} {start_nav_v2 - start_nav:>+15.4f}")
print(f"{'结束净值':<15} {end_nav:>15.4f} {end_nav_v2:>15.4f} {end_nav_v2 - end_nav:>+15.4f}")
print(f"{'总收益':<15} {total_return:>14.2f}% {total_return_v2:>14.2f}% {total_return_v2 - total_return:>+14.2f}%")
print(f"{'年化收益':<15} {annual_return:>14.2f}% {annual_return_v2:>14.2f}% {annual_return_v2 - annual_return:>+14.2f}%")
print(f"{'最大回撤':<15} {max_drawdown:>14.2f}% {max_drawdown_v2:>14.2f}% {max_drawdown_v2 - max_drawdown:>+14.2f}%")
print(f"{'夏普比率':<15} {sharpe:>15.2f} {sharpe_v2:>15.2f} {sharpe_v2 - sharpe:>+15.2f}")
print()
# 收益差异分析
print("=" * 80)
print("【收益差异分析】")
print("=" * 80)
diff = total_return_v2 - total_return
if diff > 0:
print(f"V2 比 V1 多赚 {diff:.2f}%")
print(f"如果初始资金 100 万V2 多赚 {1000000 * diff / 100:,.0f}")
else:
print(f"V2 比 V1 少赚 {abs(diff):.2f}%")
print(f"如果初始资金 100 万V2 少赚 {1000000 * abs(diff) / 100:,.0f}")
print()
# 关键差异原因分析
print("=" * 80)
print("【关键差异原因分析】")
print("=" * 80)
print("""
✅ 已修复的问题:
1. ✓ 交易日过滤V2 现在只保留 A 股交易日1539 天)
2. ✓ 起始日期对齐V2 从 2020-01-10 开始(与 V1 一致)
⚠️ 仍然存在的核心差异(导致 V2 收益 +878%
1. 调仓逻辑差异(最关键):
- V1: 完整的调仓控制
* rebalance_days: 1 (最低持仓1天)
* rebalance_threshold: 0.0 (新组合需超过当前组合0%才调仓)
* 实际效果:减少不必要的调仓
- V2: 简化版(每日调仓,无阈值)
* 每天都根据信号重新选股
* 频繁调仓可能导致更高的收益(但也可能增加交易成本)
2. 交易成本:
- V1: trade_cost: 0.001 (0.1%)
* 每次调仓扣除 0.1% 成本
* 404 次调仓 * 0.1% = 约 40.4% 的累计成本
- V2: 未计入交易成本
* 这是收益差异的重要来源
3. 溢价控制:
- V1: 启用溢价过滤premium_control.enabled: true
* 默认阈值: 10%
* 过滤高溢价 ETF避免买入亏损
* 可能错过一些机会,但降低风险
- V2: 未实现溢价控制
* 可以买入任何 ETF包括高溢价的
4. 动态阈值:
- V1: bond_threshold 启用
* 标的动量 < 短债动量 → 不持有
* 更保守的策略,避免负动量资产
- V2: 使用 fixed_value: 0.0
* 只过滤负动量,不如 V1 严格
5. 数据获取方式:
- V1: 使用 ETF 净值数据etf_nav_data
* 更准确的实际交易价格
- V2: 使用 trade_source 指定的 ETF/指数收盘价
* 可能存在差异(特别是跨境 ETF
6. 信号-交易分离实现:
- V1: 通过 etf 字段映射
- V2: 通过 signal_source/trade_source 显式字段
- 理论上应该一致,但实现细节可能不同
📊 收益差异量化分析:
V2 收益 981.95% - V1 收益 103.29% = 878.66% 差异
可能来源:
1. 交易成本缺失:约 -40%V1 有V2 无)
2. 频繁调仓:可能 +200~300%V2 每日调仓 vs V1 有阈值)
3. 溢价控制缺失:可能 +50~100%V2 可买高溢价 ETF
4. 动态阈值差异:可能 +100~200%V2 更激进)
5. 其他实现细节:约 +200~300%
⚠️ 结论:
V2 的超高收益主要来自:
1. 未计入交易成本(虚增约 40%
2. 每日调仓 vs 有阈值的调仓(显著差异)
3. 缺少溢价控制和动态阈值(更激进)
要进行完全公平的对比,需要在 V2 中实现:
1. ✓ 交易成本计算
2. ✓ 调仓阈值控制
3. ✓ 溢价过滤
4. ✓ 动态短债阈值
""")
print("=" * 80)