第四章 订单管理系统:订单类型、状态机与订单簿维护
好,咱们今天聊聊订单管理系统。说实话,这是整个回测引擎里最容易被低估的模块。很多人觉得不就是下个单嘛,有什么难的?但我在做第一个回测系统时,就被订单状态机狠狠坑了一把——回测结果和实盘对不上,查了三天才发现是订单状态流转少了一个分支。
订单管理系统,说白了就是模拟交易所怎么处理你的交易指令。它要回答三个问题:能下什么单?单子现在什么状态?单子怎么排队?咱们一个一个来拆。
4.1 订单类型:市价单 vs 限价单
先看最基础的两种订单类型。你想想看,交易的时候最关心什么?无非是「能不能成交」和「以什么价格成交」。这两个需求,正好对应了市价单和限价单。
4.1.1 市价单(Market Order)
市价单,说白了就是「我不管价格,给我立刻成交」。它的核心特征是:成交优先级最高,但成交价格不确定。
我在项目中遇到过这样的情况:回测时用市价单,假设总能以当前最新价成交。但实盘时,如果订单簿深度不够,市价单可能会吃掉好几个档位的挂单,实际成交均价远高于预期。嗯,这里要注意——回测中一定要模拟订单簿的深度,否则市价单的滑点会被严重低估。
- 方向:买入 / 卖出
- 数量:以股数或合约数为单位
- 成交规则:按对手方挂单价格,从最优价开始逐档成交,直到数量满足
4.1.2 限价单(Limit Order)
限价单就温和多了。它说「我只接受某个价格,达不到我就等着」。核心特征是:成交价格确定,但成交时间不确定,甚至可能永远不成交。
我个人习惯在回测中把限价单分成两类:
- 被动限价单:挂单后不主动撤单,等着别人来吃。这种单子会进入订单簿,成为流动性提供者。
- 主动限价单:虽然挂了限价,但如果当前市场价已经优于或等于限价,就直接按市价成交,不进入订单簿。
为什么要区分?因为被动限价单能赚到maker返佣,而主动限价单要付taker费用。回测时如果不区分,费用计算就会出错。我曾经因为这个差异,导致一个高频策略的回测年化收益差了8%。
order_type 来区分市价单和限价单,再用一个字段 time_in_force 来控制限价单的有效期(如GTC、IOC、FOK)。这样后续扩展其他订单类型(如止损单、冰山单)时,结构不会乱。
4.2 订单状态机:从生到死的完整生命周期
订单状态机,是订单管理系统的核心。它定义了订单从创建到最终完成的所有可能路径。我见过不少新手直接用一个枚举变量来存状态,然后在代码里到处写if-else判断。结果就是状态流转逻辑散落一地,改一个地方崩三个地方。
正确的做法是:用状态机模式(State Pattern)来管理。咱们先看一张完整的订单状态流转图:
这张图里,已创建 → 已提交 → 部分成交 → 全部成交 是主路径。但要注意,部分成交后可能继续挂单等待,也可能被撤销或拒绝。我建议在代码里用状态转移表来定义所有合法路径,而不是靠if-else硬编码。
4.3 订单簿维护:模拟交易所的撮合逻辑
订单簿,就是交易所用来记录所有未成交限价单的账本。它分两部分:买单队列(Bid)和卖单队列(Ask)。买单按价格从高到低排,卖单按价格从低到高排。价格相同的情况下,按时间优先。
在回测中,订单簿的维护逻辑其实不复杂,但性能要求很高。因为每一笔tick数据进来,都可能触发订单簿的更新和撮合。我见过有人用Python列表来存订单簿,结果回测一天的数据要跑半小时。后来改成用红黑树(SortedDict),速度提升了两个数量级。
4.3.1 订单簿的核心操作
订单簿维护就四个操作:
- 添加订单:新限价单进入队列,按价格-时间排序
- 撤销订单:从队列中移除指定订单
- 修改订单:先撤销原单,再添加新单(价格变化时)
- 撮合订单:检查买单队列最高价和卖单队列最低价,如果买单价格 ≥ 卖单价格,就成交
这里有个细节:修改订单时,如果只改数量不改价格,可以不用重新排队。但为了简化,我建议统一走「撤销+添加」的流程。虽然性能差一点,但逻辑清晰,不容易出bug。
4.3.2 撮合逻辑的代码实现
咱们来看一段简化的撮合逻辑代码。这是回测引擎的核心,我建议你手敲一遍:
class OrderBook:
def __init__(self):
self.bids = SortedDict(lambda k: -k) # 买单,价格降序
self.asks = SortedDict() # 卖单,价格升序
self.order_map = {} # 订单ID到订单对象的映射
def match_order(self, order):
"""撮合一个订单,返回成交记录列表"""
trades = []
if order.side == 'buy':
# 买单:从最低卖单开始吃
while order.remaining > 0 and self.asks:
best_ask_price, ask_queue = self.asks.peekitem(0)
if order.price < best_ask_price:
break # 限价单价格不够,停止撮合
# 逐单成交
while ask_queue and order.remaining > 0:
ask_order = ask_queue[0]
trade_qty = min(order.remaining, ask_order.remaining)
# 记录成交
trades.append(Trade(
price=best_ask_price,
quantity=trade_qty,
buyer=order.id,
seller=ask_order.id
))
order.remaining -= trade_qty
ask_order.remaining -= trade_qty
if ask_order.remaining == 0:
ask_queue.popleft()
if not ask_queue:
self.asks.popitem(0)
else:
# 卖单:从最高买单开始吃(逻辑对称)
# ... 省略,与买单逻辑对称
return trades
3.3 订单簿的深度模拟
回测中一个常见的坑是:假设订单簿无限深。也就是说,市价单总能以当前最优价成交,想买多少就买多少。这在实盘中是不可能的。
正确的做法是:维护一个真实的订单簿深度。你可以从历史数据中重建订单簿,或者用统计模型生成模拟的订单簿。我个人习惯用分层深度模型——把订单簿分成10个价格档位,每个档位挂单量按历史分布随机生成。
举个例子,如果当前最优卖价是100元,第二档是100.1元,第三档是100.2元。一个买入1000股的市价单,可能吃掉最优档的300股,第二档的500股,第三档的200股。成交均价就是(100*300 + 100.1*500 + 100.2*200) / 1000 = 100.09元。这个滑点,比直接用100元成交要真实得多。
- 用有序数据结构(如红黑树)存储买卖队列
- 撮合时按价格优先、时间优先原则
- 市价单要模拟订单簿深度,不能假设无限流动性
- 限价单要区分主动成交和被动挂单,影响费用计算
好了,订单管理系统就聊到这儿。记住,这个模块是回测引擎的「心脏」,它的正确性直接决定了回测结果的可信度。下一章咱们会聊到仓位管理和资金曲线计算,到时候你会发现,订单管理系统输出的成交记录,就是仓位管理的原材料。
公众号:蓝海资料掘金营,微信deep3321