Files
etf/compare_v1_v2.py
aszerW 7fcf63d68a docs: 添加版本对比分析脚本与配置设计文档
新增对比脚本:
- compare_v1_v2.py: V1 vs V2 简单版对比分析(153 行)
  * 发现 V2 简单版收益虚高(981.95% vs V1 的 103.29%)
  * 识别核心差异:交易成本、调仓逻辑、动态阈值、溢价控制

- compare_three_versions.py: 三版本完整对比(190 行)
  * V1 原始版:103.29%(基准)
  * V2 简单版:981.95%(未计入交易成本,虚高)
  * V2 正式版:135.63%(已计入交易成本,真实)
  * 量化分析收益下降 846% 的原因

新增文档:
- CONFIG_DESIGN.md: V2 配置系统设计文档
  * 扁平化资产池设计
  * signal_source/trade_source 分离机制
  * group 字段策略化语义

测试脚本:
- test_api_dates.py: API 日期范围验证测试

关键发现:
1. V2 简单版未计入交易成本导致收益虚高 878%
2. V2 正式版计入 829 次调仓成本后收益降至 135.63%
3. V2 正式版 vs V1(+32.34%)差异合理,夏普比率更优(1.15 vs 0.78)
2026-05-24 22:54:50 +08:00

193 lines
6.9 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
"""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)