4. 紧急撤单基础
在交易系统的 Kill Switch 体系中,紧急撤单不是“点一下按钮”这么简单。它是一条从策略层、风控层、柜台接口到交易所撮合引擎的完整链路。理解撤单的类型、优先级、延迟构成以及失败原因,是设计可靠 Kill Switch 的前提。本节将拆解紧急撤单的底层机制,帮助你在真实故障场景中做出正确判断。
一、撤单类型:按对象与范围划分
紧急撤单并非只有一种形态。根据撤单对象和触发范围,通常分为以下四类:
- 单笔撤单(Order Cancel):针对某一笔未成交订单发起撤销,是最细粒度的操作,常用于策略主动纠错。
- 批量撤单(Batch Cancel):按合约、账户或策略分组,一次性撤销一组订单,适合策略整体下线。
- 全市场撤单(Cancel All):撤销当前交易账户下所有未成交订单,是 Kill Switch 最常用的动作。
- 撤单并禁止新开(Cancel & Block):撤单同时冻结下单权限,防止策略在撤单后继续发单,属于“撤单+熔断”组合。
二、撤单优先级:谁先被撤掉
当系统需要在极短时间内撤销大量订单时,撤单顺序直接影响风险暴露。交易所和柜台通常不会保证“先发先撤”,因此需要在本地维护撤单优先级。常见优先级策略如下:
| 优先级 | 订单特征 | 理由 |
|---|---|---|
| P0 | 已部分成交且剩余量大 | 剩余敞口最大,继续成交风险最高 |
| P1 | 距离盘口最近(买一/卖一) | 最容易被立即成交,撤单窗口最短 |
| P2 | 大单、冰山单、算法子单 | 单笔影响大,撤单后对市场冲击明显 |
| P3 | 远价挂单、长期未成交 | 成交概率低,可最后处理 |
在代码层面,撤单优先级通常体现为排序键。例如按“剩余量降序 + 价格距离升序”生成撤单队列:
def cancel_priority(order):
# 剩余量越大越优先,价格越接近盘口越优先
return (-order.remaining_qty, abs(order.price - order.mid_price))
cancel_queue = sorted(open_orders, key=cancel_priority)
for order in cancel_queue:
cancel(order.order_id)
三、撤单延迟:从点击到确认的完整链路
撤单延迟不是单一数值,而是多个环节的累加。理解每一段延迟,才能定位“为什么撤不掉”。
从图中可以看到,撤单延迟的主要不确定性来自网关排队、网络抖动和交易所限流。在 Kill Switch 场景中,必须假设最坏情况下的延迟,而不是平均延迟。
四、撤单失败原因与影响
撤单失败并不罕见,尤其在极端行情下。常见原因包括:
- 订单已成交:撤单请求到达时订单已完全成交,返回“订单不存在”或“已成交”。
- 订单已撤销:重复撤单,交易所返回“已撤销”状态。
- 撤单频率超限:交易所对撤单有每秒次数限制,超限后请求被拒绝。
- 会话失效:API 过期、连接断开,撤单请求无法送达。
- 合约状态异常:如合约进入交割、停牌或临时风控状态,禁止撤单。
- 本地状态不一致:本地认为订单未成交,但交易所已成交,导致撤单逻辑误判。
因此,撤单后必须等待交易所回报确认,而不是以“请求已发送”作为成功依据。建议在 Kill Switch 中引入撤单确认超时机制:
cancel_all()
wait_until(timeout=3s, condition=all_orders_terminal)
if not all_orders_terminal:
escalate_to_hard_kill() # 强制断网、冻结账户、人工介入
五、案例复盘:一次撤单延迟导致的敞口扩大
某量化团队在夜盘突发行情中触发 Kill Switch,系统按优先级批量撤单。但由于网关排队,前 200ms 内仅撤掉 30% 订单,剩余订单在行情快速波动中继续成交,最终敞口比预期扩大 2.3 倍。复盘发现两个问题:一是撤单队列未按“盘口距离”排序,远价单先被处理;二是未设置撤单确认超时,系统在未确认全部撤单成功的情况下就解除了新开限制。
改进措施包括:将撤单优先级改为“盘口距离优先”,增加撤单确认超时与升级路径,并在 Kill Switch 触发后立即冻结策略下单权限,直到所有订单进入终态。这一案例说明,紧急撤单的核心不仅是“发出去”,更是“确认撤掉”和“防止再发”。