trading/doc_01_data_pipeline.md

19 KiB
Raw Permalink Blame History

量化交易数据管道详解

quant_data_pipeline_demo.py 学习文档

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


目录

  1. 为什么数据准备如此重要?
  2. 主题一:价格复权 (Price Adjustment)
  3. 主题二:收益率计算 (Return Calculation)
  4. 主题三:多标的面板与缺失值处理 (Panel & Missing Values)
  5. 主题四:异常值检测与处理 (Outlier Detection & Treatment)
  6. 主题四B涨跌停标记 (Circuit Breaker Flags)
  7. 主题五:交易日历与跨市场对齐 (Trading Calendar)
  8. 主题六:端到端数据管道类 (DataPipeline Class)
  9. 常见错误与避坑指南
  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 关键代码解析

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 类,好处是:

  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 使用示例

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 资产在市场上被买卖的难易程度

下一篇:策略开发与向量化回测