4. 订单管理模块设计:订单生命周期管理、订单状态机、撤单与改单逻辑、订单簿维护
做市系统的核心,说白了就是跟订单打交道。你策略算得再准,信号发得再快,如果订单管理这块掉链子,一切都是白搭。我见过不少团队,策略模型漂亮得很,结果上线第一天就因为订单状态没处理好,被交易所罚了个底朝天。
今天咱们就聊聊订单管理模块。这块我做了七八年,踩过的坑能写一本小册子。嗯,咱们一个一个来拆解。
4.1 订单生命周期:从生到死的完整轨迹
一个订单从诞生到消亡,经历哪些阶段?我习惯把它分成这几个关键节点:
- 创建(Created):策略模块发出指令,订单对象刚被实例化。此时它还没离开系统。
- 已发送(Sent):订单通过网络发往交易所。注意,这里只是“发出”,不代表交易所收到了。
- 已确认(Acknowledged):交易所返回了确认消息。这个状态很关键——只有到了这一步,订单才算真正“活着”。
- 部分成交(Partially Filled):订单被部分撮合,剩余数量还在挂单。
- 全部成交(Filled):订单完全成交,生命周期结束。
- 已撤销(Cancelled):主动撤单成功。
- 已拒绝(Rejected):交易所拒绝了这个订单,比如价格超出范围、数量不对等。
- 已过期(Expired):比如GTC(Good Till Cancelled)订单在收盘后被自动清除。
你可能会问:为什么要把“已发送”和“已确认”分开?
我在项目中遇到过网络闪断的情况。订单明明发出去了,但交易所的确认消息没回来。如果我把这两个状态合并,系统就会认为订单没发出去,然后重发——结果就是同一个订单被成交两次。嗯,这个教训挺深刻的。
4.2 订单状态机:别让状态乱飞
状态机是订单管理的骨架。我建议用有限状态机(FSM)来建模,每个状态之间的转换路径是严格定义的。乱跳状态?那是灾难。
下面是我常用的一个简化版状态机:
// 伪代码示例:订单状态机核心逻辑
enum OrderState {
CREATED,
SENT,
ACKNOWLEDGED,
PARTIALLY_FILLED,
FILLED,
CANCELLED,
REJECTED,
EXPIRED
}
class OrderStateMachine {
private OrderState currentState;
// 定义允许的转换
private Map<OrderState, List<OrderState>> transitions = Map.of(
CREATED, List.of(SENT, REJECTED),
SENT, List.of(ACKNOWLEDGED, REJECTED),
ACKNOWLEDGED, List.of(PARTIALLY_FILLED, FILLED, CANCELLED, REJECTED),
PARTIALLY_FILLED, List.of(PARTIALLY_FILLED, FILLED, CANCELLED),
FILLED, List.of(), // 终态
CANCELLED, List.of(), // 终态
REJECTED, List.of(), // 终态
EXPIRED, List.of() // 终态
);
public boolean transitionTo(OrderState newState) {
if (transitions.get(currentState).contains(newState)) {
currentState = newState;
return true;
}
// 我曾经在这里加过告警日志,线上出问题时排查效率极高
log.warn("非法状态转换: {} -> {}", currentState, newState);
return false;
}
}
你看,FILLED、CANCELLED、REJECTED、EXPIRED 都是终态。一旦进入终态,任何状态转换都不允许。为什么这么严格?
我曾经遇到过一个bug:某个订单已经全部成交了,但因为网络重传,系统又收到了一条成交回报,结果状态机把订单从FILLED又转到了PARTIALLY_FILLED。你想想看,这会导致什么后果?资金计算全乱套了。
4.3 撤单与改单逻辑:小心并发陷阱
撤单和改单,是订单管理里最容易出bug的地方。说白了,就是“你撤单的同时,交易所正在成交它”。
我个人的处理原则是:先确认,再操作。
具体来说:
- 撤单流程:先向交易所发送撤单请求,然后等待撤单确认。在收到确认之前,订单状态保持不变。如果撤单确认和成交回报同时到达,以成交回报为准——因为成交是事实,撤单只是请求。
- 改单流程:改单本质上是“先撤单,再下新单”。但要注意,有些交易所支持“改单”这个原子操作,有些则不支持。我建议统一用“撤单+下新单”的方式实现,这样兼容性最好。
这里有个细节:撤单请求发出后,如果超时没收到确认,怎么办?
嗯,这个问题没有标准答案。我自己的做法是:重查。主动向交易所查询该订单的状态,而不是盲目重发撤单请求。盲目重发可能导致重复撤单,有些交易所会报错,有些则不会——但后果都一样,你失去了对订单的控制。
4.4 订单簿维护:本地与交易所的同步艺术
订单簿(Order Book)是做市系统的命根子。你的策略决策,全部基于订单簿的数据。如果订单簿不准,策略就是瞎搞。
订单簿维护的核心挑战是:如何保证本地订单簿与交易所的订单簿实时一致?
我常用的方案是“增量更新 + 全量快照”双保险:
- 增量更新:监听交易所的行情推送,每收到一条成交或挂单变更,就更新本地订单簿。这种方式延迟低,但容易累积误差。
- 全量快照:每隔一段时间(比如30秒),从交易所拉一次完整的订单簿快照,用快照覆盖本地数据。这样可以把增量更新的误差清零。
你可能会问:增量更新和全量快照之间如果有冲突怎么办?
我建议以全量快照为准。增量更新只负责“快”,全量快照负责“准”。两者结合,才能又快又准。
下面是一个简化的订单簿维护逻辑:
class OrderBook {
// 买单:价格从高到低排序
private TreeMap<Double, Level> bids = new TreeMap<>(Comparator.reverseOrder());
// 卖单:价格从低到高排序
private TreeMap<Double, Level> asks = new TreeMap<>();
// 增量更新
public void applyIncrementalUpdate(OrderBookUpdate update) {
// 更新买单或卖单的某个价格档位
// 如果数量为0,则移除该档位
// 我曾经因为忘记处理数量为0的情况,导致订单簿里残留了空档位
}
// 全量快照覆盖
public void applySnapshot(OrderBookSnapshot snapshot) {
// 直接清空本地数据,用快照替换
this.bids.clear();
this.asks.clear();
// 填充快照数据
}
}
4.5 整体架构图
说了这么多,咱们用一张图把整个订单管理模块串起来。这张图是我自己项目里用的简化版,但核心逻辑是一样的。
这张图里,订单管理器是核心枢纽。它接收策略模块的指令,经过风控检查后,通过交易所网关发往交易所。同时,它维护着本地订单簿,并把订单簿数据反馈给策略模块。所有操作都会被记录到订单日志中,方便事后审计。
嗯,订单管理模块的设计,说白了就是“状态清晰、转换可控、数据一致”。把这三点做好,系统就稳了一大半。