19 KiB
量化交易数据管道详解
quant_data_pipeline_demo.py 学习文档
目标读者:零基础量化入门者
配套文件:quant_data_pipeline_demo.py
系列位置:第 1 篇 — 数据准备篇
目录
- 为什么数据准备如此重要?
- 主题一:价格复权 (Price Adjustment)
- 主题二:收益率计算 (Return Calculation)
- 主题三:多标的面板与缺失值处理 (Panel & Missing Values)
- 主题四:异常值检测与处理 (Outlier Detection & Treatment)
- 主题四B:涨跌停标记 (Circuit Breaker Flags)
- 主题五:交易日历与跨市场对齐 (Trading Calendar)
- 主题六:端到端数据管道类 (DataPipeline Class)
- 常见错误与避坑指南
- 术语速查表
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 关键代码解析
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 前视偏差警告 ⚠️
# ❌ 错误示范: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 不是删除异常值,而是把它们"拉回"到合理范围内:
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 代码的处理方式
代码只标记,不删除:
# 返回:+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)
只保留两个市场都开盘的日期:
common_days = us_calendar.intersection(cn_calendar)
- 🟢 适合:计算相关性、回归分析、信号对比
- 🔴 缺点:损失了单一市场的交易日数据
② 向前填充 (Forward Fill)
对于某市场休市的日期,用上一个交易日的数据填充:
aligned = prices.reindex(combined_index).ffill()
- 🟢 适合:持续计算净值曲线、组合市值统计
- 🔴 缺点:引入大量"人工零收益日",会低估波动率,高估夏普比率
7.3 生产环境建议
代码中使用了手写的简化假日列表,实际生产中推荐使用专业库:
# 推荐库(需额外安装)
pip install exchange_calendars
pip install pandas_market_calendars
8. 主题六:端到端数据管道类 (DataPipeline Class)
8.1 为什么要封装成类?
把以上所有步骤封装成一个 DataPipeline 类,好处是:
- 可复用:每次只需一行代码
pipeline.run()完成所有清洗 - 可配置:通过参数调整阈值,不修改核心逻辑
- 有记录:自动生成数据质量报告,便于审计
- 防错:步骤固定,不会因顺序出错导致问题
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 使用示例
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:忘记复权就直接计算收益率
# 错误:用未复权价格直接计算
returns = unadj_prices.pct_change() # 包含拆股/分红的虚假跳跃!
# 正确
returns = fwd_adj_prices.pct_change()
❌ 错误 2:用 bfill 填充缺失值
# 错误:bfill 用未来数据填补过去,引入前视偏差
clean = prices.bfill() # 危险!
# 正确:只用 ffill
clean = prices.ffill()
❌ 错误 3:异常值处理后忘记重新计算收益率
# 错误:先计算收益率,再检测,再删除某些日期
returns = prices.pct_change()
returns = returns[~outlier_mask] # 删掉某些日期后,相邻的收益率跨越了多天!
# 正确:要么 Winsorize(不删日期),要么先处理价格再算收益率
returns_clean = winsorize(returns) # 保持日期连续性
❌ 错误 4:跨市场计算时忘记对齐日历
# 错误:直接计算相关性
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 | 资产在市场上被买卖的难易程度 |
下一篇:策略开发与向量化回测