trading/doc_01_data_pipeline.md

510 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 量化交易数据管道详解
## `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)*