## 文档体系(5 个文档,互相关联) - README.md - 框架总览 + 文档索引 - DATA_ARCHITECTURE.md - 数据架构方案(Schema、验证、性能优化) - ALIGNMENT_GUIDE.md - CrossMarketAligner 使用指南 - DATA_FLOW_DEMO.md - 从 OHLCV 到最终收益的 7 个阶段推演 - ALIGNMENT_SCHEMA_INTEGRATION.md - Aligner + Schema 整合方案 ## 文档特色 - 大量代码示例(✅ 正确 vs ❌ 错误对比) - 数据流可视化(ASCII 图) - 表格总结(问题、严重度、解决方案) - 实际场景推演(2024-01-01 ~ 2024-01-31) - 文档互链(形成知识网络) ## 修复 - .gitignore: 添加 !framework_v2/shared/data/ 例外 - 允许提交对齐器相关文件
332 lines
8.7 KiB
Markdown
332 lines
8.7 KiB
Markdown
# 跨市场数据对齐方案
|
||
|
||
## 📋 问题背景
|
||
|
||
在跨市场 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*
|