3. 订单生命周期管理:订单状态机设计
订单状态机,说白了就是订单的“生死簿”。
从它诞生(New)到最终消亡(Filled、Cancelled、Rejected),每一步都得有据可查。我见过不少团队,一开始觉得状态机简单,结果上线后各种状态“穿越”,订单丢了都不知道怎么丢的。
今天咱们就把这个核心模块拆开揉碎,讲清楚。
3.1 为什么需要状态机?
你想想看,一个订单从发出到成交,中间可能经历什么?
- 刚发出去,交易所说“不行,格式错了”——Rejected
- 交易所说“好的,正在处理”——New
- 成交了一部分,还剩一点挂着——PartiallyFilled
- 全成交了——Filled
- 你突然想撤单——Cancelled
如果没有状态机,这些状态之间的转换就会乱套。比如一个已经Filled的订单,理论上不能再变成New。但代码里如果没做约束,就可能出现这种荒谬的事。
3.2 状态定义与转换规则
我个人习惯把订单状态分为两类:终态和非终态。
| 状态 | 类型 | 说明 |
|---|---|---|
| New | 非终态 | 订单已提交,等待交易所确认或成交 |
| PartiallyFilled | 非终态 | 部分成交,剩余部分仍在市场中等候 |
| Filled | 终态 | 全部成交,订单生命周期结束 |
| Cancelled | 终态 | 用户主动撤销,或系统因超时等原因撤销 |
| Rejected | 终态 | 交易所拒绝,订单无效 |
这里有个关键点:PartiallyFilled 是唯一一个可以多次进入的状态。比如一个大单,可能分10次成交,每次成交后状态还是PartiallyFilled,直到剩余量为0才变成Filled。
3.3 状态机核心逻辑
下面是我在实际项目中用过的状态机核心代码。嗯,这里要注意,状态机最好用枚举+状态转换表的方式实现,别用一堆if-else,否则维护起来会疯掉。
// 订单状态枚举
public enum OrderStatus {
New,
PartiallyFilled,
Filled,
Cancelled,
Rejected
}
// 状态转换表
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITION_MAP = new HashMap<>() {{
put(OrderStatus.New, new HashSet<>(Arrays.asList(
OrderStatus.PartiallyFilled,
OrderStatus.Filled,
OrderStatus.Cancelled,
OrderStatus.Rejected
)));
put(OrderStatus.PartiallyFilled, new HashSet<>(Arrays.asList(
OrderStatus.PartiallyFilled, // 允许继续部分成交
OrderStatus.Filled,
OrderStatus.Cancelled // 剩余部分可以撤单
)));
// 终态不允许任何转换
put(OrderStatus.Filled, Collections.emptySet());
put(OrderStatus.Cancelled, Collections.emptySet());
put(OrderStatus.Rejected, Collections.emptySet());
}};
public boolean canTransition(OrderStatus from, OrderStatus to) {
Set<OrderStatus> allowed = TRANSITION_MAP.get(from);
return allowed != null && allowed.contains(to);
}
这段代码看起来简单,但它是整个风控引擎的基石。我建议你把状态转换表做成可配置的,比如用YAML文件加载。这样以后要加新状态(比如“挂起”、“待重试”),改配置文件就行,不用动代码。
3.4 状态机流程图
光说理论不够直观,我画了一张状态机流转图,你看看就明白了。
从图上你能看到:New是唯一的入口,所有订单都得先经过New。然后根据交易所的反馈,走向不同的分支。PartiallyFilled可以自循环,这是高频交易里最常见的场景——一个大单被拆成无数个小单慢慢吃。
3.5 实战中的坑与对策
理论讲完了,说点实际的。我在搭建风控引擎时,踩过几个坑,分享给你。
同一个订单,可能同时收到成交回报和撤单回报。如果两个线程同时更新状态,就会出问题。
解决方案:用乐观锁或者单线程处理。我个人倾向用单线程的事件队列,把所有回报串行化处理。
系统崩溃后,内存里的状态全丢了。怎么办?
我建议每笔订单的状态变更都写WAL(Write-Ahead Log)。重启后从WAL恢复,保证状态不丢。
我曾经犯过一个低级错误:用浮点数计算剩余量。结果因为精度问题,明明成交完了,剩余量还显示0.0000001。
记住:所有金额和数量都用整数(最小单位),比如用“分”而不是“元”,用“股”而不是“手”。
3.6 状态机与风控的联动
状态机不只是记录状态,它还是风控的第一道防线。
举个例子:如果一个订单状态是PartiallyFilled,风控系统会检查它的未成交部分是否超过了某个阈值。如果超过,系统会自动触发撤单。
再比如,Rejected状态出现后,风控系统会记录拒绝原因。如果连续3次因为“资金不足”被拒,系统会暂时冻结该账户的订单提交权限。
这些逻辑都建立在状态机准确无误的基础上。所以,先把状态机搞扎实,后面的风控策略才能放心往上堆。
公众号:蓝海资料掘金营,微信deep3321