# 跨市场数据对齐方案 ## 📋 问题背景 在跨市场 ETF 轮动策略中,不同市场的交易日历不同: | 市场 | 典型假日 | 影响 | |------|----------|------| | A 股 | 春节、国庆 | 每年约 115 个交易日 | | 美股 | 马丁路德金日、感恩节 | 每年约 252 个交易日 | | 港股 | 佛诞日、圣诞节 | 每年约 250 个交易日 | **核心问题**:如何在不同交易日历之间对齐因子和收益率? --- ## ❌ 常见错误 ### 错误 1:先计算收益率,再 ffill ```python # ❌ 错误 returns = close.pct_change() returns_aligned = returns.reindex(a_share_dates, method='ffill') # 问题: 日期 价格 收益率 A股日历 对齐后收益率 2024-01-01 100 NaN 2024-01-01 NaN 2024-01-02 101 +1% 2024-01-02 +1% ✓ 2024-01-03 102 +1% 2024-01-03 +1% ← 错误!复制了前一天的收益率 ↑ 美股休市,价格不变 # 结果:A 股交易日"继承"了美股休市期间的收益率 # 导致:净值高估/低估 ``` ### 错误 2:用 ffill 后的价格计算因子 ```python # ❌ 错误 close_aligned = close.reindex(a_share_dates, method='ffill') factor = close_aligned.rolling(25).apply(weighted_momentum) # 问题: # 25 日窗口中包含 2-3 个重复值(ffill 填充) # 导致: # 1. 因子计算偏差(重复值影响线性回归) # 2. 动量得分虚高(价格"不变"被误认为稳定) ``` --- ## ✅ 正确方案 ### 原则 1:因子在原始日历计算,再对齐 ```python # ✅ 正确 # 1. 在原始交易日历计算因子 factor = close.rolling(25).apply(weighted_momentum) # 美股日历 # 2. 对齐因子值到 A 股日历 factor_aligned = factor.reindex(a_share_dates, method='ffill') # 为什么正确: # - 因子计算使用原始日历(25 个真实交易日) # - 对齐的是因子值,不是价格 # - ffill 填充的是"最新因子值",而不是"重复价格" ``` ### 原则 2:价格先对齐,再计算收益率 ```python # ✅ 正确 # 1. 价格对齐到 A 股日历 close_aligned = close.reindex(a_share_dates, method='ffill') # 2. 计算收益率 returns = close_aligned.pct_change(fill_method=None) # 为什么正确: # - 休市日价格不变(ffill) # - 收益率 = (今日价格 - 昨日价格) / 昨日价格 = 0% # - 不会复制前一天的非零收益率 ``` --- ## 🏗️ CrossMarketAligner 实现 ### 核心功能 ```python from framework_v2.shared.data.alignment import CrossMarketAligner # 初始化 aligner = CrossMarketAligner(target_calendar=a_share_dates) # 1. 对齐因子值 aligned_factor = aligner.align_factor( factor_series, source_calendar=us_dates, code='^GSPC' ) # 返回 DataFrame: # - value: 对齐后的因子值 # - is_filled: 是否为 ffill 填充值 # 2. 对齐收益率 returns = aligner.align_returns( close_series, code='^GSPC' ) # 返回 Series(收益率,A 股日历) # 3. 对齐多标的 returns_df = aligner.align_multi_asset({ '^GSPC': close_sp500, '^IXIC': close_nasdaq, '931862.CSI': close_bond }) # 返回 DataFrame(所有标的同索引) # 4. 验证信号与收益率对齐 aligned_signals, aligned_returns = aligner.validate_alignment( signals, returns_df ) ``` ### 验证逻辑 ```python class CrossMarketAligner: def _validate_factor_alignment(self, aligned, is_filled, code): """验证因子对齐""" # 1. 检查 NaN 比例 nan_ratio = aligned.isna().sum() / len(aligned) if nan_ratio > 0.1: warnings.warn(f"{code}: 因子 NaN 比例过高") # 2. 检查填充比例 fill_ratio = is_filled.sum() / len(is_filled) if fill_ratio > 0.3: warnings.warn(f"{code}: 因子填充比例过高") def _validate_returns(self, returns, code): """验证收益率""" # 1. 检查 NaN 比例 nan_ratio = returns.isna().sum() / len(returns) if nan_ratio > 0.1: raise ValueError(f"{code}: 收益率 NaN 比例过高") # 2. 检查异常值 max_return = returns.abs().max() if max_return > 0.5: # 单日涨跌 > 50% warnings.warn(f"{code}: 发现异常收益率") # 3. 检查索引 if not returns.index.equals(self.target_calendar): raise ValueError(f"{code}: 收益率索引与目标日历不匹配") ``` --- ## 📊 测试验证 ### 测试 1:因子对齐 ``` 源日历(美股): 8 天 目标日历(A股): 10 天 对齐后因子值: value is_filled 2024-01-01 0.10 False ← 真实值 2024-01-02 0.15 False ← 真实值 2024-01-03 0.15 True ← ffill 填充 2024-01-04 0.18 False ← 真实值 ... ✓ 填充值正确标记 ✓ NaN 比例检查通过 ``` ### 测试 2:收益率对齐 ``` 原始价格(美股日历): 2024-01-01 100.0 2024-01-02 101.0 2024-01-04 102.0 ← 2024-01-03 休市 对齐后收益率(A股日历): 2024-01-01 0.000000 ← 首日 2024-01-02 0.010000 ← +1% 2024-01-03 0.000000 ← 0%(休市,价格不变)✓ 2024-01-04 0.009901 ← +0.99% ✓ 休市日收益率 = 0% ✓ 无 NaN ✓ 索引匹配 A 股日历 ``` ### 测试 3:ffill 陷阱对比 ``` ❌ 错误做法:先 pct_change,再 reindex 步骤 1 - 收益率: 2024-01-01 NaN 2024-01-02 0.010000 2024-01-04 0.009901 步骤 2 - reindex + ffill: 2024-01-01 NaN 2024-01-02 0.010000 2024-01-03 0.010000 ← 错误!复制了前一天的收益率 2024-01-04 0.009901 ✅ 正确做法:先 reindex 价格,再 pct_change 步骤 1 - 价格 reindex: 2024-01-01 100.0 2024-01-02 101.0 2024-01-03 101.0 ← ffill(价格不变) 2024-01-04 102.0 步骤 2 - pct_change: 2024-01-01 0.000000 2024-01-02 0.010000 2024-01-03 0.000000 ← 正确!收益率 = 0% 2024-01-04 0.009901 ``` ### 测试结果 ``` ============================================================ 测试总结 ============================================================ ✓ 通过 - 因子对齐 ✓ 通过 - 收益率对齐 ✓ 通过 - 多标的对齐 ✓ 通过 - 信号与收益对齐 ✓ 通过 - ffill 陷阱 总计: 5/5 通过 ``` --- ## 🎯 在策略中使用 ### 完整流程 ```python from framework_v2.shared.data.alignment import CrossMarketAligner class RotationStrategy(StrategyBase): def run_backtest(self, data: dict) -> dict: # 1. 获取数据 index_data = data['index_data'] a_share_dates = data['a_share_dates'] valid_codes = data['valid_codes'] # 2. 创建对齐器 aligner = CrossMarketAligner(target_calendar=a_share_dates) # 3. 计算因子(原始日历)→ 对齐到 A 股日历 factor_dict = {} for code in valid_codes: close_series = index_data[code]['close'] # 在原始日历计算因子 factor_series = self._factor.compute( pd.DataFrame({'close': close_series}) ) # 对齐到 A 股日历 aligned = aligner.align_factor( factor_series, source_calendar=close_series.index, code=code ) factor_dict[code] = aligned['value'] factor_df = pd.DataFrame(factor_dict) # 4. 生成信号 signals = self._selector.generate(factor_df) # 5. 计算收益率(价格对齐 → 收益率) returns_df = aligner.align_multi_asset({ code: index_data[code]['close'] for code in valid_codes }) # 6. 验证信号与收益率对齐 aligned_signals, aligned_returns = aligner.validate_alignment( signals, returns_df ) # 7. 执行回测 ... ``` --- ## ⚠️ 注意事项 1. **填充值标记**:`is_filled` 列标记哪些是 ffill 填充的,可用于后续分析 2. **NaN 处理**:收益率对齐后自动填充为 0(表示"无数据,收益率为 0") 3. **异常检测**:单日收益率 > 50% 会发出警告 4. **索引验证**:对齐后严格验证索引是否匹配目标日历 5. **统计信息**:通过 `aligner.get_stats()` 获取对齐统计 --- ## 📚 相关文档 - **[数据架构方案](DATA_ARCHITECTURE.md)** - 完整的数据架构设计 - **[数据流完整推演](DATA_FLOW_DEMO.md)** - 从 OHLCV 到最终收益的 7 个阶段推演 - **[框架 V2 README](README.md)** - 框架总览 - 实现:`framework_v2/shared/data/alignment.py` - 测试:`framework_v2/tests/test_alignment.py` --- *创建日期: 2026-05-06* *版本: 1.0.0*