各位 Quant 朋友,今天聊一个细节但致命的问题:回测和实盘中,你是怎么处理停牌股票的行情的? 是不是经常发现某些停牌票的“最新价”还留在时间序列里,拉偏了整个板块的收益率?我最近重构一个高频选股策略时就重点优化了这一块,从客户需求、投顾痛点一路到具体实现,给大家做个分享。
客户与策略的需求:干净的行情输入
我们做量化,最怕输入数据有“脏东西”。停牌票的价格如果不剔除,会直接污染因子计算和组合表现。例如在市值因子、动量因子中混入一个停牌不变的数值,信号就会失真。对投顾型策略输出而言,客户也要求推荐池子里不能有不可交易的标的,否则合规和体验都成问题。
投顾/策略开发者的真实痛点
传统的行情数据,有些源并不提供明确的停牌标识。我就碰到过一个接口,停牌后成交量的确不动了,但价格还在每秒推送,甚至买卖盘口遗留了停牌前的挂单。回测时没注意,选股池里混进一只停牌股,结果用市价单模拟无法成交,信号回测偏差大到离谱。而实盘监控里,如果订阅几千只股票,停牌票持续消耗 WebSocket 流量,CPU 占用也会无谓升高。
数据支撑:停牌股票的接口特征
我梳理过多个数据源在停牌期间的行为,基本覆盖以下几种:
| 接口特征 | 判断方式 | 量化策略里的应用 |
|---|---|---|
| 最新价冻结 | 价格序列方差为零 | 不能单独作为停牌依据 |
| 成交量持续为零 | 连续N分钟无成交量 | 可作为辅助过滤 |
状态字段suspend |
布尔值直接判断 | 可靠性最高,建议优先使用 |
| 时间戳连续但无交易 | 序列有更新时间 | 直接忽略时间戳 |
显然,有状态字段的接口对量化最友好。
我的过滤升级方案
我现在实盘和回测都在用的三层架构,效果很好。
数据清洗层:在接收数据后,立即对每条 tick 检查状态字段;若无,则用“成交量 == 0 且 last_price 连续 N 秒不变”作为备用规则,打上停牌标签。
因子计算层:所有计算因子的函数,都先执行 df = df[df['suspend'] == False],保证输入干净。
订阅管理层:在 WebSocket 回调里,一旦识别出停牌,就动态取消对该标的的订阅,释放带宽。我目前使用的实时数据源(如 AllTick)直接提供 suspend 字段,让这步操作十分轻量。
import websocket
import json
def on_message(ws, message):
data = json.loads(message)
for tick in data.get("ticks", []):
if tick.get("suspend"): # 判断是否停牌
print(f"{tick['symbol']} 停牌中")
else:
print(f"{tick['symbol']} 最新价: {tick['last_price']}")
ws = websocket.WebSocketApp("wss://api.alltick.co/stock", on_message=on_message)
ws.run_forever()
接口差异与适配
不同行情 API 差异挺大,有些停牌就断流,有些价格照推但盘口为空。我习惯在接入新数据源时,先跑一天观察,把股票分三类:活跃、停牌、新上市/退市,用不同管道处理。停牌处理本质上是数据清洗的第一关,把这关做好了,策略的鲁棒性会显著提升。


