前面写委托状态机时埋了个引子:撤单和成交会赛跑。这篇把撤单当主角。它看起来是交易的逆操作,"没成交就退掉"——但撤单是写操作,要动订单簿、要并发控制、要处理和成交的时序竞争,还要面对量化策略带来的撤单率风暴。A 股还有个特殊背景:没有改单,改价改量 = 撤单 + 新报两笔,这让撤单量天然是改单市场的两倍。
撤单的完整链路
一笔撤单从客户按键到确认,走完整条交易链路:
- 接入与校验:网关收撤单请求,验会话、验该委托存在且属于此客户;
- 状态预检:委托当前状态必须是可撤的(已报/部成有余量)。已废、已撤、已成的撤单请求直接拒绝,拒绝原因码要精确——"已成交"和"不存在"是两种完全不同的拒绝,客户的策略对两者的反应不同;
- 送撮合核心:撤单请求进入撮合线程的输入队列。这里是撤单成本的第一个来源:撮合是单线程的,撤单要和委托、成交在同一个串行队列里排队。行情火爆时队列前面堆着几百个委托,撤单排在后面干着急——客户感受到的"撤单慢"其实是撮合队列的拥塞;
- 簿内删除:O(1) 摘除(order_index 哈希定位 + 双向链表摘除),更新档总量,该档清空则摘树;
- 回报与解冻:发出撤单确认回报,柜台侧解冻剩余冻结资金/持仓。
赛跑的正确裁决方式
撤单和成交谁赢,答案只能由一个权威点给出:撮合线程内的处理顺序。这是单线程撮合红利的一部分——所有状态变更在一个线程内串行发生,"先撤先成交"有了唯一语义。具体两条纪律:
- 撮合线程读到撤单请求时,若该委托还在簿上 → 删除,发撤单确认;
- 读到撤单时委托已被之前的成交清空 → 撤单拒绝(原因码"已成交"),按成交走结算。
柜台侧绝对不能自作主张做"预撤":客户端点了撤单,柜台先把内存状态改成"已撤"再通知撮合——这是双账本竞争的教科书错误。正确的"撤单中"是一个本地标志位,只影响 UI 展示和重复撤单请求的拦截,不触发任何账务动作。所有账务后果(解冻)都等撮合/交易所的撤单确认回报,这是状态机篇"迁移即账务、账务随确认"的又一个实例。
撤单率:一个被低估的容量变量
撮合系统的容量规划通常按"峰值委托笔数"算,这是不够的。撤单在队列里的成本和委托等量齐观——都要进同一个 FIFO 排队,都要占撮合循环的一拍。而量化策略的撤单率(撤单笔数 / 委托笔数)可以达到 70% 以上:排队位置靠后、成交希望渺茫的单,策略会主动撤掉重报。极端行情下(涨跌停排队),撤单量和委托量一起爆炸,撮合循环的总处理量接近委托量的两倍。
更微妙的是撤单的微突发:策略们对同一信号(比如盘口变薄)同时反应,几百个撤单在毫秒级内涌入。平均容量的规划在这里失效——系统要在 P99 乃至 P99.9 延迟上扛住突发。工程对策:
- 队列分级:部分极速柜台给撤单独辟队列或与委托交错消费,保证"想撤的人不必排在想吃单的人后面等太久"——注意这是调度策略的优化,不破坏串行裁决(仍在同一撮合线程内);
- 撤单限流:按客户/账户的撤单速率设阈值,超限延迟处理或直接拒——交易所本身就有撤单比考核(撤单率过高的账户被限制),柜台侧限流是自保;
- 容量按"委托 + 撤单 + 回报"三者之和规划,任何单指标的容量论证在评审时都值得打回去。
改单:另一种语义秩序
港美股市场支持改单(amend/replace):改价改量是一个簿内原地操作——委托保持它在队列里的时间优先位次(或按规则重排),订单号不变。这和 A 股的撤单重报有本质差别:撤单重报的新委托丧失时间优先(重新排队),改单保留位次。同一个策略在 A 股和港股跑,下单逻辑要分别实现,因为"改价"在两个市场的语义完全不同。
改单的系统实现也暴露了它的复杂度来源:簿内原地修改意味着订单簿的所有索引(order_index、档位队列、总量字段)要支持原子更新,且改单请求本身要和撮合循环竞争——它和撤单一样是写操作,一样要进串行队列。所以改单并没有比撤单更省撮合资源,它省的是网络往返和委托序号(一笔消息代替两笔),对高频策略来说省的是报盘带宽和风控检查次数。A 股用撤单重报换来规则的简单(没有改单的簿一致性状态,没有部分改单的重排规则),代价是流量翻倍——这是规则设计向系统简单性倾斜的典型例子,理解了这一层,就理解了为什么不同市场看起来"差不多的功能"背后是完全不同的系统。
撤单这件事的总结论:它是写操作,写操作就要排队,排队就有成本,成本就要计价。把撤单当"免费取消"来设计容量和计费的系统,会在第一个极端交易日付出学费。