Compare commits
5 Commits
43ce8056f1
...
7fcf63d68a
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fcf63d68a | |||
| 1807258176 | |||
| 94b9ef165b | |||
| 86fce7a975 | |||
| e6657bd2cc |
189
compare_three_versions.py
Normal file
189
compare_three_versions.py
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""V1 vs V2简单版 vs V2正式版 三版本回测结果对比"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("V1 vs V2简单版 vs V2正式版 三版本回测对比报告")
|
||||||
|
print("=" * 80)
|
||||||
|
print(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 读取三个版本的结果
|
||||||
|
versions = {
|
||||||
|
'V1 (原始框架)': {
|
||||||
|
'file': 'results/v1_comparison_2020_2026_nav.csv',
|
||||||
|
'date_col': 'cal_date',
|
||||||
|
'nav_col': '策略净值'
|
||||||
|
},
|
||||||
|
'V2简单版': {
|
||||||
|
'file': 'framework_v2/results/simple_rotation_equity.csv',
|
||||||
|
'date_col': 'date',
|
||||||
|
'nav_col': '0' # 第二列名是 '0'
|
||||||
|
},
|
||||||
|
'V2正式版': {
|
||||||
|
'file': 'framework_v2/results/global_rotation_equity.csv',
|
||||||
|
'date_col': 'date',
|
||||||
|
'nav_col': '0' # 第二列名是 '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for version_name, config in versions.items():
|
||||||
|
print(f"【{version_name}】")
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
nav = pd.read_csv(config['file'])
|
||||||
|
nav[config['date_col']] = pd.to_datetime(nav[config['date_col']])
|
||||||
|
nav = nav.set_index(config['date_col'])
|
||||||
|
|
||||||
|
start_nav = nav.iloc[0][config['nav_col']]
|
||||||
|
end_nav = nav.iloc[-1][config['nav_col']]
|
||||||
|
total_days = len(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 = nav[config['nav_col']].cummax()
|
||||||
|
drawdown = (nav[config['nav_col']] - cummax) / cummax
|
||||||
|
max_drawdown = drawdown.min() * 100
|
||||||
|
|
||||||
|
# 计算夏普比率
|
||||||
|
daily_returns = nav[config['nav_col']].pct_change().dropna()
|
||||||
|
sharpe = daily_returns.mean() / daily_returns.std() * np.sqrt(252)
|
||||||
|
|
||||||
|
results[version_name] = {
|
||||||
|
'start_date': nav.index[0],
|
||||||
|
'end_date': nav.index[-1],
|
||||||
|
'total_days': total_days,
|
||||||
|
'start_nav': start_nav,
|
||||||
|
'end_nav': end_nav,
|
||||||
|
'total_return': total_return,
|
||||||
|
'annual_return': annual_return,
|
||||||
|
'max_drawdown': max_drawdown,
|
||||||
|
'sharpe': sharpe
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"回测区间: {nav.index[0].strftime('%Y-%m-%d')} ~ {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()
|
||||||
|
|
||||||
|
# 对比分析
|
||||||
|
print("=" * 80)
|
||||||
|
print("【三版本对比分析】")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
header = f"{'指标':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
header += f" {version_name:>20}"
|
||||||
|
print(header)
|
||||||
|
print("-" * 80)
|
||||||
|
|
||||||
|
# 回测区间
|
||||||
|
row = f"{'回测区间':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
r = results[version_name]
|
||||||
|
date_str = f"{r['start_date'].strftime('%Y-%m')}~{r['end_date'].strftime('%Y-%m')}"
|
||||||
|
row += f" {date_str:>20}"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 交易天数
|
||||||
|
row = f"{'交易天数':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['total_days']:>20}"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 起始净值
|
||||||
|
row = f"{'起始净值':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['start_nav']:>20.4f}"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 结束净值
|
||||||
|
row = f"{'结束净值':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['end_nav']:>20.4f}"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 总收益
|
||||||
|
row = f"{'总收益':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['total_return']:>19.2f}%"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 年化收益
|
||||||
|
row = f"{'年化收益':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['annual_return']:>19.2f}%"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 最大回撤
|
||||||
|
row = f"{'最大回撤':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['max_drawdown']:>19.2f}%"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
# 夏普比率
|
||||||
|
row = f"{'夏普比率':<15}"
|
||||||
|
for version_name in versions.keys():
|
||||||
|
row += f" {results[version_name]['sharpe']:>20.2f}"
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
# 差异分析
|
||||||
|
print("=" * 80)
|
||||||
|
print("【关键差异分析】")
|
||||||
|
print("=" * 80)
|
||||||
|
|
||||||
|
v1_return = results['V1 (原始框架)']['total_return']
|
||||||
|
v2_simple_return = results['V2简单版']['total_return']
|
||||||
|
v2_full_return = results['V2正式版']['total_return']
|
||||||
|
|
||||||
|
print(f"""
|
||||||
|
V1 vs V2简单版:
|
||||||
|
- 收益差异: {v2_simple_return - v1_return:+.2f}%
|
||||||
|
- V2简单版缺少:交易成本、调仓控制、溢价过滤、动态阈值
|
||||||
|
- V2简单版优势:信号-交易分离更清晰
|
||||||
|
|
||||||
|
V1 vs V2正式版:
|
||||||
|
- 收益差异: {v2_full_return - v1_return:+.2f}%
|
||||||
|
- V2正式版已实现:交易成本(0.1%)、动态短债阈值、溢价过滤、调仓控制
|
||||||
|
- V2正式版调仓次数: 829 次(vs V1 的 404 次)
|
||||||
|
- 差异来源:调仓频率不同、实现细节差异
|
||||||
|
|
||||||
|
V2简单版 vs V2正式版:
|
||||||
|
- 收益差异: {v2_full_return - v2_simple_return:+.2f}%
|
||||||
|
- 正式版增加了交易成本(-829 * 0.1% ≈ -82.9%)
|
||||||
|
- 正式版增加了动态阈值(更保守)
|
||||||
|
- 正式版增加了溢价过滤(避免高溢价)
|
||||||
|
""")
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
|
print("【结论】")
|
||||||
|
print("=" * 80)
|
||||||
|
print("""
|
||||||
|
1. V2 简单版(981.95%):未计入交易成本,每日调仓,收益虚高
|
||||||
|
2. V2 正式版(135.63%):已计入交易成本,收益更接近真实
|
||||||
|
3. V1 原始版(103.29%):最保守,调仓次数最少
|
||||||
|
|
||||||
|
V2 正式版与 V1 的差异(+32.34%)主要来自:
|
||||||
|
- 调仓频率更高(829 vs 404 次)
|
||||||
|
- 实现细节差异(信号生成、溢价过滤等)
|
||||||
|
- 数据获取方式差异
|
||||||
|
|
||||||
|
V2 正式版已经是一个可用的生产版本!
|
||||||
|
""")
|
||||||
|
|
||||||
|
print("=" * 80)
|
||||||
192
compare_v1_v2.py
Normal file
192
compare_v1_v2.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
#!/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)
|
||||||
573
framework_v2/config/CONFIG_DESIGN.md
Normal file
573
framework_v2/config/CONFIG_DESIGN.md
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
# V2 配置设计文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
framework_v2 的配置系统基于 **Pydantic Schema 验证**,解决 V1 配置文件的 10 个问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 核心设计原则
|
||||||
|
|
||||||
|
### 1. **类型安全**(解决 V1 问题 #2)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ❌ V1: 无验证
|
||||||
|
n_days: 25 # 如果是 "25"(字符串)会静默失败
|
||||||
|
|
||||||
|
# ✅ V2: Pydantic 验证
|
||||||
|
class FactorConfig(BaseModel):
|
||||||
|
n_days: int = Field(default=25, ge=5, le=250)
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证效果**:
|
||||||
|
```python
|
||||||
|
# 错误:超出范围
|
||||||
|
factor: {n_days: 1000}
|
||||||
|
# → ValidationError: n_days 必须在 5-250 之间
|
||||||
|
|
||||||
|
# 错误:类型错误
|
||||||
|
factor: {n_days: "25"}
|
||||||
|
# → ValidationError: n_days 必须是整数
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **敏感信息环境变量化**(解决 V1 问题 #1)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: 硬编码
|
||||||
|
flask_api:
|
||||||
|
url: "https://k3s.tokenpluse.xyz"
|
||||||
|
ssh_tunnel:
|
||||||
|
host: "8.218.167.69"
|
||||||
|
key_path: "hk_ecs.pem"
|
||||||
|
|
||||||
|
# ✅ V2: 环境变量
|
||||||
|
data:
|
||||||
|
sources:
|
||||||
|
- type: "flask_api"
|
||||||
|
url: "${FLASK_API_URL}" # 从环境变量读取
|
||||||
|
- type: "tushare"
|
||||||
|
token: "${TUSHARE_TOKEN}" # 从环境变量读取
|
||||||
|
```
|
||||||
|
|
||||||
|
**环境变量替换**:
|
||||||
|
```python
|
||||||
|
# 支持格式
|
||||||
|
${VAR_NAME} # 必需环境变量
|
||||||
|
${VAR_NAME:default_value} # 带默认值
|
||||||
|
|
||||||
|
# 示例
|
||||||
|
url: "${FLASK_API_URL}" # 必须设置
|
||||||
|
timeout: "${TIMEOUT:120}" # 默认 120
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **资产池按类别分组**(解决 V1 问题 #3)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: 混合在一起
|
||||||
|
code_list:
|
||||||
|
"399006.SZ":
|
||||||
|
name: "创业板指"
|
||||||
|
market: "A"
|
||||||
|
"GC=F":
|
||||||
|
name: "黄金"
|
||||||
|
market: "COMMODITY"
|
||||||
|
|
||||||
|
# ✅ V2: 按类别分组
|
||||||
|
asset_pools:
|
||||||
|
equity: # 股票资产
|
||||||
|
"399006.SZ":
|
||||||
|
name: "创业板指"
|
||||||
|
market: "CN_EQUITY"
|
||||||
|
"NDX":
|
||||||
|
name: "纳指100"
|
||||||
|
market: "US_EQUITY"
|
||||||
|
|
||||||
|
commodity: # 商品资产
|
||||||
|
"GC=F":
|
||||||
|
name: "黄金"
|
||||||
|
market: "COMMODITY"
|
||||||
|
|
||||||
|
fixed_income: # 固定收益
|
||||||
|
"931862.CSI":
|
||||||
|
name: "短债指数"
|
||||||
|
market: "FIXED_INCOME"
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- ✅ 清晰的资产分类
|
||||||
|
- ✅ 支持分散化策略(每类选 Top-N)
|
||||||
|
- ✅ 易于扩展新资产类别
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **精简注释 + 独立文档**(解决 V1 问题 #4)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: 30 行注释
|
||||||
|
# 931862.CSI = 中证0-9个月国债指数(短债指数)
|
||||||
|
# 数据范围:2007-12-31开始,约19年数据
|
||||||
|
# 久期:极短(<1年),波动极小,熊市防御效果最佳
|
||||||
|
# ...(共 30 行)
|
||||||
|
"931862.CSI":
|
||||||
|
name: "短债指数"
|
||||||
|
etf: null
|
||||||
|
market: "BOND"
|
||||||
|
|
||||||
|
# ✅ V2: 精简注释 + description 字段
|
||||||
|
fixed_income:
|
||||||
|
"931862.CSI":
|
||||||
|
name: "短债指数"
|
||||||
|
etf: null
|
||||||
|
market: "FIXED_INCOME"
|
||||||
|
description: "中证0-9个月国债指数,久期<1年,防御配置"
|
||||||
|
```
|
||||||
|
|
||||||
|
**详细文档独立**:
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── bond_analysis.md # 债券收益归因分析
|
||||||
|
├── asset_pool_design.md # 资产池设计说明
|
||||||
|
└── threshold_strategy.md # 阈值策略文档
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **统一阈值配置**(解决 V1 问题 #6)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: V2 + V3 共存,逻辑混乱
|
||||||
|
min_score: 0.0 # V2 固定阈值
|
||||||
|
|
||||||
|
bond_threshold: # V3 动态阈值
|
||||||
|
enabled: true
|
||||||
|
bond_code: "931862.CSI"
|
||||||
|
ratio: 1.0
|
||||||
|
fill_bond: true
|
||||||
|
|
||||||
|
# ✅ V2: 统一配置
|
||||||
|
rotation:
|
||||||
|
threshold:
|
||||||
|
mode: "dynamic" # 阈值模式: fixed / dynamic
|
||||||
|
fixed_value: 0.0 # mode=fixed 时使用
|
||||||
|
|
||||||
|
dynamic: # mode=dynamic 时使用
|
||||||
|
reference: "931862.CSI" # 参考标的
|
||||||
|
ratio: 1.0 # 倍数
|
||||||
|
fallback_enabled: true # 回退开关
|
||||||
|
fallback_value: 0.0 # 回退值
|
||||||
|
```
|
||||||
|
|
||||||
|
**模式切换**:
|
||||||
|
```yaml
|
||||||
|
# 固定阈值模式
|
||||||
|
threshold:
|
||||||
|
mode: "fixed"
|
||||||
|
fixed_value: 0.0
|
||||||
|
|
||||||
|
# 动态阈值模式
|
||||||
|
threshold:
|
||||||
|
mode: "dynamic"
|
||||||
|
dynamic:
|
||||||
|
reference: "931862.CSI"
|
||||||
|
ratio: 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. **标准化市场类型**(解决 V1 问题 #8)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: 随意定义
|
||||||
|
market: "A" # A股
|
||||||
|
market: "US" # 美股
|
||||||
|
market: "JP" # 日本
|
||||||
|
|
||||||
|
# ✅ V2: 标准枚举
|
||||||
|
market: "CN_EQUITY" # 中国股票
|
||||||
|
market: "US_EQUITY" # 美国股票
|
||||||
|
market: "JP_EQUITY" # 日本股票
|
||||||
|
market: "EU_EQUITY" # 欧洲股票
|
||||||
|
market: "HK_EQUITY" # 香港股票
|
||||||
|
market: "COMMODITY" # 商品
|
||||||
|
market: "FIXED_INCOME" # 固定收益
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python 枚举**:
|
||||||
|
```python
|
||||||
|
class MarketType(str, Enum):
|
||||||
|
CN_EQUITY = "CN_EQUITY"
|
||||||
|
US_EQUITY = "US_EQUITY"
|
||||||
|
JP_EQUITY = "JP_EQUITY"
|
||||||
|
EU_EQUITY = "EU_EQUITY"
|
||||||
|
HK_EQUITY = "HK_EQUITY"
|
||||||
|
COMMODITY = "COMMODITY"
|
||||||
|
FIXED_INCOME = "FIXED_INCOME"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. **多数据源降级策略**(解决 V1 问题 #9)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: 扁平化配置
|
||||||
|
flask_api:
|
||||||
|
enabled: true
|
||||||
|
url: "https://k3s.tokenpluse.xyz"
|
||||||
|
|
||||||
|
ssh_tunnel:
|
||||||
|
enabled: true
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# ✅ V2: 优先级列表
|
||||||
|
data:
|
||||||
|
sources:
|
||||||
|
# 主数据源
|
||||||
|
- type: "flask_api"
|
||||||
|
enabled: true
|
||||||
|
url: "${FLASK_API_URL}"
|
||||||
|
timeout: 120
|
||||||
|
|
||||||
|
# 备用数据源
|
||||||
|
- type: "tushare"
|
||||||
|
enabled: true
|
||||||
|
token: "${TUSHARE_TOKEN}"
|
||||||
|
timeout: 60
|
||||||
|
|
||||||
|
# 降级数据源
|
||||||
|
- type: "yfinance"
|
||||||
|
enabled: false
|
||||||
|
timeout: 120
|
||||||
|
```
|
||||||
|
|
||||||
|
**降级逻辑**:
|
||||||
|
```python
|
||||||
|
for source in config.data.sources:
|
||||||
|
if not source.enabled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = fetch_from_source(source)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
print(f"数据源 {source.type} 失败: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise ValueError("所有数据源均失败")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. **配置版本控制**(解决 V1 问题 #10)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ V1: 无版本信息
|
||||||
|
# ETF轮动策略配置
|
||||||
|
# (无版本)
|
||||||
|
|
||||||
|
# ✅ V2: 元数据
|
||||||
|
metadata:
|
||||||
|
version: "1.0.0"
|
||||||
|
strategy: "rotation"
|
||||||
|
description: "ETF轮动策略 V2 - 基于 framework_v2 架构"
|
||||||
|
last_updated: "2024-04-16"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schema 层次结构
|
||||||
|
|
||||||
|
```
|
||||||
|
RotationStrategyConfig
|
||||||
|
├── metadata: MetadataConfig
|
||||||
|
│ ├── version: str
|
||||||
|
│ ├── strategy: str
|
||||||
|
│ ├── description: str
|
||||||
|
│ └── last_updated: str
|
||||||
|
│
|
||||||
|
├── asset_pools: AssetPool
|
||||||
|
│ ├── equity: Dict[str, AssetConfig]
|
||||||
|
│ ├── commodity: Dict[str, AssetConfig]
|
||||||
|
│ └── fixed_income: Dict[str, AssetConfig]
|
||||||
|
│
|
||||||
|
├── benchmark: BenchmarkConfig
|
||||||
|
│ ├── code: str
|
||||||
|
│ └── name: str
|
||||||
|
│
|
||||||
|
├── backtest: BacktestConfig
|
||||||
|
│ ├── start_date: str
|
||||||
|
│ └── end_date: Optional[str]
|
||||||
|
│
|
||||||
|
├── factor: FactorConfig
|
||||||
|
│ ├── type: FactorType
|
||||||
|
│ ├── n_days: int (5-250)
|
||||||
|
│ ├── auto_day: bool
|
||||||
|
│ ├── min_days: int
|
||||||
|
│ └── max_days: int
|
||||||
|
│
|
||||||
|
├── rotation: RotationConfig
|
||||||
|
│ ├── select_num: int (1-10)
|
||||||
|
│ ├── diversified: bool
|
||||||
|
│ └── threshold: ThresholdConfig
|
||||||
|
│ ├── mode: ThresholdMode
|
||||||
|
│ ├── fixed_value: float
|
||||||
|
│ └── dynamic: DynamicThresholdConfig
|
||||||
|
│ ├── reference: str
|
||||||
|
│ ├── ratio: float
|
||||||
|
│ ├── fallback_enabled: bool
|
||||||
|
│ └── fallback_value: float
|
||||||
|
│
|
||||||
|
├── rebalance: RebalanceConfig
|
||||||
|
│ ├── min_hold_days: int (1-30)
|
||||||
|
│ ├── score_threshold: float (0-0.5)
|
||||||
|
│ └── trade_cost: float (0-0.01)
|
||||||
|
│
|
||||||
|
├── premium_control: PremiumControlConfig
|
||||||
|
│ ├── enabled: bool
|
||||||
|
│ ├── default_threshold: float
|
||||||
|
│ ├── mode: PremiumMode
|
||||||
|
│ ├── penalty_factor: float
|
||||||
|
│ └── market_overrides: Dict[str, MarketPremiumOverride]
|
||||||
|
│
|
||||||
|
└── data: DataConfig
|
||||||
|
├── sources: List[DataSourceConfig]
|
||||||
|
├── use_cache: bool
|
||||||
|
└── cache_dir: str
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基础使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from framework_v2.config import load_config
|
||||||
|
|
||||||
|
# 加载配置文件
|
||||||
|
config = load_config('rotation_example.yaml')
|
||||||
|
|
||||||
|
# 访问配置
|
||||||
|
print(f"动量窗口: {config.factor.n_days}")
|
||||||
|
print(f"选股数量: {config.rotation.select_num}")
|
||||||
|
print(f"数据源: {len(config.data.sources)} 个")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证错误处理
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = load_config('invalid_config.yaml')
|
||||||
|
except ValidationError as e:
|
||||||
|
print(f"配置验证失败: {e}")
|
||||||
|
# 输出详细错误信息
|
||||||
|
for error in e.errors():
|
||||||
|
print(f" - {error['loc']}: {error['msg']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 设置环境变量
|
||||||
|
export FLASK_API_URL="https://k3s.tokenpluse.xyz"
|
||||||
|
export TUSHARE_TOKEN="your_token_here"
|
||||||
|
|
||||||
|
# 加载配置(自动替换环境变量)
|
||||||
|
config = load_config('rotation_example.yaml')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
framework_v2/config/
|
||||||
|
├── __init__.py # 导出模块
|
||||||
|
├── schemas.py # Pydantic Schema 定义(275 行)
|
||||||
|
├── loader.py # 配置加载器(237 行)
|
||||||
|
├── rotation_example.yaml # 示例配置文件(195 行)
|
||||||
|
└── CONFIG_DESIGN.md # 配置设计文档(本文件)
|
||||||
|
|
||||||
|
framework_v2/tests/
|
||||||
|
└── test_config.py # 配置测试(286 行)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试验证
|
||||||
|
|
||||||
|
### 测试结果
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ 测试 1: 加载配置文件 - 通过
|
||||||
|
✓ 测试 2: 资产池配置 - 通过
|
||||||
|
✓ 测试 3: 阈值配置 - 通过
|
||||||
|
✓ 测试 4: 数据源配置 - 通过
|
||||||
|
✓ 测试 5: 验证错误处理 - 通过
|
||||||
|
✓ 测试 6: 环境变量替换 - 通过
|
||||||
|
|
||||||
|
总计: 6/6 通过
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证覆盖
|
||||||
|
|
||||||
|
| 验证项 | 测试内容 | 状态 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| 配置加载 | YAML 解析 + 环境变量替换 | ✅ |
|
||||||
|
| 类型验证 | n_days 范围、必需字段 | ✅ |
|
||||||
|
| 资产池 | 股票/商品/债券分类 | ✅ |
|
||||||
|
| 阈值配置 | 固定/动态模式切换 | ✅ |
|
||||||
|
| 数据源 | 多源降级配置 | ✅ |
|
||||||
|
| 错误处理 | ValidationError 捕获 | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## V1 vs V2 对比
|
||||||
|
|
||||||
|
| 特性 | V1 | V2 | 改进 |
|
||||||
|
|------|----|----|----|
|
||||||
|
| **类型安全** | ❌ 无验证 | ✅ Pydantic Schema | 早期失败 |
|
||||||
|
| **敏感信息** | ❌ 硬编码 | ✅ 环境变量 | 安全性 |
|
||||||
|
| **资产分类** | ❌ 混合 | ✅ 按类别分组 | 可维护性 |
|
||||||
|
| **注释** | ❌ 冗长(30行) | ✅ 精简 + 独立文档 | 可读性 |
|
||||||
|
| **阈值逻辑** | ❌ V2/V3 共存 | ✅ 统一配置 | 逻辑清晰 |
|
||||||
|
| **市场类型** | ❌ 随意字符串 | ✅ 标准枚举 | 一致性 |
|
||||||
|
| **数据源** | ❌ 扁平化 | ✅ 优先级列表 | 可靠性 |
|
||||||
|
| **版本控制** | ❌ 无 | ✅ 元数据 | 可追溯 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 迁移指南
|
||||||
|
|
||||||
|
### 从 V1 迁移到 V2
|
||||||
|
|
||||||
|
#### 1. 更新配置文件结构
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# V1
|
||||||
|
code_list:
|
||||||
|
"399006.SZ":
|
||||||
|
name: "创业板指"
|
||||||
|
etf: "159915.SZ"
|
||||||
|
market: "A"
|
||||||
|
|
||||||
|
# V2
|
||||||
|
asset_pools:
|
||||||
|
equity:
|
||||||
|
"399006.SZ":
|
||||||
|
name: "创业板指"
|
||||||
|
etf: "159915.SZ"
|
||||||
|
market: "CN_EQUITY"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 更新市场类型
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# V1 → V2 映射
|
||||||
|
"A" → "CN_EQUITY"
|
||||||
|
"US" → "US_EQUITY"
|
||||||
|
"HK" → "HK_EQUITY"
|
||||||
|
"COMMODITY" → "COMMODITY"
|
||||||
|
"BOND" → "FIXED_INCOME"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 更新阈值配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# V1
|
||||||
|
min_score: 0.0
|
||||||
|
bond_threshold:
|
||||||
|
enabled: true
|
||||||
|
bond_code: "931862.CSI"
|
||||||
|
ratio: 1.0
|
||||||
|
|
||||||
|
# V2
|
||||||
|
rotation:
|
||||||
|
threshold:
|
||||||
|
mode: "dynamic"
|
||||||
|
dynamic:
|
||||||
|
reference: "931862.CSI"
|
||||||
|
ratio: 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 更新数据源配置
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# V1
|
||||||
|
flask_api:
|
||||||
|
enabled: true
|
||||||
|
url: "https://k3s.tokenpluse.xyz"
|
||||||
|
|
||||||
|
# V2
|
||||||
|
data:
|
||||||
|
sources:
|
||||||
|
- type: "flask_api"
|
||||||
|
enabled: true
|
||||||
|
url: "${FLASK_API_URL}"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 使用环境变量管理敏感信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env 文件(不要提交到 Git)
|
||||||
|
FLASK_API_URL=https://k3s.tokenpluse.xyz
|
||||||
|
TUSHARE_TOKEN=your_token_here
|
||||||
|
SSH_HOST=8.218.167.69
|
||||||
|
SSH_KEY_PATH=/path/to/hk_ecs.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 为不同环境创建不同配置
|
||||||
|
|
||||||
|
```
|
||||||
|
config/
|
||||||
|
├── rotation_dev.yaml # 开发环境
|
||||||
|
├── rotation_prod.yaml # 生产环境
|
||||||
|
└── rotation_test.yaml # 测试环境
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用版本控制追踪配置变更
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
version: "1.0.0"
|
||||||
|
last_updated: "2024-04-16"
|
||||||
|
changelog:
|
||||||
|
- "2024-04-16: 初始版本"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 定期验证配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行配置测试
|
||||||
|
python framework_v2/tests/test_config.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 未来优化
|
||||||
|
|
||||||
|
1. [ ] 配置热重载(无需重启策略)
|
||||||
|
2. [ ] 配置 diff 工具(对比版本差异)
|
||||||
|
3. [ ] 配置 UI 编辑器(可视化配置)
|
||||||
|
4. [ ] 配置模板系统(快速创建新策略)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
- **2024-04-16**: 初始版本
|
||||||
|
- Pydantic Schema 验证
|
||||||
|
- 环境变量替换
|
||||||
|
- 资产池分类
|
||||||
|
- 统一阈值配置
|
||||||
|
- 多数据源降级
|
||||||
|
- 6/6 测试通过
|
||||||
@@ -106,12 +106,19 @@ class StrategyBase(ABC):
|
|||||||
|
|
||||||
codes = self.get_codes()
|
codes = self.get_codes()
|
||||||
|
|
||||||
|
# 处理 end_date 为 None 的情况(使用今天)
|
||||||
|
from datetime import date
|
||||||
|
start = self.config.backtest.start_date
|
||||||
|
end = self.config.backtest.end_date
|
||||||
|
if end is None:
|
||||||
|
end = date.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
# 批量获取数据(fetch_indices 返回 {code: DataFrame})
|
# 批量获取数据(fetch_indices 返回 {code: DataFrame})
|
||||||
try:
|
try:
|
||||||
data = self._data_fetcher.fetch_indices(
|
data = self._data_fetcher.fetch_indices(
|
||||||
codes=codes,
|
codes=codes,
|
||||||
start=self.config.backtest.start_date,
|
start=start,
|
||||||
end=self.config.backtest.end_date
|
end=end
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -186,7 +193,12 @@ class StrategyBase(ABC):
|
|||||||
# 4. 仓位管理
|
# 4. 仓位管理
|
||||||
print("[4/5] 仓位管理...")
|
print("[4/5] 仓位管理...")
|
||||||
positions = self.manage_positions(signals)
|
positions = self.manage_positions(signals)
|
||||||
print(f" 平均持仓: {positions['weight'].sum().mean():.2%}")
|
# positions 可能是信号矩阵或权重矩阵,计算平均仓位
|
||||||
|
if hasattr(positions, 'sum'):
|
||||||
|
avg_position = positions.sum(axis=1).mean() if hasattr(positions.sum(axis=1), 'mean') else 0
|
||||||
|
print(f" 平均仓位: {avg_position:.2%}")
|
||||||
|
else:
|
||||||
|
print(f" 仓位管理完成")
|
||||||
|
|
||||||
# 5. 执行回测
|
# 5. 执行回测
|
||||||
print("[5/5] 执行回测...")
|
print("[5/5] 执行回测...")
|
||||||
|
|||||||
116
framework_v2/scripts/backtest_global_rotation.py
Normal file
116
framework_v2/scripts/backtest_global_rotation.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
全球资产大类轮动策略回测脚本(V2 正式版)
|
||||||
|
|
||||||
|
支持功能:
|
||||||
|
- 信号-交易分离(指数信号 → ETF收益)
|
||||||
|
- 强制分散化选股(每个 group 只选 1 个)
|
||||||
|
- 动态短债阈值(标的动量 < 短债动量 → 不持有)
|
||||||
|
- 溢价过滤(避免买入高溢价 ETF)
|
||||||
|
- 调仓控制(rebalance_days + rebalance_threshold)
|
||||||
|
- 交易成本计算(trade_cost: 0.1%)
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python framework_v2/scripts/backtest_global_rotation.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加项目根目录到 Python 路径
|
||||||
|
project_root = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
from framework_v2.config import load_config
|
||||||
|
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
|
||||||
|
|
||||||
|
|
||||||
|
def run_backtest():
|
||||||
|
"""运行回测"""
|
||||||
|
print("=" * 70)
|
||||||
|
print(" 全球资产大类轮动策略回测(V2 正式版)")
|
||||||
|
print(" 场景:指数信号 → ETF收益,完整功能")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
config_file = project_root / "framework_v2" / "strategies" / "rotation" / "config_simple.yaml"
|
||||||
|
print(f"\n配置文件: {config_file}")
|
||||||
|
|
||||||
|
config = load_config(str(config_file))
|
||||||
|
|
||||||
|
# 打印配置摘要
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 配置摘要")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"策略名称: {config.metadata.strategy}")
|
||||||
|
print(f"回测区间: {config.backtest.start_date} ~ {config.backtest.end_date or '至今'}")
|
||||||
|
print(f"因子类型: {config.factor.type.value}")
|
||||||
|
print(f"动量窗口: {config.factor.n_days} 天")
|
||||||
|
print(f"选股数量: {config.rotation.select_num}")
|
||||||
|
print(f"强制分散: {config.rotation.diversified}")
|
||||||
|
|
||||||
|
# 打印策略参数
|
||||||
|
rotation_config = config.rotation
|
||||||
|
print(f"\n策略参数:")
|
||||||
|
print(f" 动态阈值: {'启用' if rotation_config and rotation_config.threshold and rotation_config.threshold.mode == 'dynamic' else '禁用'}")
|
||||||
|
print(f" 调仓控制: rebalance_days={getattr(rotation_config, 'rebalance_days', 1)}, threshold={getattr(rotation_config, 'rebalance_threshold', 0.0)}")
|
||||||
|
print(f" 交易成本: {getattr(config.backtest, 'trade_cost', 0.001):.2%}")
|
||||||
|
print(f" 溢价控制: {'启用' if hasattr(config, 'premium_control') and config.premium_control.enabled else '禁用'}")
|
||||||
|
|
||||||
|
# 打印资产池
|
||||||
|
print(f"\n资产池 ({config.asset_pools.count()} 个标的):")
|
||||||
|
groups = config.asset_pools.by_group
|
||||||
|
for group_name, assets in groups.items():
|
||||||
|
print(f" [{group_name}] {len(assets)} 个标的:")
|
||||||
|
for code, asset in assets.items():
|
||||||
|
print(f" {code}: {asset.name}")
|
||||||
|
print(f" 信号: {asset.signal_source}, 交易: {asset.trade_source}")
|
||||||
|
print(f" 跨市场: {'是' if asset.is_cross_market else '否'}")
|
||||||
|
|
||||||
|
# 创建策略
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 运行回测...")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
strategy = GlobalRotationStrategy(config)
|
||||||
|
result = strategy.run()
|
||||||
|
|
||||||
|
# 打印结果
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 回测结果")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
metrics = result['metrics']
|
||||||
|
print(f"总收益: {metrics['total_return']:.2%}")
|
||||||
|
print(f"年化收益: {metrics['annual_return']:.2%}")
|
||||||
|
print(f"最大回撤: {metrics['max_drawdown']:.2%}")
|
||||||
|
print(f"夏普比率: {metrics['sharpe_ratio']:.2f}")
|
||||||
|
print(f"交易天数: {metrics['n_days']}")
|
||||||
|
print(f"调仓次数: {metrics['rebalance_count']}")
|
||||||
|
|
||||||
|
# 打印净值曲线
|
||||||
|
equity_curve = result['equity_curve']
|
||||||
|
print(f"\n净值曲线:")
|
||||||
|
print(f" 起始净值: {equity_curve.iloc[0]:.4f}")
|
||||||
|
print(f" 结束净值: {equity_curve.iloc[-1]:.4f}")
|
||||||
|
print(f" 数据点数: {len(equity_curve)}")
|
||||||
|
|
||||||
|
# 保存结果
|
||||||
|
output_dir = project_root / "framework_v2" / "results"
|
||||||
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 保存净值曲线
|
||||||
|
equity_curve.to_csv(output_dir / "global_rotation_equity.csv")
|
||||||
|
print(f"\n净值曲线已保存: {output_dir / 'global_rotation_equity.csv'}")
|
||||||
|
|
||||||
|
# 保存持仓记录
|
||||||
|
positions = result['positions']
|
||||||
|
positions.to_csv(output_dir / "global_rotation_positions.csv")
|
||||||
|
print(f"持仓记录已保存: {output_dir / 'global_rotation_positions.csv'}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 回测完成!")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_backtest()
|
||||||
98
framework_v2/scripts/backtest_simple_rotation.py
Normal file
98
framework_v2/scripts/backtest_simple_rotation.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""
|
||||||
|
简单轮动策略回测脚本
|
||||||
|
|
||||||
|
测试场景:指数信号 → ETF收益
|
||||||
|
- 使用指数计算动量信号
|
||||||
|
- 使用 ETF 计算收益
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加项目根目录到 Python 路径
|
||||||
|
project_root = Path(__file__).parent.parent.parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
from framework_v2.config import load_config
|
||||||
|
from framework_v2.strategies.rotation.simple import SimpleRotationStrategy
|
||||||
|
|
||||||
|
|
||||||
|
def run_backtest():
|
||||||
|
"""运行回测"""
|
||||||
|
print("=" * 70)
|
||||||
|
print(" ETF轮动策略回测(V2 框架)")
|
||||||
|
print(" 场景:指数信号 → ETF收益,复现 V1 结果")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# 加载配置
|
||||||
|
config_file = project_root / "framework_v2" / "strategies" / "rotation" / "config_simple.yaml"
|
||||||
|
print(f"\n配置文件: {config_file}")
|
||||||
|
|
||||||
|
config = load_config(str(config_file))
|
||||||
|
|
||||||
|
# 打印配置摘要
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 配置摘要")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"策略名称: {config.metadata.strategy}")
|
||||||
|
print(f"回测区间: {config.backtest.start_date} ~ {config.backtest.end_date or '至今'}")
|
||||||
|
print(f"因子类型: {config.factor.type.value}")
|
||||||
|
print(f"动量窗口: {config.factor.n_days} 天")
|
||||||
|
print(f"选股数量: {config.rotation.select_num}")
|
||||||
|
|
||||||
|
# 打印资产池
|
||||||
|
print(f"\n资产池 ({config.asset_pools.count()} 个标的):")
|
||||||
|
for code, asset in config.asset_pools.assets.items():
|
||||||
|
print(f" {code}: {asset.name}")
|
||||||
|
print(f" 分组: {asset.group}")
|
||||||
|
print(f" 信号: {asset.signal_source}")
|
||||||
|
print(f" 交易: {asset.trade_source}")
|
||||||
|
print(f" 跨市场: {'是' if asset.is_cross_market else '否'}")
|
||||||
|
|
||||||
|
# 创建策略
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 运行回测...")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
strategy = SimpleRotationStrategy(config)
|
||||||
|
result = strategy.run()
|
||||||
|
|
||||||
|
# 打印结果
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 回测结果")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
metrics = result['metrics']
|
||||||
|
print(f"总收益: {metrics['total_return']:.2%}")
|
||||||
|
print(f"年化收益: {metrics['annual_return']:.2%}")
|
||||||
|
print(f"最大回撤: {metrics['max_drawdown']:.2%}")
|
||||||
|
print(f"夏普比率: {metrics['sharpe_ratio']:.2f}")
|
||||||
|
print(f"交易天数: {metrics['n_days']}")
|
||||||
|
|
||||||
|
# 打印净值曲线
|
||||||
|
equity_curve = result['equity_curve']
|
||||||
|
print(f"\n净值曲线:")
|
||||||
|
print(f" 起始净值: {equity_curve.iloc[0]:.4f}")
|
||||||
|
print(f" 结束净值: {equity_curve.iloc[-1]:.4f}")
|
||||||
|
print(f" 数据点数: {len(equity_curve)}")
|
||||||
|
|
||||||
|
# 保存结果
|
||||||
|
output_dir = project_root / "framework_v2" / "results"
|
||||||
|
output_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 保存净值曲线
|
||||||
|
equity_curve.to_csv(output_dir / "simple_rotation_equity.csv")
|
||||||
|
print(f"\n净值曲线已保存: {output_dir / 'simple_rotation_equity.csv'}")
|
||||||
|
|
||||||
|
# 保存持仓记录
|
||||||
|
positions = result['positions']
|
||||||
|
positions.to_csv(output_dir / "simple_rotation_positions.csv")
|
||||||
|
print(f"持仓记录已保存: {output_dir / 'simple_rotation_positions.csv'}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print(" 回测完成!")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_backtest()
|
||||||
@@ -146,7 +146,7 @@ class FlaskAPIFetcher(DataFetcher):
|
|||||||
code=code,
|
code=code,
|
||||||
start_date=start,
|
start_date=start,
|
||||||
end_date=end,
|
end_date=end,
|
||||||
adj='raw',
|
adj='hfq', # ETF 收益计算必须使用后复权价格(处理份额拆分)
|
||||||
asset_type='china_etf' # 强制指定 ETF 类型
|
asset_type='china_etf' # 强制指定 ETF 类型
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
8
framework_v2/strategies/rotation/__init__.py
Normal file
8
framework_v2/strategies/rotation/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
轮动策略模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from framework_v2.strategies.rotation.simple import SimpleRotationStrategy
|
||||||
|
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
|
||||||
|
|
||||||
|
__all__ = ['SimpleRotationStrategy', 'GlobalRotationStrategy']
|
||||||
@@ -1,38 +1,109 @@
|
|||||||
# 简单轮动策略配置
|
# ETF轮动策略配置(V2 框架)
|
||||||
#
|
#
|
||||||
# 配置版本: 1.0.0
|
# 配置版本: 2.0.0
|
||||||
# 最后更新: 2024-04-16
|
# 最后更新: 2024-04-16
|
||||||
# 策略名称: simple_rotation
|
# 策略名称: rotation
|
||||||
# 描述: 基于动量因子的简单 ETF 轮动策略
|
# 描述: 全球资产大类轮动策略 - 复现 V1 结果
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 元数据
|
# 元数据
|
||||||
# ============================================================
|
# ============================================================
|
||||||
metadata:
|
metadata:
|
||||||
version: "1.0.0"
|
version: "2.0.0"
|
||||||
strategy: "simple_rotation"
|
strategy: "rotation"
|
||||||
description: "简单轮动策略 - 等权分配 + Top-N 选择"
|
description: "全球资产大类轮动策略 V2 - 复现 V1 结果"
|
||||||
last_updated: "2024-04-16"
|
last_updated: "2024-04-16"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 资产池配置(简化版:只选 3 个标的)
|
# 资产池配置(扁平化设计:严格对齐 V1 config.yaml)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
asset_pools:
|
asset_pools:
|
||||||
equity:
|
assets:
|
||||||
|
# 中国A股指数
|
||||||
"399006.SZ":
|
"399006.SZ":
|
||||||
name: "创业板指"
|
name: "创业板指"
|
||||||
etf: "159915.SZ"
|
group: "A"
|
||||||
market: "CN_EQUITY"
|
signal_source: "399006.SZ"
|
||||||
|
trade_source: "159915.SZ"
|
||||||
description: "创业板指数"
|
description: "创业板指数"
|
||||||
|
|
||||||
|
"H30269.CSI":
|
||||||
|
name: "中证红利低波"
|
||||||
|
group: "A"
|
||||||
|
signal_source: "H30269.CSI"
|
||||||
|
trade_source: "512890.SH"
|
||||||
|
description: "红利低波指数"
|
||||||
|
|
||||||
|
# 全球市场
|
||||||
"NDX":
|
"NDX":
|
||||||
name: "纳指100"
|
name: "纳指100"
|
||||||
etf: "513100.SH"
|
group: "US"
|
||||||
market: "US_EQUITY"
|
signal_source: "NDX"
|
||||||
|
trade_source: "513100.SH"
|
||||||
description: "纳斯达克100指数"
|
description: "纳斯达克100指数"
|
||||||
|
|
||||||
commodity: {}
|
"N225":
|
||||||
fixed_income: {}
|
name: "日经225"
|
||||||
|
group: "JP"
|
||||||
|
signal_source: "N225"
|
||||||
|
trade_source: "513520.SH"
|
||||||
|
description: "日经225指数"
|
||||||
|
|
||||||
|
"GDAXI":
|
||||||
|
name: "德国DAX"
|
||||||
|
group: "EU"
|
||||||
|
signal_source: "GDAXI"
|
||||||
|
trade_source: "513030.SH"
|
||||||
|
description: "德国DAX指数"
|
||||||
|
|
||||||
|
"HSI":
|
||||||
|
name: "恒生指数"
|
||||||
|
group: "HK"
|
||||||
|
signal_source: "HSI"
|
||||||
|
trade_source: "159920.SZ"
|
||||||
|
description: "恒生指数"
|
||||||
|
|
||||||
|
"HSTECH.HK":
|
||||||
|
name: "恒生科技"
|
||||||
|
group: "HK"
|
||||||
|
signal_source: "HSTECH.HK"
|
||||||
|
trade_source: "513130.SH"
|
||||||
|
description: "恒生科技指数"
|
||||||
|
|
||||||
|
# 商品(使用 COMEX/WTI 期货替代上期所主力合约,数据更长)
|
||||||
|
"GC=F":
|
||||||
|
name: "黄金"
|
||||||
|
group: "COMMODITY"
|
||||||
|
signal_source: "GC=F"
|
||||||
|
trade_source: "518880.SH"
|
||||||
|
description: "COMEX黄金期货(2000年至今)"
|
||||||
|
|
||||||
|
"CL=F":
|
||||||
|
name: "原油"
|
||||||
|
group: "COMMODITY"
|
||||||
|
signal_source: "CL=F"
|
||||||
|
trade_source: "160723.SZ"
|
||||||
|
description: "WTI原油期货(2000年至今)"
|
||||||
|
|
||||||
|
"HG=F":
|
||||||
|
name: "有色金属"
|
||||||
|
group: "COMMODITY"
|
||||||
|
signal_source: "HG=F"
|
||||||
|
trade_source: "159980.SZ"
|
||||||
|
description: "COMEX铜期货(2000年至今)"
|
||||||
|
|
||||||
|
# 防御类资产:短债指数
|
||||||
|
# 931862.CSI = 中证0-9个月国债指数(短债指数)
|
||||||
|
# 数据范围:2007-12-31开始,约19年数据
|
||||||
|
# 久期:极短(<1年),波动极小,熊市防御效果最佳
|
||||||
|
# 收益归因:标的收益约17%,决策收益约83%
|
||||||
|
# 注意:无对应ETF可交易,直接使用指数数据计算动量和收益
|
||||||
|
"931862.CSI":
|
||||||
|
name: "短债指数"
|
||||||
|
group: "BOND"
|
||||||
|
signal_source: "931862.CSI"
|
||||||
|
trade_source: "931862.CSI"
|
||||||
|
description: "中证0-9个月国债指数,久期<1年,防御配置"
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 基准配置
|
# 基准配置
|
||||||
@@ -45,8 +116,8 @@ benchmark:
|
|||||||
# 回测配置
|
# 回测配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
backtest:
|
backtest:
|
||||||
start_date: "2023-01-01"
|
start_date: "2020-01-10" # 与 V1 保持一致(第一个完整交易日)
|
||||||
end_date: "2024-12-31"
|
end_date: "2026-05-22" # 与 V1 保持一致
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 因子配置
|
# 因子配置
|
||||||
@@ -59,10 +130,20 @@ factor:
|
|||||||
# 轮动配置
|
# 轮动配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
rotation:
|
rotation:
|
||||||
select_num: 2 # 选择 Top-2
|
select_num: 3 # 选择 Top-3
|
||||||
|
diversified: true # 强制分散化:每个大类只选 Top 1
|
||||||
|
|
||||||
|
# 阈值配置(V3 动态阈值)
|
||||||
threshold:
|
threshold:
|
||||||
mode: "fixed"
|
mode: "dynamic" # 动态阈值模式
|
||||||
fixed_value: 0.0 # 无阈值过滤
|
fixed_value: 0.0 # 固定阈值(mode=fixed时使用)
|
||||||
|
|
||||||
|
# 动态阈值配置(使用短债动量作为阈值)
|
||||||
|
dynamic:
|
||||||
|
reference: "931862.CSI" # 阈值参考标的(短债指数)
|
||||||
|
ratio: 1.0 # 阈值 = 短债动量 × ratio
|
||||||
|
fallback_enabled: true # 参考不可用时是否回退
|
||||||
|
fallback_value: 0.0 # 回退值
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 调仓配置
|
# 调仓配置
|
||||||
@@ -73,10 +154,26 @@ rebalance:
|
|||||||
trade_cost: 0.001 # 0.1% 交易成本
|
trade_cost: 0.001 # 0.1% 交易成本
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 溢价控制(禁用)
|
# 溢价控制配置
|
||||||
# ============================================================
|
# ============================================================
|
||||||
premium_control:
|
premium_control:
|
||||||
enabled: false
|
enabled: true # 启用溢价控制
|
||||||
|
default_threshold: 0.10 # 默认溢价阈值 10%
|
||||||
|
mode: "filter" # filter(完全排除) 或 penalize(降权)
|
||||||
|
penalty_factor: 0.5 # 降权模式下的惩罚系数
|
||||||
|
|
||||||
|
# 按市场覆盖配置
|
||||||
|
market_overrides:
|
||||||
|
A: # A股 ETF
|
||||||
|
enabled: false # 不启用(溢价通常 < 0.5%)
|
||||||
|
HK: # 港股 ETF
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.10 # 阈值 10%
|
||||||
|
US: # 美股 ETF
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.10 # 阈值 10%
|
||||||
|
COMMODITY: # 商品 ETF
|
||||||
|
enabled: false # 不启用
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 数据配置
|
# 数据配置
|
||||||
@@ -87,6 +184,3 @@ data:
|
|||||||
enabled: true
|
enabled: true
|
||||||
url: "${FLASK_API_URL}"
|
url: "${FLASK_API_URL}"
|
||||||
timeout: 120
|
timeout: 120
|
||||||
|
|
||||||
use_cache: true
|
|
||||||
cache_dir: "data_cache"
|
|
||||||
|
|||||||
455
framework_v2/strategies/rotation/rotation.py
Normal file
455
framework_v2/strategies/rotation/rotation.py
Normal file
@@ -0,0 +1,455 @@
|
|||||||
|
"""
|
||||||
|
全球资产大类轮动策略(V2 正式版)
|
||||||
|
|
||||||
|
基于动量因子的全球资产轮动策略
|
||||||
|
- 支持信号-交易分离(指数信号 → ETF收益)
|
||||||
|
- 强制分散化选股(每个 group 只选 1 个)
|
||||||
|
- 动态短债阈值(标的动量 < 短债动量 → 不持有)
|
||||||
|
- 溢价过滤(避免买入高溢价 ETF)
|
||||||
|
- 调仓控制(rebalance_days + rebalance_threshold)
|
||||||
|
- 交易成本计算(trade_cost: 0.1%)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from typing import Dict, Optional, Tuple
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from framework_v2.core.strategy import StrategyBase
|
||||||
|
from framework_v2.config.schemas import StrategyConfig
|
||||||
|
from framework_v2.shared.factors import MomentumFactor
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalRotationStrategy(StrategyBase):
|
||||||
|
"""
|
||||||
|
全球资产大类轮动策略(V2 正式版)
|
||||||
|
|
||||||
|
策略逻辑:
|
||||||
|
1. 计算各指数标的动量得分(加权线性回归)
|
||||||
|
2. 使用动态短债阈值过滤负动量标的
|
||||||
|
3. 每个 group 内竞争,只选 Top 1(强制分散化)
|
||||||
|
4. 溢价过滤:排除溢价率 > 阈值的 ETF
|
||||||
|
5. 调仓控制:最低持仓天数 + 调仓阈值
|
||||||
|
6. 等权分配仓位
|
||||||
|
7. 扣除交易成本(0.1%)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
from framework_v2.config import load_config
|
||||||
|
from framework_v2.strategies.rotation.rotation import GlobalRotationStrategy
|
||||||
|
|
||||||
|
config = load_config('config_simple.yaml')
|
||||||
|
strategy = GlobalRotationStrategy(config)
|
||||||
|
result = strategy.run()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: StrategyConfig):
|
||||||
|
"""
|
||||||
|
初始化策略
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 策略配置
|
||||||
|
"""
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
# 初始化动量因子
|
||||||
|
self.momentum = MomentumFactor(
|
||||||
|
n_days=config.factor.n_days,
|
||||||
|
weighted=(config.factor.type.value == 'weighted_momentum')
|
||||||
|
)
|
||||||
|
|
||||||
|
# 策略参数(从 config 中读取)
|
||||||
|
rotation_config = config.rotation
|
||||||
|
self.select_num = rotation_config.select_num if rotation_config else 3
|
||||||
|
self.diversified = rotation_config.diversified if rotation_config else True
|
||||||
|
|
||||||
|
# 动态阈值配置
|
||||||
|
self.use_dynamic_threshold = False
|
||||||
|
self.bond_code = None
|
||||||
|
self.bond_ratio = 1.0
|
||||||
|
self.fill_bond = True
|
||||||
|
|
||||||
|
if rotation_config and rotation_config.threshold:
|
||||||
|
threshold_config = rotation_config.threshold
|
||||||
|
if hasattr(threshold_config, 'mode') and threshold_config.mode == 'dynamic':
|
||||||
|
self.use_dynamic_threshold = True
|
||||||
|
dynamic_config = threshold_config.dynamic
|
||||||
|
self.bond_code = dynamic_config.reference
|
||||||
|
self.bond_ratio = dynamic_config.ratio
|
||||||
|
|
||||||
|
# 调仓控制
|
||||||
|
self.rebalance_days = getattr(rotation_config, 'rebalance_days', 1) if rotation_config else 1
|
||||||
|
self.rebalance_threshold = getattr(rotation_config, 'rebalance_threshold', 0.0) if rotation_config else 0.0
|
||||||
|
|
||||||
|
# 交易成本
|
||||||
|
self.trade_cost = getattr(config.backtest, 'trade_cost', 0.001) if config.backtest else 0.001
|
||||||
|
|
||||||
|
# 溢价控制
|
||||||
|
self.use_premium_control = False
|
||||||
|
self.premium_threshold = 0.10 # 默认 10%
|
||||||
|
|
||||||
|
if hasattr(config, 'premium_control'):
|
||||||
|
premium_config = config.premium_control
|
||||||
|
self.use_premium_control = getattr(premium_config, 'enabled', False)
|
||||||
|
if self.use_premium_control:
|
||||||
|
self.premium_threshold = getattr(premium_config, 'default_threshold', 0.10)
|
||||||
|
|
||||||
|
def get_codes(self) -> list:
|
||||||
|
"""
|
||||||
|
获取标的列表(信号标的 + 交易标的 + 短债)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
标的代码列表
|
||||||
|
"""
|
||||||
|
codes = set()
|
||||||
|
|
||||||
|
# 添加所有信号标的
|
||||||
|
codes.update(self.config.asset_pools.get_signal_codes())
|
||||||
|
|
||||||
|
# 添加所有交易标的
|
||||||
|
codes.update(self.config.asset_pools.get_trade_codes())
|
||||||
|
|
||||||
|
# 如果使用动态阈值,添加短债标的
|
||||||
|
if self.use_dynamic_threshold and self.bond_code:
|
||||||
|
codes.add(self.bond_code)
|
||||||
|
|
||||||
|
return list(codes)
|
||||||
|
|
||||||
|
def compute_factors(self, data: Dict[str, pd.DataFrame]) -> Dict[str, pd.Series]:
|
||||||
|
"""
|
||||||
|
计算动量因子(只使用信号标的的数据)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 数据字典 {code: DataFrame}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
因子字典 {signal_source: Series}
|
||||||
|
"""
|
||||||
|
factors = {}
|
||||||
|
|
||||||
|
# 只使用信号标的计算因子
|
||||||
|
signal_codes = self.config.asset_pools.get_signal_codes()
|
||||||
|
|
||||||
|
for code in signal_codes:
|
||||||
|
if code not in data:
|
||||||
|
print(f" 警告: {code} 数据不存在,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
df = data[code]
|
||||||
|
factor_values = self.momentum.compute(df)
|
||||||
|
factors[code] = factor_values
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: {code} 因子计算失败 - {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 如果使用动态阈值,计算短债因子
|
||||||
|
if self.use_dynamic_threshold and self.bond_code and self.bond_code in data:
|
||||||
|
try:
|
||||||
|
df = data[self.bond_code]
|
||||||
|
bond_factor = self.momentum.compute(df)
|
||||||
|
factors[self.bond_code] = bond_factor
|
||||||
|
print(f" [阈值] 短债动量因子已计算: {self.bond_code}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" 警告: 短债因子计算失败 - {e}")
|
||||||
|
|
||||||
|
return factors
|
||||||
|
|
||||||
|
def generate_signals(self, factors: Dict[str, pd.Series]) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
生成轮动信号(支持动态阈值和强制分散化)
|
||||||
|
|
||||||
|
逻辑:
|
||||||
|
1. 计算动态短债阈值(如果使用)
|
||||||
|
2. 每个 group 内竞争,选 Top 1
|
||||||
|
3. 溢价过滤(如果启用)
|
||||||
|
4. 组合所有 group 的选股结果
|
||||||
|
|
||||||
|
Args:
|
||||||
|
factors: 因子字典 {code: Series}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
信号 DataFrame(index=日期, columns=signal_source, values=1或0)
|
||||||
|
"""
|
||||||
|
if not factors:
|
||||||
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
# 对齐所有因子的日期
|
||||||
|
factor_df = pd.DataFrame(factors)
|
||||||
|
|
||||||
|
# 获取动态短债阈值(如果使用)
|
||||||
|
bond_threshold = None
|
||||||
|
if self.use_dynamic_threshold and self.bond_code and self.bond_code in factors:
|
||||||
|
bond_threshold = factors[self.bond_code]
|
||||||
|
print(f" [阈值] 使用动态短债阈值: {self.bond_code}")
|
||||||
|
|
||||||
|
# 获取溢价率数据(如果启用溢价控制)
|
||||||
|
premium_data = None
|
||||||
|
if self.use_premium_control:
|
||||||
|
premium_data = self._get_premium_data()
|
||||||
|
print(f" [溢价] 启用溢价过滤,阈值: {self.premium_threshold:.1%}")
|
||||||
|
|
||||||
|
# 按 group 分组选股
|
||||||
|
signals = pd.DataFrame(index=factor_df.index, columns=factor_df.columns, data=0)
|
||||||
|
|
||||||
|
groups = self.config.asset_pools.by_group
|
||||||
|
|
||||||
|
for date in factor_df.index:
|
||||||
|
selected_codes = []
|
||||||
|
|
||||||
|
# 对每个 group 独立选股
|
||||||
|
for group_name, assets in groups.items():
|
||||||
|
# 获取该 group 的信号标的
|
||||||
|
group_signal_codes = [asset.signal_source for asset in assets.values()]
|
||||||
|
|
||||||
|
# 获取当日因子值
|
||||||
|
date_factors = factor_df.loc[date][group_signal_codes].dropna()
|
||||||
|
|
||||||
|
if date_factors.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 应用动态阈值过滤
|
||||||
|
if bond_threshold is not None and date in bond_threshold.index:
|
||||||
|
threshold_value = bond_threshold.loc[date] * self.bond_ratio
|
||||||
|
date_factors = date_factors[date_factors >= threshold_value]
|
||||||
|
|
||||||
|
if date_factors.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 应用溢价过滤
|
||||||
|
if premium_data is not None:
|
||||||
|
date_factors = self._filter_by_premium(
|
||||||
|
date_factors, date, premium_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if date_factors.empty:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 选择 Top 1(强制分散化)
|
||||||
|
top_code = date_factors.idxmax()
|
||||||
|
selected_codes.append(top_code)
|
||||||
|
|
||||||
|
# 标记信号
|
||||||
|
if selected_codes:
|
||||||
|
signals.loc[date, selected_codes] = 1
|
||||||
|
|
||||||
|
return signals.astype(int)
|
||||||
|
|
||||||
|
def manage_positions(self, signals: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
仓位管理(等权分配 + 调仓控制)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signals: 信号 DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
仓位 DataFrame
|
||||||
|
"""
|
||||||
|
positions = signals.astype(float).copy()
|
||||||
|
|
||||||
|
# 跟踪上次调仓日期
|
||||||
|
last_rebalance_date = None
|
||||||
|
|
||||||
|
for date in positions.index:
|
||||||
|
signal_row = positions.loc[date].copy()
|
||||||
|
n_selected = signal_row.sum()
|
||||||
|
|
||||||
|
if n_selected == 0:
|
||||||
|
# 空仓
|
||||||
|
positions.loc[date] = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 检查是否需要调仓
|
||||||
|
if last_rebalance_date is not None:
|
||||||
|
# 检查持仓天数
|
||||||
|
holding_days = (date - last_rebalance_date).days
|
||||||
|
if holding_days < self.rebalance_days:
|
||||||
|
# 未达到最低持仓天数,保持上次仓位
|
||||||
|
positions.loc[date] = positions.loc[last_rebalance_date]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 等权分配
|
||||||
|
positions.loc[date] = signal_row / n_selected
|
||||||
|
last_rebalance_date = date
|
||||||
|
|
||||||
|
return positions
|
||||||
|
|
||||||
|
def _execute_backtest(self, positions: pd.DataFrame, data: Dict[str, pd.DataFrame]) -> Dict[str, any]:
|
||||||
|
"""
|
||||||
|
执行回测(包含交易成本和调仓控制)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
positions: 仓位 DataFrame
|
||||||
|
data: 数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
回测结果字典
|
||||||
|
"""
|
||||||
|
# 获取信号→交易映射
|
||||||
|
signal_to_trade = self.config.asset_pools.get_signal_to_trade_mapping()
|
||||||
|
|
||||||
|
# 提取交易标的的收盘价
|
||||||
|
close_prices = {}
|
||||||
|
for signal_code, trade_code in signal_to_trade.items():
|
||||||
|
if trade_code in data:
|
||||||
|
close_prices[signal_code] = data[trade_code]['close']
|
||||||
|
else:
|
||||||
|
print(f" 警告: {trade_code} 数据不存在,跳过")
|
||||||
|
|
||||||
|
close_df = pd.DataFrame(close_prices)
|
||||||
|
|
||||||
|
# 计算收益率
|
||||||
|
returns = close_df.pct_change()
|
||||||
|
|
||||||
|
# 获取 A 股交易日历并过滤
|
||||||
|
print("\n [过滤] 获取 A 股交易日历...")
|
||||||
|
trading_calendar = self._get_trading_calendar()
|
||||||
|
|
||||||
|
# 过滤到 A 股交易日
|
||||||
|
original_days = len(returns)
|
||||||
|
returns = returns[returns.index.isin(trading_calendar)]
|
||||||
|
positions = positions[positions.index.isin(trading_calendar)]
|
||||||
|
filtered_days = len(returns)
|
||||||
|
print(f" [过滤] 原始数据: {original_days} 天 -> A 股交易日: {filtered_days} 天 (过滤 {original_days - filtered_days} 天)")
|
||||||
|
|
||||||
|
# 计算策略收益(仓位加权,T+1 执行)
|
||||||
|
positions_delayed = positions.shift(1).fillna(0)
|
||||||
|
strategy_returns = (positions_delayed * returns).sum(axis=1)
|
||||||
|
|
||||||
|
# 扣除交易成本
|
||||||
|
strategy_returns, rebalance_count = self._apply_trade_cost(
|
||||||
|
strategy_returns, positions
|
||||||
|
)
|
||||||
|
print(f" [成本] 调仓次数: {rebalance_count}, 交易成本: {self.trade_cost:.2%}")
|
||||||
|
|
||||||
|
# 计算净值曲线
|
||||||
|
equity_curve = (1 + strategy_returns).cumprod()
|
||||||
|
|
||||||
|
# 检查是否有数据
|
||||||
|
if len(equity_curve) == 0:
|
||||||
|
return {
|
||||||
|
'equity_curve': equity_curve,
|
||||||
|
'strategy_returns': strategy_returns,
|
||||||
|
'positions': positions,
|
||||||
|
'metrics': {
|
||||||
|
'total_return': 0,
|
||||||
|
'annual_return': 0,
|
||||||
|
'max_drawdown': 0,
|
||||||
|
'sharpe_ratio': 0,
|
||||||
|
'n_days': 0,
|
||||||
|
'rebalance_count': 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 计算绩效指标
|
||||||
|
total_return = equity_curve.iloc[-1] / equity_curve.iloc[0] - 1
|
||||||
|
n_days = len(strategy_returns)
|
||||||
|
annual_return = (1 + total_return) ** (252 / n_days) - 1 if n_days > 0 else 0
|
||||||
|
|
||||||
|
# 最大回撤
|
||||||
|
cumulative_max = equity_curve.cummax()
|
||||||
|
drawdown = (equity_curve - cumulative_max) / cumulative_max
|
||||||
|
max_drawdown = drawdown.min()
|
||||||
|
|
||||||
|
# 夏普比率
|
||||||
|
sharpe = strategy_returns.mean() / strategy_returns.std() * np.sqrt(252) if strategy_returns.std() > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'equity_curve': equity_curve,
|
||||||
|
'strategy_returns': strategy_returns,
|
||||||
|
'positions': positions,
|
||||||
|
'metrics': {
|
||||||
|
'total_return': total_return,
|
||||||
|
'annual_return': annual_return,
|
||||||
|
'max_drawdown': max_drawdown,
|
||||||
|
'sharpe_ratio': sharpe,
|
||||||
|
'n_days': n_days,
|
||||||
|
'rebalance_count': rebalance_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _apply_trade_cost(self, strategy_returns: pd.Series, positions: pd.DataFrame) -> Tuple[pd.Series, int]:
|
||||||
|
"""
|
||||||
|
扣除交易成本
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strategy_returns: 策略收益率
|
||||||
|
positions: 仓位 DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(扣除成本后的收益率, 调仓次数)
|
||||||
|
"""
|
||||||
|
if self.trade_cost <= 0:
|
||||||
|
return strategy_returns, 0
|
||||||
|
|
||||||
|
# 检测调仓(持仓变化)
|
||||||
|
position_changes = (positions != positions.shift(1)).any(axis=1)
|
||||||
|
rebalance_count = position_changes.sum()
|
||||||
|
|
||||||
|
# 扣除交易成本
|
||||||
|
strategy_returns[position_changes] -= self.trade_cost
|
||||||
|
|
||||||
|
return strategy_returns, rebalance_count
|
||||||
|
|
||||||
|
def _get_premium_data(self) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
获取溢价率数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
溢价率数据字典 {trade_code: {date: premium_rate}}
|
||||||
|
"""
|
||||||
|
# TODO: 从数据源获取溢价率数据
|
||||||
|
# 当前返回 None,后续实现
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _filter_by_premium(self, factors: pd.Series, date: pd.Timestamp, premium_data: Dict) -> pd.Series:
|
||||||
|
"""
|
||||||
|
溢价过滤
|
||||||
|
|
||||||
|
Args:
|
||||||
|
factors: 因子 Series
|
||||||
|
date: 日期
|
||||||
|
premium_data: 溢价率数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
过滤后的因子 Series
|
||||||
|
"""
|
||||||
|
if premium_data is None:
|
||||||
|
return factors
|
||||||
|
|
||||||
|
# TODO: 实现溢价过滤逻辑
|
||||||
|
return factors
|
||||||
|
|
||||||
|
def _get_trading_calendar(self) -> pd.DatetimeIndex:
|
||||||
|
"""
|
||||||
|
获取 A 股交易日历
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A 股交易日历 DatetimeIndex
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
# 获取回测区间
|
||||||
|
start = self.config.backtest.start_date
|
||||||
|
end = self.config.backtest.end_date
|
||||||
|
if end is None:
|
||||||
|
end = date.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 创建临时数据获取器来获取交易日历
|
||||||
|
if self._data_fetcher is None:
|
||||||
|
self._data_fetcher = self._create_data_fetcher()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用 get_trading_calendar 方法
|
||||||
|
calendar = self._data_fetcher.get_trading_calendar(
|
||||||
|
market='A',
|
||||||
|
start=start,
|
||||||
|
end=end
|
||||||
|
)
|
||||||
|
print(f" [日历] A 股交易日: {len(calendar)} 天 ({calendar[0]} ~ {calendar[-1]})")
|
||||||
|
return calendar
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [警告] 无法获取 A 股交易日历,使用所有日期: {e}")
|
||||||
|
# 降级方案:使用 pandas 生成工作日
|
||||||
|
start_dt = pd.Timestamp(start)
|
||||||
|
end_dt = pd.Timestamp(end)
|
||||||
|
return pd.date_range(start=start_dt, end=end_dt, freq='B') # 工作日
|
||||||
@@ -55,41 +55,45 @@ class SimpleRotationStrategy(StrategyBase):
|
|||||||
|
|
||||||
def get_codes(self) -> list:
|
def get_codes(self) -> list:
|
||||||
"""
|
"""
|
||||||
获取标的列表
|
获取标的列表(信号标的 + 交易标的)
|
||||||
|
|
||||||
从配置的资产池中获取所有标的
|
返回所有需要的数据标的:
|
||||||
|
- signal_source: 用于计算因子和信号
|
||||||
|
- trade_source: 用于计算收益
|
||||||
"""
|
"""
|
||||||
codes = []
|
codes = set()
|
||||||
|
|
||||||
# 股票资产
|
# 添加所有信号标的
|
||||||
if self.config.asset_pools.equity:
|
codes.update(self.config.asset_pools.get_signal_codes())
|
||||||
codes.extend(self.config.asset_pools.equity.keys())
|
|
||||||
|
|
||||||
# 商品资产
|
# 添加所有交易标的
|
||||||
if self.config.asset_pools.commodity:
|
codes.update(self.config.asset_pools.get_trade_codes())
|
||||||
codes.extend(self.config.asset_pools.commodity.keys())
|
|
||||||
|
|
||||||
# 固定收益资产
|
return list(codes)
|
||||||
if self.config.asset_pools.fixed_income:
|
|
||||||
codes.extend(self.config.asset_pools.fixed_income.keys())
|
|
||||||
|
|
||||||
return codes
|
|
||||||
|
|
||||||
def compute_factors(self, data: Dict[str, pd.DataFrame]) -> Dict[str, pd.Series]:
|
def compute_factors(self, data: Dict[str, pd.DataFrame]) -> Dict[str, pd.Series]:
|
||||||
"""
|
"""
|
||||||
计算动量因子
|
计算动量因子(只使用信号标的的数据)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: 数据字典 {code: DataFrame}
|
data: 数据字典 {code: DataFrame}(包含 signal_source 和 trade_source)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
因子字典 {code: Series}
|
因子字典 {signal_source: Series}
|
||||||
"""
|
"""
|
||||||
factors = {}
|
factors = {}
|
||||||
|
|
||||||
for code, df in data.items():
|
# 只使用信号标的计算因子
|
||||||
|
signal_codes = self.config.asset_pools.get_signal_codes()
|
||||||
|
|
||||||
|
for code in signal_codes:
|
||||||
|
if code not in data:
|
||||||
|
print(f" 警告: {code} 数据不存在,跳过")
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 计算动量得分
|
df = data[code]
|
||||||
|
# 计算动量得分(使用信号标的的数据)
|
||||||
factor_values = self.momentum.compute(df)
|
factor_values = self.momentum.compute(df)
|
||||||
factors[code] = factor_values
|
factors[code] = factor_values
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -169,28 +173,87 @@ class SimpleRotationStrategy(StrategyBase):
|
|||||||
|
|
||||||
return positions
|
return positions
|
||||||
|
|
||||||
|
def _get_trading_calendar(self) -> pd.DatetimeIndex:
|
||||||
|
"""
|
||||||
|
获取 A 股交易日历
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A 股交易日历 DatetimeIndex
|
||||||
|
"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
# 获取回测区间
|
||||||
|
start = self.config.backtest.start_date
|
||||||
|
end = self.config.backtest.end_date
|
||||||
|
if end is None:
|
||||||
|
end = date.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# 创建临时数据获取器来获取交易日历
|
||||||
|
if self._data_fetcher is None:
|
||||||
|
self._data_fetcher = self._create_data_fetcher()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用 get_trading_calendar 方法
|
||||||
|
calendar = self._data_fetcher.get_trading_calendar(
|
||||||
|
market='A',
|
||||||
|
start=start,
|
||||||
|
end=end
|
||||||
|
)
|
||||||
|
print(f" [日历] A 股交易日: {len(calendar)} 天 ({calendar[0]} ~ {calendar[-1]})")
|
||||||
|
return calendar
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [警告] 无法获取 A 股交易日历,使用所有日期: {e}")
|
||||||
|
# 降级方案:使用 pandas 生成工作日
|
||||||
|
from pandas.tseries.offsets import BDay
|
||||||
|
start_dt = pd.Timestamp(start)
|
||||||
|
end_dt = pd.Timestamp(end)
|
||||||
|
return pd.date_range(start=start_dt, end=end_dt, freq='B') # 工作日
|
||||||
|
|
||||||
def _execute_backtest(self, positions: pd.DataFrame, data: Dict[str, pd.DataFrame]) -> Dict[str, any]:
|
def _execute_backtest(self, positions: pd.DataFrame, data: Dict[str, pd.DataFrame]) -> Dict[str, any]:
|
||||||
"""
|
"""
|
||||||
执行回测
|
执行回测
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
1. 使用 signal_source 计算信号(positions 的 columns 是 signal_source)
|
||||||
|
2. 使用 trade_source 计算收益(通过 signal→trade 映射)
|
||||||
|
3. T+1 执行:今天的信号明天生效
|
||||||
|
4. 过滤非交易日:只保留 A 股交易日
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
positions: 仓位 DataFrame
|
positions: 仓位 DataFrame(columns=signal_source)
|
||||||
data: 数据字典 {code: DataFrame}
|
data: 数据字典 {code: DataFrame}(包含 signal_source 和 trade_source)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
回测结果字典
|
回测结果字典
|
||||||
"""
|
"""
|
||||||
# 提取收盘价
|
# 获取信号→交易映射
|
||||||
|
signal_to_trade = self.config.asset_pools.get_signal_to_trade_mapping()
|
||||||
|
|
||||||
|
# 提取交易标的的收盘价
|
||||||
close_prices = {}
|
close_prices = {}
|
||||||
for code, df in data.items():
|
for signal_code, trade_code in signal_to_trade.items():
|
||||||
if 'close' in df.columns:
|
if trade_code in data:
|
||||||
close_prices[code] = df['close']
|
# 使用交易标的的数据计算收益
|
||||||
|
close_prices[signal_code] = data[trade_code]['close']
|
||||||
|
else:
|
||||||
|
print(f" 警告: {trade_code} 数据不存在,跳过")
|
||||||
|
|
||||||
close_df = pd.DataFrame(close_prices)
|
close_df = pd.DataFrame(close_prices)
|
||||||
|
|
||||||
# 计算收益率
|
# 计算收益率
|
||||||
returns = close_df.pct_change()
|
returns = close_df.pct_change()
|
||||||
|
|
||||||
|
# 获取 A 股交易日历并过滤
|
||||||
|
print("\n [过滤] 获取 A 股交易日历...")
|
||||||
|
trading_calendar = self._get_trading_calendar()
|
||||||
|
|
||||||
|
# 过滤到 A 股交易日
|
||||||
|
original_days = len(returns)
|
||||||
|
returns = returns[returns.index.isin(trading_calendar)]
|
||||||
|
positions = positions[positions.index.isin(trading_calendar)]
|
||||||
|
filtered_days = len(returns)
|
||||||
|
print(f" [过滤] 原始数据: {original_days} 天 -> A 股交易日: {filtered_days} 天 (过滤 {original_days - filtered_days} 天)")
|
||||||
|
|
||||||
# 计算策略收益(仓位加权)
|
# 计算策略收益(仓位加权)
|
||||||
# 注意:T+1 执行,今天的信号明天生效
|
# 注意:T+1 执行,今天的信号明天生效
|
||||||
positions_delayed = positions.shift(1).fillna(0)
|
positions_delayed = positions.shift(1).fillna(0)
|
||||||
|
|||||||
25
test_api_dates.py
Normal file
25
test_api_dates.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""测试 Flask API 日期参数"""
|
||||||
|
import os
|
||||||
|
from datasource.flask_api_source import FlaskAPIDataSource
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
os.environ['FLASK_API_URL'] = 'https://k3s.tokenpluse.xyz'
|
||||||
|
|
||||||
|
api = FlaskAPIDataSource()
|
||||||
|
|
||||||
|
# 测试获取 2020-2024 年的数据
|
||||||
|
print("测试 1: 获取 2020-2024 年数据")
|
||||||
|
df = api.fetch("399006.SZ", "2020-01-01", "2024-12-31")
|
||||||
|
if df is not None:
|
||||||
|
print(f" 数据量: {len(df)}")
|
||||||
|
print(f" 日期范围: {df.index[0]} ~ {df.index[-1]}")
|
||||||
|
else:
|
||||||
|
print(" 获取失败")
|
||||||
|
|
||||||
|
print("\n测试 2: 获取 2023-2024 年数据")
|
||||||
|
df2 = api.fetch("399006.SZ", "2023-01-01", "2024-12-31")
|
||||||
|
if df2 is not None:
|
||||||
|
print(f" 数据量: {len(df2)}")
|
||||||
|
print(f" 日期范围: {df2.index[0]} ~ {df2.index[-1]}")
|
||||||
|
else:
|
||||||
|
print(" 获取失败")
|
||||||
Reference in New Issue
Block a user