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