数据清洗与预处理:处理停牌、异常值、缺失值,统一时间索引,计算收益率、对数收益率
在量化分析中,数据是策略的基石。原始行情数据往往包含停牌导致的长时间无交易、异常跳空、缺失记录、时间索引错位等问题。如果直接将这些数据送入回测框架,轻则导致信号计算错误,重则产生未来函数或幸存者偏差。本章将系统讲解数据清洗的标准流程,并深入解析收益率与对数收益率的计算逻辑及其在量化建模中的适用场景。
上图展示了从原始数据到回测就绪数据的完整链路。数据清洗并非简单的“删除空值”,而是需要结合市场规则、策略频率和金融逻辑进行综合判断。下面我们将逐一拆解每个环节的核心原理与实操方法。
4.1 停牌数据处理:识别、标记与填充策略
4.1.1 核心原理
停牌是指股票因重大事项、涨跌停或临时公告等原因暂停交易。在日线数据中,停牌通常表现为当日无成交记录(成交量、成交额均为0或NaN),但部分数据源会保留一条“空记录”或直接缺失该行。停牌数据对量化策略的影响主要体现在三个方面:
- 信号失真:基于均线、动量等指标计算的信号在停牌期间无法真实反映市场状态。
- 收益率计算错误:若直接使用停牌日收盘价计算收益率,会产生大量0收益率或异常跳变。
- 未来函数风险:若未正确处理停牌,回测时可能错误地使用停牌日之后的数据进行交易决策。
4.1.2 判断方法与操作流程
处理停牌数据的第一步是准确识别。推荐使用以下流程:
- 检查成交量与成交额:若某交易日成交量为0或NaN,且非节假日,则判定为停牌。
- 检查价格变动:若当日收盘价与前一日完全相同且成交量极低(如低于历史均值的1%),需结合公告确认是否停牌。
- 使用交易日历对齐:将数据与交易所官方交易日历对齐,缺失的交易日即为停牌日。
识别停牌后,根据策略需求选择处理方式:
| 策略类型 | 推荐处理方式 | 原因 |
|---|---|---|
| 日频低频策略 | 前向填充(ffill) | 假设停牌期间价格不变,保持数据连续性 |
| 事件驱动策略 | 剔除停牌日 | 避免停牌事件干扰信号计算 |
| 高频/分钟策略 | 标记并单独处理 | 分钟级停牌需精确到时间戳 |
4.1.3 案例拆解:处理连续停牌股票
假设我们获取了某股票2024年1月1日至3月31日的日线数据,其中1月15日至1月26日因重大资产重组停牌10个交易日。原始数据中,这10天完全缺失。我们的处理步骤如下:
import pandas as pd
import numpy as np
# 假设df包含date, open, high, low, close, volume列
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)
# 生成完整交易日历(使用交易所日历)
trade_cal = pd.date_range(start='2024-01-01', end='2024-03-31', freq='B') # 简化示例,实际应使用exchange_calendars
# 重索引,缺失日期自动变为NaN
df = df.reindex(trade_cal)
# 识别停牌日(volume为NaN或0)
df['is_suspended'] = df['volume'].isna() | (df['volume'] == 0)
# 价格前向填充,成交量填充0
df['close'] = df['close'].ffill()
df['volume'] = df['volume'].fillna(0)
# 标记停牌日,供后续策略判断
df.loc[df['is_suspended'], 'signal'] = 0 # 停牌期间不产生交易信号
通过上述操作,我们保留了停牌期间的价格连续性,同时明确标记了停牌状态,避免策略在停牌日发出错误交易指令。复盘经验:在回测中,务必在停牌日禁止任何开仓或平仓操作,否则会产生“幽灵成交”。
4.2 异常值检测与处理:识别跳空、涨跌停与数据错误
4.2.1 核心原理
异常值是指明显偏离正常分布的数据点,可能源于数据源错误、除权除息未复权、或真实的极端行情(如涨停/跌停)。区分“真实异常”与“数据错误”是本节的关键。常见的异常类型包括:
- 价格跳空:单日涨跌幅超过20%(A股主板),可能为数据错误或极端事件。
- 负价格/零价格:明显的数据错误。
- 成交量突变:成交量突然放大或缩小至历史均值的100倍以上。
4.2.2 检测方法
推荐使用“规则+统计”双通道检测:
| 方法 | 公式/阈值 | 适用场景 |
|---|---|---|
| 涨跌停规则 | 涨跌幅 > 10%(主板)或 > 20%(创业板/科创板) | 快速识别极端行情 |
| MAD(中位差) | |x - median| > 5 * MAD | 稳健的统计异常检测 |
| Z-Score | |z| > 3 | 正态分布假设下的检测 |
4.2.3 实战场景:处理除权除息导致的“假异常”
某股票在2024年5月20日实施10送5派2元的分红方案,除权后股价从20元变为13.2元,单日跌幅达34%。若未使用复权数据,该日会被误判为异常值。解决方案:
# 使用前复权或后复权数据
# 若使用前复权,历史价格会被调整,保证收益率计算正确
df_adj = df['close'] * df['adj_factor'] # adj_factor为复权因子
# 检测异常时,应基于复权后的价格计算涨跌幅
daily_ret = df_adj.pct_change()
outliers = daily_ret[abs(daily_ret) > 0.2] # 复权后仍超过20%才视为异常
复盘经验:在数据清洗阶段,务必先完成复权处理,再进行异常值检测。否则,除权除息日会被误判为异常值而错误剔除,导致后续收益率计算出现偏差。
4.3 缺失值处理与时间索引统一
4.3.1 缺失值来源与处理策略
缺失值不仅来自停牌,还可能源于数据源本身的不完整(如部分交易日数据未下载)。处理策略需根据缺失比例和位置决定:
- 少量缺失(<1%):线性插值或前向填充。
- 连续缺失(>5个交易日):需检查是否停牌,若为停牌按4.1处理。
- 关键位置缺失(如月末):若影响财务指标计算,建议重新获取数据。
4.3.2 统一时间索引的完整流程
- 标准化日期格式:统一为YYYY-MM-DD格式。
- 对齐交易日历:使用交易所官方日历,剔除周末和节假日。
- 去重与排序:检查是否有重复日期,按时间升序排列。
- 重采样:若需将日线转为周线或月线,使用resample方法。
# 统一时间索引示例
df.index = pd.to_datetime(df.index)
df = df[~df.index.duplicated(keep='first')] # 去重
df = df.sort_index() # 排序
# 对齐交易日历(使用exchange_calendars库)
import exchange_calendars as xcals
cn_cal = xcals.get_calendar('XSHG')
valid_days = cn_cal.sessions_in_range('2024-01-01', '2024-12-31')
df = df.reindex(valid_days)
4.4 收益率与对数收益率的计算及应用
4.4.1 核心原理与数学定义
收益率是量化分析中最基础的指标。简单收益率(算术收益率)和对数收益率各有优劣:
| 类型 | 公式 | 优点 | 缺点 |
|---|---|---|---|
| 简单收益率 | R_t = (P_t - P_{t-1}) / P_{t-1} | 直观,易于理解 | 不可加性(多期收益率需连乘) |
| 对数收益率 | r_t = ln(P_t / P_{t-1}) | 可加性(多期收益率直接相加) | 数值较小,解释性稍弱 |
在量化建模中,对数收益率更受青睐,原因在于:
- 统计性质:对数收益率近似正态分布,便于使用参数化模型(如GARCH)。
- 时间可加性:计算多期收益率时,只需将单期对数收益率相加,避免连乘误差。
- 数值稳定性:对数变换可压缩极端值的影响。
4.4.2 计算实操与注意事项
# 计算简单收益率
df['simple_ret'] = df['close'].pct_change()
# 计算对数收益率
df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
# 多期收益率计算
# 简单收益率: (1+R1)*(1+R2) - 1
multi_simple = (1 + df['simple_ret']).prod() - 1
# 对数收益率: sum(r1 + r2)
multi_log = df['log_ret'].sum()
4.4.3 案例拆解:收益率计算在回测中的应用
假设我们构建了一个简单的双均线策略(5日均线上穿20日均线买入),回测周期为2024年1月至6月。关键步骤:
- 数据清洗后,计算对数收益率作为策略绩效评估基准。
- 策略信号基于复权后的收盘价计算均线。
- 交易成本按单边0.1%计算,在收益率中扣除。
# 策略绩效计算
df['strategy_ret'] = df['signal'].shift(1) * df['log_ret'] # 信号滞后一天避免未来函数
df['strategy_ret'] -= 0.001 # 扣除交易成本(简化处理)
# 累计收益
cumulative_ret = df['strategy_ret'].sum()
# 年化波动率
annual_vol = df['strategy_ret'].std() * np.sqrt(252)
复盘经验:使用对数收益率计算累计收益时,直接求和即可;若使用简单收益率,则需连乘。在回测框架中,统一使用对数收益率可以简化多资产组合的收益聚合计算。
4.5 常见误区与总结复盘
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 直接删除所有含NaN的行 | 丢失停牌信息,导致时间轴断裂 | 区分停牌与缺失,分别处理 |
| 未复权就计算收益率 | 除权日产生虚假负收益 | 先复权,再计算 |
| 使用未对齐的时间索引 | 信号与价格错位,产生未来函数 | 统一交易日历,严格对齐 |
| 忽略涨跌停数据 | 策略在极端行情下失效 | 保留涨跌停,但需检查流动性 |