feat(framework_v2): 对齐 V1 配置,实现指数信号→ETF收益回测
配置对齐:
- config_simple.yaml 严格对齐 V1 config.yaml
* 11 个标的覆盖 7 个策略分组
* 回测区间: 2020-01-01 ~ 至今
* 选股数量: Top-3,强制分散化
* V3 动态阈值(短债动量参考)
* 溢价控制启用(HK/US 10%阈值)
策略实现:
- SimpleRotationStrategy 支持 signal_source/trade_source 分离
* get_codes() 同时获取信号和交易标的
* compute_factors() 只使用 signal_source 计算因子
* _execute_backtest() 使用 trade_source 计算收益
* 支持跨市场场景(指数信号 → ETF收益)
回测验证:
- 成功运行端到端回测
- 获取 21 个标的(11 signal + 10 trade)
- 平均仓位 84.42%
- ⚠️ 已知问题: Flask API 只返回缓存数据(2026年),需修复
修复项:
- StrategyBase.run() 兼容信号矩阵(移除 'weight' 列假设)
This commit is contained in:
@@ -186,7 +186,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] 执行回测...")
|
||||||
|
|||||||
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()
|
||||||
@@ -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: "CN_GROWTH"
|
||||||
market: "CN_EQUITY"
|
signal_source: "399006.SZ"
|
||||||
|
trade_source: "159915.SZ"
|
||||||
description: "创业板指数"
|
description: "创业板指数"
|
||||||
|
|
||||||
|
"H30269.CSI":
|
||||||
|
name: "中证红利低波"
|
||||||
|
group: "CN_VALUE"
|
||||||
|
signal_source: "H30269.CSI"
|
||||||
|
trade_source: "512890.SH"
|
||||||
|
description: "红利低波指数"
|
||||||
|
|
||||||
|
# 全球市场
|
||||||
"NDX":
|
"NDX":
|
||||||
name: "纳指100"
|
name: "纳指100"
|
||||||
etf: "513100.SH"
|
group: "US_TECH"
|
||||||
market: "US_EQUITY"
|
signal_source: "NDX"
|
||||||
|
trade_source: "513100.SH"
|
||||||
description: "纳斯达克100指数"
|
description: "纳斯达克100指数"
|
||||||
|
|
||||||
commodity: {}
|
"N225":
|
||||||
fixed_income: {}
|
name: "日经225"
|
||||||
|
group: "JP_BROAD"
|
||||||
|
signal_source: "N225"
|
||||||
|
trade_source: "513520.SH"
|
||||||
|
description: "日经225指数"
|
||||||
|
|
||||||
|
"GDAXI":
|
||||||
|
name: "德国DAX"
|
||||||
|
group: "EU_BROAD"
|
||||||
|
signal_source: "GDAXI"
|
||||||
|
trade_source: "513030.SH"
|
||||||
|
description: "德国DAX指数"
|
||||||
|
|
||||||
|
"HSI":
|
||||||
|
name: "恒生指数"
|
||||||
|
group: "HK_BROAD"
|
||||||
|
signal_source: "HSI"
|
||||||
|
trade_source: "159920.SZ"
|
||||||
|
description: "恒生指数"
|
||||||
|
|
||||||
|
"HSTECH.HK":
|
||||||
|
name: "恒生科技"
|
||||||
|
group: "HK_TECH"
|
||||||
|
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: "FIXED_INCOME"
|
||||||
|
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-01"
|
||||||
end_date: "2024-12-31"
|
# end_date: null # null 表示至今
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 因子配置
|
# 因子配置
|
||||||
@@ -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:
|
||||||
|
CN_EQUITY: # A股 ETF
|
||||||
|
enabled: false # 不启用(溢价通常 < 0.5%)
|
||||||
|
HK_EQUITY: # 港股 ETF
|
||||||
|
enabled: true
|
||||||
|
threshold: 0.10 # 阈值 10%
|
||||||
|
US_EQUITY: # 美股 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"
|
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -173,18 +177,29 @@ class SimpleRotationStrategy(StrategyBase):
|
|||||||
"""
|
"""
|
||||||
执行回测
|
执行回测
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
1. 使用 signal_source 计算信号(positions 的 columns 是 signal_source)
|
||||||
|
2. 使用 trade_source 计算收益(通过 signal→trade 映射)
|
||||||
|
3. T+1 执行:今天的信号明天生效
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user