第四章:订单管理模块
4.1 订单生命周期管理
订单管理,说白了就是管好每一笔订单从生到死的全过程。我做了这么多年系统,最深的感触就是:订单生命周期管理做不好,后面所有策略都是空中楼阁。
一个订单从创建到成交或撤销,会经历几个关键状态。我习惯把它分成这样几个阶段:
- 创建(Created):订单刚生成,还没发出去。这时候可以修改或取消。
- 已发送(Sent):订单已经推到交易所了,但还没收到确认。
- 已确认(Acknowledged):交易所确认收到了。这时候订单进入排队状态。
- 部分成交(Partially Filled):成交了一部分,剩下的还在排队。
- 全部成交(Filled):全成了,订单生命周期结束。
- 已撤销(Cancelled):被手动或自动取消了。
- 已拒绝(Rejected):交易所认为订单无效,比如价格超出范围。
嗯,这里要注意:状态机设计一定要严谨。我在项目中遇到过一个问题——订单状态回滚。比如交易所先确认了订单,然后又发来一个拒绝消息。这种情况虽然少见,但必须处理。
核心原则:订单状态只能向前推进,不能回退。如果出现矛盾状态,以交易所最新消息为准。
我建议用有限状态机(FSM)来管理。每个状态定义好合法的下一个状态,非法转换直接报错。代码实现大概是这样:
class OrderStateMachine:
def __init__(self):
self.state = 'Created'
self.valid_transitions = {
'Created': ['Sent'],
'Sent': ['Acknowledged', 'Rejected'],
'Acknowledged': ['PartiallyFilled', 'Filled', 'Cancelled'],
'PartiallyFilled': ['PartiallyFilled', 'Filled', 'Cancelled'],
'Filled': [],
'Cancelled': [],
'Rejected': []
}
def transition(self, new_state):
if new_state in self.valid_transitions[self.state]:
self.state = new_state
else:
raise InvalidStateTransition(f"不能从{self.state}转到{new_state}")
4.2 订单类型与路由
订单类型这块,说实话,不同交易所支持的品种差别挺大。但核心就那么几种,你搞懂了就能应付大部分场景。
| 订单类型 | 说明 | 适用场景 |
|---|---|---|
| 限价单(Limit) | 指定价格,不高于(买)/不低于(卖)这个价就成交 | 做市商常用,控制成本 |
| 市价单(Market) | 不指定价格,按当前最优价立即成交 | 需要快速成交时用 |
| 冰山单(Iceberg) | 只显示部分数量,隐藏真实总量 | 大单拆小,避免暴露意图 |
| 止损单(Stop) | 价格触发后变成市价单 | 风控止损 |
路由策略就更有意思了。你想想看,一个订单发到哪个交易所,什么时候发,这些都是学问。我一般会考虑这几个因素:
- 流动性:哪个交易所深度好,优先发哪里
- 延迟:网络延迟低的交易所,成交更快
- 费用:不同交易所手续费不一样,做市商特别在意这个
- 订单簿状态:当前买卖盘情况,决定挂单还是吃单
小技巧:我曾经做过一个智能路由模块,它会根据实时市场数据动态调整路由策略。比如某交易所突然延迟变高,系统自动把订单切到备用交易所。这个逻辑写起来不复杂,但效果很明显。
4.3 订单簿快照重建
订单簿快照重建,说白了就是断线重连后,怎么把订单簿恢复到最新状态。这个问题我踩过坑,所以特别想说说。
交易所通常提供两种数据:增量更新和全量快照。增量更新就是告诉你「有人撤单了」「有人挂单了」,全量快照就是直接把整个订单簿发给你。
重建的逻辑其实不复杂:
- 收到全量快照,清空本地订单簿,用快照数据填充
- 然后开始接收增量更新,逐条应用
- 如果发现增量序号不连续,说明中间有丢失,需要重新请求快照
嗯,这里有个关键点:快照和增量的时序问题。我遇到过这样的情况——先收到了增量更新,后收到快照。如果直接应用增量,会把快照之前的数据也加进去,导致订单簿错乱。
避坑指南:我曾经因为没处理好时序,导致订单簿多了一堆不存在的订单。后来我加了个序列号校验,增量更新的序列号必须大于快照的序列号才应用。这个逻辑虽然简单,但能避免很多麻烦。
代码实现大致是这样:
class OrderBookRebuilder:
def __init__(self):
self.order_book = {}
self.last_snapshot_seq = 0
self.last_update_seq = 0
def apply_snapshot(self, snapshot, seq_num):
if seq_num > self.last_snapshot_seq:
self.order_book = snapshot
self.last_snapshot_seq = seq_num
self.last_update_seq = seq_num
def apply_update(self, update, seq_num):
if seq_num > self.last_update_seq:
# 应用增量更新
for change in update:
if change['type'] == 'add':
self.order_book[change['price']] = change['quantity']
elif change['type'] == 'delete':
self.order_book.pop(change['price'], None)
self.last_update_seq = seq_num
else:
# 序列号过期,丢弃
pass
4.4 订单簿数据压缩
数据压缩这块,很多人觉得不重要。其实不然。你想想看,高频交易每秒产生几千条订单簿更新,如果不压缩,带宽和存储都是问题。
我常用的压缩方法有几种:
- 价格精度压缩:很多交易所的价格精度很高,但实际交易中,只有少数几个价格档位有订单。可以只存储有数据的档位。
- 增量压缩:只记录变化的部分,不记录全量。这个配合快照重建使用。
- 数值编码:用整数代替浮点数,减少存储空间。比如价格乘以10000后存整数。
- 差分编码:记录相邻价格档位的差值,而不是绝对值。因为相邻档位的价格差通常是固定的。
实际效果:我在一个项目中用了这些方法,订单簿数据从原来的每秒钟500KB压缩到了50KB左右,压缩比达到10:1。而且解压速度很快,不影响交易延迟。
举个例子,原始订单簿数据可能是这样:
{
"bids": [
{"price": 100.01, "quantity": 1000},
{"price": 100.00, "quantity": 2000},
{"price": 99.99, "quantity": 1500}
],
"asks": [
{"price": 100.02, "quantity": 800},
{"price": 100.03, "quantity": 1200},
{"price": 100.04, "quantity": 900}
]
}
压缩后可以变成:
{
"bids": [
[10001, 1000],
[10000, 2000],
[9999, 1500]
],
"asks": [
[10002, 800],
[10003, 1200],
[10004, 900]
]
}
你看,价格从浮点数变成了整数,而且去掉了字段名,数据量直接减半。如果再配合差分编码,还能进一步压缩。
个人经验:我建议在系统设计初期就把数据压缩考虑进去。不要等到数据量大了再优化,那时候改起来成本很高。而且压缩算法要选对,有些算法压缩率高但解压慢,不适合高频场景。
知识体系总览
下面这张图展示了订单管理模块的核心逻辑和各个部分之间的关系:
这张图把四个核心模块串起来了。订单生命周期管理负责状态流转,订单类型与路由决定怎么发、发哪里,订单簿快照重建保证数据一致性,数据压缩则解决性能问题。它们环环相扣,缺一不可。