diff --git a/framework_v2/core/strategy.py b/framework_v2/core/strategy.py index ab62b9f..5851eac 100644 --- a/framework_v2/core/strategy.py +++ b/framework_v2/core/strategy.py @@ -186,7 +186,12 @@ class StrategyBase(ABC): # 4. 仓位管理 print("[4/5] 仓位管理...") 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. 执行回测 print("[5/5] 执行回测...") diff --git a/framework_v2/scripts/backtest_simple_rotation.py b/framework_v2/scripts/backtest_simple_rotation.py new file mode 100644 index 0000000..3bc708a --- /dev/null +++ b/framework_v2/scripts/backtest_simple_rotation.py @@ -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() diff --git a/framework_v2/strategies/rotation/config_simple.yaml b/framework_v2/strategies/rotation/config_simple.yaml index 17bdb58..25babf6 100644 --- a/framework_v2/strategies/rotation/config_simple.yaml +++ b/framework_v2/strategies/rotation/config_simple.yaml @@ -1,38 +1,109 @@ -# 简单轮动策略配置 +# ETF轮动策略配置(V2 框架) # -# 配置版本: 1.0.0 +# 配置版本: 2.0.0 # 最后更新: 2024-04-16 -# 策略名称: simple_rotation -# 描述: 基于动量因子的简单 ETF 轮动策略 +# 策略名称: rotation +# 描述: 全球资产大类轮动策略 - 复现 V1 结果 # ============================================================ # 元数据 # ============================================================ metadata: - version: "1.0.0" - strategy: "simple_rotation" - description: "简单轮动策略 - 等权分配 + Top-N 选择" + version: "2.0.0" + strategy: "rotation" + description: "全球资产大类轮动策略 V2 - 复现 V1 结果" last_updated: "2024-04-16" # ============================================================ -# 资产池配置(简化版:只选 3 个标的) +# 资产池配置(扁平化设计:严格对齐 V1 config.yaml) # ============================================================ asset_pools: - equity: + assets: + # 中国A股指数 "399006.SZ": name: "创业板指" - etf: "159915.SZ" - market: "CN_EQUITY" + group: "CN_GROWTH" + signal_source: "399006.SZ" + trade_source: "159915.SZ" description: "创业板指数" + "H30269.CSI": + name: "中证红利低波" + group: "CN_VALUE" + signal_source: "H30269.CSI" + trade_source: "512890.SH" + description: "红利低波指数" + + # 全球市场 "NDX": name: "纳指100" - etf: "513100.SH" - market: "US_EQUITY" + group: "US_TECH" + signal_source: "NDX" + trade_source: "513100.SH" description: "纳斯达克100指数" - - commodity: {} - fixed_income: {} + + "N225": + 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: - start_date: "2023-01-01" - end_date: "2024-12-31" + start_date: "2020-01-01" + # end_date: null # null 表示至今 # ============================================================ # 因子配置 @@ -59,10 +130,20 @@ factor: # 轮动配置 # ============================================================ rotation: - select_num: 2 # 选择 Top-2 + select_num: 3 # 选择 Top-3 + diversified: true # 强制分散化:每个大类只选 Top 1 + + # 阈值配置(V3 动态阈值) threshold: - mode: "fixed" - fixed_value: 0.0 # 无阈值过滤 + mode: "dynamic" # 动态阈值模式 + 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% 交易成本 # ============================================================ -# 溢价控制(禁用) +# 溢价控制配置 # ============================================================ 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 url: "${FLASK_API_URL}" timeout: 120 - - use_cache: true - cache_dir: "data_cache" diff --git a/framework_v2/strategies/rotation/simple.py b/framework_v2/strategies/rotation/simple.py index f142b4f..8bd2e56 100644 --- a/framework_v2/strategies/rotation/simple.py +++ b/framework_v2/strategies/rotation/simple.py @@ -55,41 +55,45 @@ class SimpleRotationStrategy(StrategyBase): def get_codes(self) -> list: """ - 获取标的列表 + 获取标的列表(信号标的 + 交易标的) - 从配置的资产池中获取所有标的 + 返回所有需要的数据标的: + - signal_source: 用于计算因子和信号 + - trade_source: 用于计算收益 """ - codes = [] + codes = set() - # 股票资产 - if self.config.asset_pools.equity: - codes.extend(self.config.asset_pools.equity.keys()) + # 添加所有信号标的 + codes.update(self.config.asset_pools.get_signal_codes()) - # 商品资产 - if self.config.asset_pools.commodity: - codes.extend(self.config.asset_pools.commodity.keys()) + # 添加所有交易标的 + codes.update(self.config.asset_pools.get_trade_codes()) - # 固定收益资产 - if self.config.asset_pools.fixed_income: - codes.extend(self.config.asset_pools.fixed_income.keys()) - - return codes + return list(codes) def compute_factors(self, data: Dict[str, pd.DataFrame]) -> Dict[str, pd.Series]: """ - 计算动量因子 + 计算动量因子(只使用信号标的的数据) Args: - data: 数据字典 {code: DataFrame} + data: 数据字典 {code: DataFrame}(包含 signal_source 和 trade_source) Returns: - 因子字典 {code: Series} + 因子字典 {signal_source: Series} """ 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: - # 计算动量得分 + df = data[code] + # 计算动量得分(使用信号标的的数据) factor_values = self.momentum.compute(df) factors[code] = factor_values 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: - positions: 仓位 DataFrame - data: 数据字典 {code: DataFrame} + positions: 仓位 DataFrame(columns=signal_source) + data: 数据字典 {code: DataFrame}(包含 signal_source 和 trade_source) Returns: 回测结果字典 """ - # 提取收盘价 + # 获取信号→交易映射 + signal_to_trade = self.config.asset_pools.get_signal_to_trade_mapping() + + # 提取交易标的的收盘价 close_prices = {} - for code, df in data.items(): - if 'close' in df.columns: - close_prices[code] = df['close'] + 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)