第四章:订单管理模块

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 订单簿快照重建

订单簿快照重建,说白了就是断线重连后,怎么把订单簿恢复到最新状态。这个问题我踩过坑,所以特别想说说。

交易所通常提供两种数据:增量更新和全量快照。增量更新就是告诉你「有人撤单了」「有人挂单了」,全量快照就是直接把整个订单簿发给你。

重建的逻辑其实不复杂:

  1. 收到全量快照,清空本地订单簿,用快照数据填充
  2. 然后开始接收增量更新,逐条应用
  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]
  ]
}

你看,价格从浮点数变成了整数,而且去掉了字段名,数据量直接减半。如果再配合差分编码,还能进一步压缩。

个人经验:我建议在系统设计初期就把数据压缩考虑进去。不要等到数据量大了再优化,那时候改起来成本很高。而且压缩算法要选对,有些算法压缩率高但解压慢,不适合高频场景。

知识体系总览

下面这张图展示了订单管理模块的核心逻辑和各个部分之间的关系:

订单管理模块知识体系 订单生命周期管理 订单类型与路由 订单簿快照重建 订单簿数据压缩 状态机管理 状态转换校验 限价单/市价单 冰山单/止损单 智能路由策略 全量快照接收 增量更新应用 序列号校验 价格精度压缩 增量压缩 数值编码

这张图把四个核心模块串起来了。订单生命周期管理负责状态流转,订单类型与路由决定怎么发、发哪里,订单簿快照重建保证数据一致性,数据压缩则解决性能问题。它们环环相扣,缺一不可。

专注资料整理