Archive legacy framework and utility modules that are no longer referenced by the active core (datasource/ and rotation/): - framework/ -> archive/framework/ - framework_v2/ -> archive/framework_v2/ - strategies/ -> archive/strategies/ - config/ -> archive/config/ - visualization/ -> archive/visualization/ - scripts/ -> archive/scripts/ - tests/ -> archive/tests/ - run_rotation.py, run_us_rotation.py -> archive/single_files/ - compare_*.py, test_api_dates.py -> archive/single_files/
8.7 KiB
8.7 KiB
跨市场数据对齐方案
📋 问题背景
在跨市场 ETF 轮动策略中,不同市场的交易日历不同:
| 市场 | 典型假日 | 影响 |
|---|---|---|
| A 股 | 春节、国庆 | 每年约 115 个交易日 |
| 美股 | 马丁路德金日、感恩节 | 每年约 252 个交易日 |
| 港股 | 佛诞日、圣诞节 | 每年约 250 个交易日 |
核心问题:如何在不同交易日历之间对齐因子和收益率?
❌ 常见错误
错误 1:先计算收益率,再 ffill
# ❌ 错误
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 后的价格计算因子
# ❌ 错误
close_aligned = close.reindex(a_share_dates, method='ffill')
factor = close_aligned.rolling(25).apply(weighted_momentum)
# 问题:
# 25 日窗口中包含 2-3 个重复值(ffill 填充)
# 导致:
# 1. 因子计算偏差(重复值影响线性回归)
# 2. 动量得分虚高(价格"不变"被误认为稳定)
✅ 正确方案
原则 1:因子在原始日历计算,再对齐
# ✅ 正确
# 1. 在原始交易日历计算因子
factor = close.rolling(25).apply(weighted_momentum) # 美股日历
# 2. 对齐因子值到 A 股日历
factor_aligned = factor.reindex(a_share_dates, method='ffill')
# 为什么正确:
# - 因子计算使用原始日历(25 个真实交易日)
# - 对齐的是因子值,不是价格
# - ffill 填充的是"最新因子值",而不是"重复价格"
原则 2:价格先对齐,再计算收益率
# ✅ 正确
# 1. 价格对齐到 A 股日历
close_aligned = close.reindex(a_share_dates, method='ffill')
# 2. 计算收益率
returns = close_aligned.pct_change(fill_method=None)
# 为什么正确:
# - 休市日价格不变(ffill)
# - 收益率 = (今日价格 - 昨日价格) / 昨日价格 = 0%
# - 不会复制前一天的非零收益率
🏗️ CrossMarketAligner 实现
核心功能
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
)
验证逻辑
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 通过
🎯 在策略中使用
完整流程
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. 执行回测
...
⚠️ 注意事项
- 填充值标记:
is_filled列标记哪些是 ffill 填充的,可用于后续分析 - NaN 处理:收益率对齐后自动填充为 0(表示"无数据,收益率为 0")
- 异常检测:单日收益率 > 50% 会发出警告
- 索引验证:对齐后严格验证索引是否匹配目标日历
- 统计信息:通过
aligner.get_stats()获取对齐统计
📚 相关文档
- 数据架构方案 - 完整的数据架构设计
- 数据流完整推演 - 从 OHLCV 到最终收益的 7 个阶段推演
- 框架 V2 README - 框架总览
- 实现:
framework_v2/shared/data/alignment.py - 测试:
framework_v2/tests/test_alignment.py
创建日期: 2026-05-06 版本: 1.0.0