# 量化交易数据管道详解
## `quant_data_pipeline_demo.py` 学习文档

> **目标读者**：零基础量化入门者  
> **配套文件**：`quant_data_pipeline_demo.py`  
> **系列位置**：第 1 篇 — 数据准备篇

---

## 目录

1. [为什么数据准备如此重要？](#1-为什么数据准备如此重要)
2. [主题一：价格复权 (Price Adjustment)](#2-主题一价格复权-price-adjustment)
3. [主题二：收益率计算 (Return Calculation)](#3-主题二收益率计算-return-calculation)
4. [主题三：多标的面板与缺失值处理 (Panel & Missing Values)](#4-主题三多标的面板与缺失值处理)
5. [主题四：异常值检测与处理 (Outlier Detection & Treatment)](#5-主题四异常值检测与处理)
6. [主题四B：涨跌停标记 (Circuit Breaker Flags)](#6-主题四b涨跌停标记-circuit-breaker-flags)
7. [主题五：交易日历与跨市场对齐 (Trading Calendar)](#7-主题五交易日历与跨市场对齐)
8. [主题六：端到端数据管道类 (DataPipeline Class)](#8-主题六端到端数据管道类-datapipeline-class)
9. [常见错误与避坑指南](#9-常见错误与避坑指南)
10. [术语速查表](#10-术语速查表)

---

## 1. 为什么数据准备如此重要？

> **"Garbage in, garbage out."（垃圾进，垃圾出。）**

在量化交易 (Quantitative Trading) 中，策略再好，如果输入的数据有问题，回测结果就毫无意义，甚至会误导你投入真实资金。

**一个生动的例子：**

假设你手里有一支股票的历史价格数据，在某一天价格从 10 元突然跳到 5 元。  
如果你直接计算收益率，会得到 `-50%` 的单日大跌。  
但实际上，那一天股票进行了 **1:2 拆股（Stock Split）**，每股变成两股，价格减半是完全正常的市场行为，真实的收益率是 **0%**。

如果你的算法把这个 `-50%` 当成真实的大跌信号，就会产生错误的交易决策。

**数据准备解决的核心问题：**

| 问题 | 来源 | 后果（如果不处理）|
|------|------|------------------|
| 价格跳跃（非真实波动）| 拆股、分红 | 虚假的极端收益率，扭曲指标计算 |
| 缺失值 (Missing Values) | 停牌、数据源错误 | NaN 传播导致指标全部失效 |
| 异常值 (Outliers) | 数据错误、极端行情 | 拉偏统计量，产生虚假信号 |
| 涨跌停 (Price Limits) | A 股特有规则 | 误以为可以成交，策略无法执行 |
| 日历错位 (Calendar Mismatch) | 跨市场不同假日 | 计算相关性时出现错位 |

---

## 2. 主题一：价格复权 (Price Adjustment)

### 2.1 什么是"公司行为"？

**公司行为 (Corporate Actions)** 是公司自愿或法定的资本结构变化，会导致股价出现非市场原因的人为跳跃。最常见的两种：

**① 股票拆分 (Stock Split)**  
- 一股变多股，价格等比下降  
- 例：1:2 拆股 → 原来 1 股 20 元 → 变成 2 股各 10 元  
- 持有人总市值不变，但价格图上会出现 `-50%` 的"假下跌"

**② 现金分红 (Cash Dividend)**  
- 公司将利润的一部分以现金形式分配给股东  
- 股价在除息日 (Ex-Dividend Date) 会自然下降分红金额  
- 例：每股分红 0.5 元，股价从 20 元跌到 19.5 元（这是"假跌"）

### 2.2 三种价格序列

代码生成并演示了三种价格序列：

```
未复权价格 (Unadjusted Price)
  ↓ 拆股/分红后，历史价格出现人为跳跃
  ↓ 不能直接用于计算收益率

前复权价格 (Forward-Adjusted / Backward-Adjusted Price)
  ↓ 将历史价格按复权因子折算到"当前价格水平"
  ↓ 连续可微，适合计算技术指标

后复权价格 (Backward-Adjusted Price)
  ↓ 以最早的价格为基准，向后调整
  ↓ 保留了历史价格的"真实感"，适合长期价值分析
```

**形象理解：**

想象你记录了一个朋友的身高，但 TA 第 3 年做了骨骼手术增高了 5 厘米。  
- **未复权**：第 1-2 年记录真实数据，第 3 年突然跳高 5 厘米  
- **前复权**：把第 1-2 年的数据都加 5 厘米，保持最新身高是真实的  
- **后复权**：第 3 年以后的数据都减 5 厘米，保持早期身高是真实的  

### 2.3 关键代码解析

```python
def forward_adjust(unadj_prices, adj_factor):
    """前复权：以最新价格为基准，向前调整历史价格"""
    # adj_factor / adj_factor.iloc[-1]
    # → 将复权因子标准化为"相对于最新时刻的倍数"
    normalized_factor = adj_factor / adj_factor.iloc[-1]
    return unadj_prices * normalized_factor
```

**复权因子 (Adjustment Factor)** 是一个随时间变化的乘数，记录了从最初到当前发生的所有拆股和分红的累积影响。

### 2.4 实践建议

| 场景 | 推荐价格类型 |
|------|-------------|
| 计算历史收益率、构建技术指标 | **前复权** (Forward-Adjusted) |
| 研究历史上的真实交易价格 | **后复权** (Backward-Adjusted) |
| 展示给非专业用户的图表 | **未复权** (Unadjusted)，加注说明 |

---

## 3. 主题二：收益率计算 (Return Calculation)

### 3.1 两种收益率的公式

**简单收益率 (Simple Return)**

$$r_t = \frac{P_t - P_{t-1}}{P_{t-1}} = \frac{P_t}{P_{t-1}} - 1$$

用代码表示：`prices.pct_change()`

**对数收益率 (Log Return / Continuously Compounded Return)**

$$r_t^{log} = \ln\left(\frac{P_t}{P_{t-1}}\right)$$

用代码表示：`np.log(prices / prices.shift(1))`

### 3.2 为什么量化研究更偏爱对数收益率？

**理由一：时间可加性 (Time Additivity)**

对数收益率可以直接相加得到总收益率：

$$r_{总}^{log} = r_1^{log} + r_2^{log} + \cdots + r_n^{log}$$

简单收益率必须连乘（而不是相加）：

$$1 + r_{总} = (1 + r_1) \times (1 + r_2) \times \cdots \times (1 + r_n)$$

**理由二：对称性更好**

- 价格涨 100%（翻倍），简单收益率 = +100%
- 价格跌 50%（腰斩），简单收益率 = -50%
- 但两者其实是反向对称操作！用对数收益率：涨 100% ≈ +69.3%，跌 50% ≈ -69.3%，完美对称

**理由三：统计性质更好**

对数收益率更接近正态分布 (Normal Distribution)，更适合统计建模。

### 3.3 什么时候用哪种？

| 用途 | 推荐 |
|------|------|
| 构建技术指标、计算波动率 | 对数收益率 |
| 计算多资产组合收益率 | 简单收益率（加权平均在简单收益率下才正确）|
| 日内高频交易 | 两者差异极小，均可 |

> ⚠️ **重要警告**：绝对不要在同一个计算中混用两种收益率！

---

## 4. 主题三：多标的面板与缺失值处理

### 4.1 什么是数据面板 (Panel Data)？

**面板数据**是一张二维表格，行是时间（每个交易日），列是不同的股票（或其他资产）：

```
日期        Stock_A   Stock_B   Stock_C
2022-01-03   100.5     50.2      NaN
2022-01-04   101.2     NaN       80.3
2022-01-05   NaN       51.0      81.0
...
```

`NaN` 就是缺失值，表示"这一天没有数据"。

### 4.2 缺失值的两大来源

**① 停牌 (Trading Suspension)**  
- 中国 A 股常见：某只股票因重大事项公告临时暂停交易  
- 可能持续数天到数月  
- 停牌期间通常用"前值填充"

**② 数据源错误**  
- 数据提供商漏报、乱码、延迟上报  
- 通常是随机的单日缺失

### 4.3 四种处理方法对比

代码中演示了四种方法，以下是直观对比：

| 方法 | 代码 | 适用场景 | 缺点 |
|------|------|----------|------|
| **向前填充 (ffill)** | `prices.ffill()` | 停牌（假设价格不变）| 长时间停牌后恢复会有虚假零收益 |
| **线性插值 (linear)** | `prices.interpolate()` | 短期随机缺失 | 插值出"未来数据"，可能引入前视偏差 |
| **有限填充 (ffill+limit)** | `prices.ffill(limit=5)` | 最多允许填充 5 天 | 超过限制仍会有 NaN |
| **直接删除 (drop)** | `prices.dropna()` | 整行/整列数据质量差 | 丢失样本，影响后续计算 |

### 4.4 前视偏差警告 ⚠️

```python
# ❌ 错误示范：bfill 会用"未来价格"填充"过去缺失"
clean_prices = price_panel.bfill()  # 危险！引入前视偏差！

# ✅ 正确做法：只向前看，不向后看
clean_prices = price_panel.ffill()
clean_prices = clean_prices.dropna(how='any')  # 删掉最开头还剩的 NaN
```

**前视偏差 (Lookahead Bias / Look-Ahead Bias)**：在回测时，不小心使用了"当时还不知道的未来数据"，导致回测结果虚高，实盘中无法复现。这是回测中最常见也最危险的错误之一。

### 4.5 缺失值超过多少就应该排除该标的？

代码中使用的规则：`缺失比例 > 10%` → 排除该标的

这是经验阈值，具体情况具体分析：
- 研究期只有 1 年（250 天）：缺失 > 25 天就该考虑排除
- 研究期有 5 年（1250 天）：缺失 > 125 天才排除（同比例）

---

## 5. 主题四：异常值检测与处理

### 5.1 什么是异常值？

**异常值 (Outlier)** 是远离其他数据点的极端值，可能来自：
- 数据错误（数据库录入错误、单位错误）
- 真实极端行情（市场崩盘、涨停首日）
- 算法错误

对于收益率数据，典型的异常值是单日涨跌超过 30%（非 A 股市场）的数据。

### 5.2 两种检测方法

#### Z-score 方法（标准分数法）

$$Z = \frac{x - \bar{x}}{\sigma}$$

- 计算每个值偏离均值多少个标准差
- `|Z| > 3` 视为异常值

**通俗理解**：班级考试平均 70 分，标准差 10 分。  
你考了 100 分，Z = (100-70)/10 = 3，属于极端高分。

**缺点**：均值 $\bar{x}$ 和标准差 $\sigma$ 本身会被异常值"污染"，所以 Z-score 对异常值不够稳健 (Not Robust)。

#### MAD 方法（中位数绝对偏差，Median Absolute Deviation）

$$\text{MAD} = \text{median}(|x_i - \text{median}(x)|)$$

$$\text{Modified Z} = \frac{0.6745 \times (x - \text{median}(x))}{\text{MAD}}$$

- 用**中位数**代替均值，对极端值天然免疫
- 量化研究中更常用

**通俗理解**：Z-score 用"平均数"衡量偏差，异常值会把平均数拉偏，进而影响判断。MAD 用"中位数"衡量，中位数不受极端值影响。

### 5.3 Winsorize（缩尾处理）

**Winsorize** 不是删除异常值，而是把它们"拉回"到合理范围内：

```python
def winsorize(series, lower_pct=0.01, upper_pct=0.99):
    lower = series.quantile(0.01)   # 1% 分位数
    upper = series.quantile(0.99)   # 99% 分位数
    return series.clip(lower=lower, upper=upper)
    # 低于 1% 分位数的值 → 拉回到 1% 分位数
    # 高于 99% 分位数的值 → 拉回到 99% 分位数
```

**优点**：保留所有数据量，只是限制极端值的影响范围。这在构建因子时是标准操作。

### 5.4 Q-Q 图 (Quantile-Quantile Plot)

Q-Q 图用于直观判断数据是否符合正态分布：
- **点在对角线上**：数据是正态分布
- **两端翘起（像字母 S 或笑脸）**：分布有**厚尾 (Fat Tails)**

金融收益率几乎**总是**有厚尾。这意味着极端事件（大涨大跌）的发生频率远高于正态分布的预测，所以不能天真地假设收益率服从正态分布。

---

## 6. 主题四B：涨跌停标记 (Circuit Breaker Flags)

### 6.1 中国 A 股特有规则

**涨跌停板制度 (Price Limit System)**：
- 普通股票：单日最大涨幅 **+10%**，最大跌幅 **-10%**
- ST 股票（风险警示股）：限制为 **±5%**

这不是数据错误！这是真实发生的市场数据。

### 6.2 为什么需要标记？

当股票涨停或跌停时：
- ✅ 收盘价是真实的
- ❌ 但这只股票**很可能全天无法成交**（买盘/卖盘全部封死）

**后果**：如果你的策略在涨停日产生"卖出"信号，实际上根本卖不掉；跌停日产生"买入"信号，根本买不到。  
这叫**流动性陷阱 (Liquidity Trap)**。

### 6.3 代码的处理方式

代码**只标记，不删除**：

```python
# 返回：+1 = 涨停，-1 = 跌停，0 = 正常
def flag_price_limits(prices, limit_pct=0.10):
    ...
```

策略代码可以根据这个标记自行决定：
- 跳过涨跌停日的信号（最常见）
- 降低该日的信号权重
- 完全排除该日的统计计算

### 6.4 幸存者偏差警告 ⚠️

> **幸存者偏差 (Survivorship Bias)**：数据集只包含"活到现在"的股票，而不包含已退市的股票。这会导致回测收益率虚高，因为你实际上只回测了"最后赢家"。  
> 解决方案：使用包含历史退市股票的完整数据集（通常需要付费数据源）。

---

## 7. 主题五：交易日历与跨市场对齐

### 7.1 问题的来源

不同国家的市场有不同的节假日：
- A 股有春节（7 天以上假期）
- 美股没有春节，但有感恩节、独立日等
- 港股假期介于两者之间

当你想计算"A 股与美股的相关性"时，两个市场的交易日不完全重合，怎么对齐是一个实际问题。

### 7.2 两种对齐策略

**① 取交集 (Intersection)**  
只保留两个市场都开盘的日期：

```python
common_days = us_calendar.intersection(cn_calendar)
```

- 🟢 适合：计算相关性、回归分析、信号对比
- 🔴 缺点：损失了单一市场的交易日数据

**② 向前填充 (Forward Fill)**  
对于某市场休市的日期，用上一个交易日的数据填充：

```python
aligned = prices.reindex(combined_index).ffill()
```

- 🟢 适合：持续计算净值曲线、组合市值统计
- 🔴 缺点：引入大量"人工零收益日"，会低估波动率，高估夏普比率

### 7.3 生产环境建议

代码中使用了手写的简化假日列表，实际生产中推荐使用专业库：

```python
# 推荐库（需额外安装）
pip install exchange_calendars
pip install pandas_market_calendars
```

---

## 8. 主题六：端到端数据管道类 (DataPipeline Class)

### 8.1 为什么要封装成类？

把以上所有步骤封装成一个 `DataPipeline` 类，好处是：
1. **可复用**：每次只需一行代码 `pipeline.run()` 完成所有清洗
2. **可配置**：通过参数调整阈值，不修改核心逻辑
3. **有记录**：自动生成数据质量报告，便于审计
4. **防错**：步骤固定，不会因顺序出错导致问题

### 8.2 管道的 6 个步骤

```
原始价格面板 (Raw Price Panel)
      │
      ▼ Step 1: 过滤缺失率 > 10% 的标的
      │  (Filter stocks with > 10% missing data)
      ▼ Step 2: ffill + bfill 填充剩余缺失值
      │  (Forward + Backward fill remaining NaN)
      ▼ Step 3: 计算日收益率
      │  (Compute daily returns)
      ▼ Step 4: MAD 异常值检测 + Winsorize 处理
      │  (Detect outliers with MAD, Winsorize at 1%/99%)
      ▼ Step 5: 生成数据质量报告
      │  (Generate quality report: mean, vol, skew, kurtosis)
      ▼
干净的收益率面板 (Clean Returns Panel)
```

### 8.3 使用示例

```python
from pipeline import DataPipeline  # 或直接在 demo 文件末尾运行

pipeline = DataPipeline(
    price_panel,
    max_missing_pct=0.15,      # 缺失率阈值 15%
    winsorize_pct=(0.01, 0.99) # 缩尾范围 1%~99%
)
clean_returns = pipeline.run()
```

### 8.4 数据质量报告解读

管道运行后会打印各标的的统计表，关键指标含义：

| 指标 | 含义 | 健康范围（日频）|
|------|------|----------------|
| 日均收益率 (bp) | 平均每天的收益，以基点表示（1bp = 0.01%）| -5 ~ +5 bp |
| 日波动率 (%) | 每天收益率的标准差 | 0.5% ~ 3% |
| 年化收益率 (%) | 日均收益率 × 252 | -30% ~ +50% |
| 年化波动率 (%) | 日波动率 × √252 | 10% ~ 60% |
| 偏度 (Skewness) | 分布左右对称性（负值=左偏=有重大下跌）| -1 ~ +1 |
| 峰度 (Kurtosis) | 尾部厚度（>0 代表比正态分布更厚尾）| 金融数据通常 > 3 |

---

## 9. 常见错误与避坑指南

### ❌ 错误 1：忘记复权就直接计算收益率

```python
# 错误：用未复权价格直接计算
returns = unadj_prices.pct_change()  # 包含拆股/分红的虚假跳跃！

# 正确
returns = fwd_adj_prices.pct_change()
```

### ❌ 错误 2：用 bfill 填充缺失值

```python
# 错误：bfill 用未来数据填补过去，引入前视偏差
clean = prices.bfill()  # 危险！

# 正确：只用 ffill
clean = prices.ffill()
```

### ❌ 错误 3：异常值处理后忘记重新计算收益率

```python
# 错误：先计算收益率，再检测，再删除某些日期
returns = prices.pct_change()
returns = returns[~outlier_mask]  # 删掉某些日期后，相邻的收益率跨越了多天！

# 正确：要么 Winsorize（不删日期），要么先处理价格再算收益率
returns_clean = winsorize(returns)  # 保持日期连续性
```

### ❌ 错误 4：跨市场计算时忘记对齐日历

```python
# 错误：直接计算相关性
corr = returns_us.corr(returns_cn)  # 如果索引日期不同，pandas 会产生大量 NaN

# 正确：先对齐
common_idx = returns_us.index.intersection(returns_cn.index)
corr = returns_us[common_idx].corr(returns_cn[common_idx])
```

---

## 10. 术语速查表

| 中文 | English | 简要说明 |
|------|---------|----------|
| 量化交易 | Quantitative Trading | 用数学模型和程序自动化交易 |
| 复权 | Price Adjustment | 消除拆股/分红对价格的人为影响 |
| 前复权 | Forward Adjustment | 历史价格折算到当前水平 |
| 后复权 | Backward Adjustment | 当前价格折算到历史水平 |
| 复权因子 | Adjustment Factor | 价格调整的乘数 |
| 拆股 | Stock Split | 一股变多股，价格等比下降 |
| 除息日 | Ex-Dividend Date | 分红的价格自然下降日 |
| 简单收益率 | Simple Return | (P_t - P_{t-1}) / P_{t-1} |
| 对数收益率 | Log Return | ln(P_t / P_{t-1}) |
| 时间可加性 | Time Additivity | 对数收益率可直接相加的性质 |
| 面板数据 | Panel Data | 多标的 × 多时间的二维数据表 |
| 停牌 | Trading Suspension | 股票暂停交易 |
| 缺失值 | Missing Value / NaN | 数据中的空白 |
| 向前填充 | Forward Fill (ffill) | 用前一个有效值填充 NaN |
| 异常值 | Outlier | 远离正常范围的极端值 |
| 标准分数 | Z-score | 偏离均值的标准差倍数 |
| 中位数绝对偏差 | MAD (Median Absolute Deviation) | 更稳健的异常值检测方法 |
| 缩尾处理 | Winsorize | 将极端值截断到分位数边界 |
| 前视偏差 | Lookahead Bias | 回测中使用未来数据的错误 |
| 幸存者偏差 | Survivorship Bias | 数据集只包含"活下来"的标的 |
| 涨跌停 | Price Limit / Circuit Breaker | 单日涨跌幅上限（A 股 ±10%）|
| 厚尾 | Fat Tails / Leptokurtosis | 极端事件比正态分布更频繁 |
| 偏度 | Skewness | 分布的左右不对称程度 |
| 峰度 | Kurtosis | 分布尾部的厚度 |
| 交易日历 | Trading Calendar | 某市场的所有交易日集合 |
| 流动性 | Liquidity | 资产在市场上被买卖的难易程度 |

---

*下一篇：[策略开发与向量化回测](doc_02_strategy_backtest.md)*
