# 跨市场数据对齐方案

## 📋 问题背景

在跨市场 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*
