← 返回文章列表 数据工程

复权、停牌与退市:把行情数据洗干净的清单

六个最常见的数据陷阱,以及每一个对应的检查代码。跑完这份清单再开始建模。

六个陷阱

在开始任何建模之前,先花半天把下面六件事确认一遍。它们造成的偏差通常比模型选择带来的差异大得多。

# 复权:用后复权做研究,用不复权价算真实成交额
adj = raw['close'] * raw['adj_factor']

# 停牌:先标记,再决定怎么处理,不要直接 ffill 了事
suspended = raw['volume'].eq(0) | raw['close'].isna()
print(suspended.groupby(raw['code']).sum().describe())

# 短停牌向前填充,长停牌整段剔除
filled = raw.groupby('code')['close'].ffill(limit=5)

为什么是 limit=5

连续停牌超过一周的标的,复牌当天的价格跳变已经不是「延续」而是「重定价」。向前填充只适合覆盖临时停牌,长停必须整段标记为不可交易。

停牌怎么填才不作弊

关键不是「填什么值」,而是「填完之后这一天能不能交易」。研究里最常见的错误是价格填上了、权重也分配了,回测里却买入了一个当天根本无法成交的标的。

tradable = (~suspended) & raw['amount'].gt(1e6)

# 权重先算,再按可交易掩码归零并重新归一
w = w.where(tradable, 0.0)
w = w.div(w.sum(axis=1).replace(0, pd.NA), axis=0).fillna(0)

# 涨跌停也不可买入:用当日最高价 == 最低价近似识别一字板
limit_up = raw['high'].eq(raw['low']) & raw['close'].gt(raw['pre_close'])

退市股一定要留着

把退市股删掉是最省事也最致命的做法。一个在 2018 年买了 30 只股票的策略,如果其中 3 只后来退市而数据里没有它们,回测收益会凭空多出十几个点。正确做法是保留全部历史标的,用一张 listing 表记录每个代码的上市日和退市日,回测时按日期切片。

下一篇我们把这份清单写成一组 pytest 断言,每次数据更新后自动跑一遍。

数据清洗复权幸存者偏差
下一篇

用 cvxpy 做带约束的权重优化