4. 数据清洗:处理停牌、涨跌停、复权因子等特殊情况的实战技巧

数据清洗这步,说实话,是量化投资里最磨人但又最绕不开的环节。很多人觉得策略牛逼就行,数据差不多得了。我早年也这么想,结果回测曲线漂亮得像假的一样,实盘一跑就崩。后来我明白了——数据不干净,策略就是空中楼阁

今天咱们就聊聊指数跟踪里那几个最头疼的数据坑:停牌、涨跌停、复权因子。嗯,一个一个来。

4.1 停牌数据:别让「消失的股票」搞乱你的净值

停牌这事,说白了就是股票突然不交易了。指数还在继续算,但你的持仓里这只票没法动。怎么办?

核心原则:停牌期间,用上一交易日收盘价替代。

我个人习惯的做法是,先标记停牌日,然后做前向填充。举个例子:

import pandas as pd
import numpy as np

# 假设 df 是某只股票的日线数据,包含 'close' 和 'volume'
# 停牌日 volume 为 0 或 NaN
df['is_suspended'] = df['volume'].isna() | (df['volume'] == 0)

# 停牌期间,用前一个交易日收盘价填充
df['adj_close'] = df['close'].fillna(method='ffill')

# 同时标记停牌状态,方便后续计算权重
df['suspended_flag'] = df['is_suspended'].astype(int)

我在项目中遇到过一个问题:有些股票停牌时间特别长,比如重组停牌半年。这时候如果只用前向填充,复牌那天价格会跳空,导致跟踪误差突然变大。我的建议是——在复牌当天,重新计算该股票在指数中的权重,而不是简单沿用停牌前的比例。

⚠️ 避坑指南
我曾经踩过一个坑:停牌股票在指数中的权重会随着其他股票涨跌而被动变化。比如某只票停牌了,其他票涨了,它的权重就自动变小了。如果你不重新平衡,复牌那天你的持仓比例就和指数对不上了。所以,停牌股票的权重需要每日重算,公式是:
权重 = 停牌前市值 / 当日指数总市值

4.2 涨跌停数据:封板了,你买不到也卖不掉

涨跌停是A股特色。你想想看,策略信号说「买入」,结果股票涨停了,你根本买不进去。回测里如果忽略这个,收益会被严重高估。

处理逻辑:涨跌停日,实际成交价可能不是收盘价,而且成交量受限。

我的做法分两步:

  1. 识别涨跌停:比较当日收盘价与前一交易日收盘价的涨跌幅,超过阈值(A股一般±10%)就标记。
  2. 模拟成交限制:涨停时只能卖出不能买入,跌停时只能买入不能卖出。
# 假设 df 有 'close' 和 'pre_close'
df['pct_chg'] = (df['close'] - df['pre_close']) / df['pre_close']

# 识别涨跌停(考虑ST股5%限制,这里简化处理)
limit_up = df['pct_chg'] >= 0.095  # 涨停
limit_down = df['pct_chg'] <= -0.095  # 跌停

# 标记交易限制
df['trade_restriction'] = 'normal'
df.loc[limit_up, 'trade_restriction'] = 'limit_up'
df.loc[limit_down, 'trade_restriction'] = 'limit_down'

嗯,这里要注意:涨跌停日的成交量通常很小,如果你在回测里假设能按收盘价成交全部目标数量,那是不现实的。我一般会加一个「成交比例」参数,比如涨停日只能成交正常量的20%。

💡 实战技巧
对于指数跟踪来说,涨跌停最大的影响是「无法及时调仓」。比如指数调仓日,某只新纳入的股票涨停了,你买不进去。这时候我的做法是:延迟到下一个交易日买入,并在跟踪误差计算中把这个延迟成本算进去。别小看这个,一次两次没事,积累多了误差就大了。

4.3 复权因子:后复权、前复权,到底用哪个?

复权这事,我见过太多人搞混了。说白了,股票分红送股后,价格会跳空,但你的实际资产没变。复权就是为了消除这种「假跳空」。

三种复权方式:

类型 说明 适用场景
前复权 调整历史价格,使当前价格不变 看历史走势、技术分析
后复权 调整当前价格,使历史价格不变 计算真实收益率
不复权 原始价格,有跳空 计算分红、送股影响

我个人习惯在指数跟踪里用后复权。为什么?因为后复权能真实反映你从买入到现在到底赚了多少。前复权虽然看起来连续,但会改变历史收益率,不适合做业绩归因。

# 假设从数据源获取了复权因子 adj_factor
# 后复权价格 = 收盘价 * 复权因子
df['adj_close_back'] = df['close'] * df['adj_factor']

# 前复权价格 = 收盘价 / 最新复权因子 * 历史复权因子
latest_factor = df['adj_factor'].iloc[-1]
df['adj_close_forward'] = df['close'] * df['adj_factor'] / latest_factor
📌 核心要点
在指数跟踪中,计算成分股权重时必须用后复权价格。因为指数的计算是基于真实市值变化的,前复权会扭曲市值数据。我见过有人用前复权算权重,结果跟踪误差莫名其妙大了0.5%,查了半天才发现是复权方式搞错了。

4.4 综合处理流程:一张图说清楚

上面讲了三个坑,实际处理时它们是交织在一起的。比如一只股票今天涨停又停牌?嗯,这种情况很少见,但逻辑上要先判断停牌,再判断涨跌停。我画了个流程图,帮你理清顺序:

数据清洗综合处理流程 原始日线数据 步骤1:停牌检测与填充 volume=0 → 前向填充收盘价,标记停牌状态 步骤2:涨跌停识别与限制 涨跌幅≥9.5% → 标记交易限制类型 是否调仓日? (指数调仓/再平衡) 调仓逻辑: 涨停→延迟买入 跌停→延迟卖出 正常持有 权重被动变化 步骤3:复权因子计算与价格调整 后复权价格 = 收盘价 × 复权因子

你看,流程其实不复杂,但顺序很重要。先处理停牌,再处理涨跌停,最后做复权。调仓日要单独判断,因为涨跌停对调仓的影响远大于日常持有。

4.5 实战中的几个小细节

最后分享几个我踩过的坑,希望能帮你省点时间:

  • 复权因子要每日更新:有些数据源只提供历史复权因子,不提供每日的。我建议自己算,公式是:复权因子 = 后复权价格 / 收盘价。后复权价格可以从Wind或Tushare获取。
  • 停牌复牌当天要特别小心:复牌那天通常会有补涨或补跌,价格波动很大。我一般会在复牌当天把这只股票的权重调低,等价格稳定了再恢复。
  • 涨跌停的成交量阈值:我习惯用「过去20日均量的20%」作为涨停日的可成交上限。低于这个量,就认为无法成交。
  • 数据对齐:不同股票的交易日期可能不一样(比如有的停牌了),做指数计算前一定要先对齐日期。我常用pd.DataFrame.reindex()来统一时间轴。
💡 一个小工具
我写了个函数叫 clean_index_data(),把上面三步封装在一起。输入原始日线数据和复权因子,输出清洗后的数据框。代码大概200行,核心逻辑就是上面讲的这些。如果你需要,可以自己封装一个,以后每次跑数据直接调用就行。

数据清洗这事,说白了就是「细节决定成败」。你花80%的时间处理数据,20%的时间写策略,这很正常。别嫌麻烦,数据干净了,后面的分析才能站得住脚。