Skip to content

Latest commit

 

History

History
39 lines (20 loc) · 4.06 KB

README.md

File metadata and controls

39 lines (20 loc) · 4.06 KB

matching

(这是一个实验项目,请勿在现实中使用!)

撮合引擎简介

在一个交易市场中,买卖双方会按各自意愿出价,下不同价格/数量的订单,而撮合引擎就是交易所中,让价格合适的订单相互匹配成交的程序。

在撮合引擎中,对某个市场(Market)会维护两个订单册(OrderBook):买方订单册(bid_order_book)和卖方订单册(ask_order_book),分别存储双方的订单。当有新的订单被下单时,引擎会找这个订单的对手订单册,看看这个订单册里有没有能与新订单匹配的,如果有,就消耗这两个订单的数量,耗空的移除出系统;如果没有或新订单还有剩余,就把新订单添加到订单册中。

因此,我们只需要在新订单加入时进行匹配,平时两个订单册将会维持互无交集的状态。

订单类型

我们的订单类型分为两种,限价单(OrderKind::Limit)和市价单(OrderKind::Market)。限价单就是只在有价格合适的对手单时成交,市价单是按对手的价格成交,所以市价单能尽快成交。

市价单的匹配比较简单,只要按顺序匹配就好了,所以我们只需要一个队列来做市价单订单册。当新订单到来时,从市价单队列中出队一个,直接成交即可。

而对于限价单,我们需要让新进入的订单和价格最优的对手单逐一匹配。对于买单而言,价格最高的是最优的;对于卖单而言,价格最低的是最优的。如果最优订单无法成交,其他订单也不可能成交了,新订单便会被放入订单册。对此,我们需要一个优先队列,让订单按价格优先程度顺序出队。

数据结构

有许多数据结构可以满足优先队列的要求,例如红黑树和二分堆,二者理论上的性能差距不是很大。这次我们用二分堆实现,以后可以再做一个红黑树实现,对比一下性能。

在撮合引擎里,我们需要以下三种操作:下单(添加元素)、成交(获取最优元素)、撤单(随机删除元素),红黑树对三种操作的时间复杂度都是O(log n),而二分堆的时间复杂度分别为O(log n)、O(1)、O(n)。二分堆撤单需要O(n)是因为,二分堆里想随机找出一个元素,必须遍历整个堆。然而这一点可以用一个HashMap进行优化,只要在HashMap里维护好每个元素的引用,就可以在O(log n)的复杂度内完成删除。最终,堆完成三项操作的时间复杂度是O(log n)、O(1)、O(log n)。

红黑树在获取最优元素的时候,虽然需要O(log n),但是在实际使用的时候,最优元素可以缓存起来的,不需要时时获取,所以影响应该不会很大。

对于市价单,我们可以使用链表+HashMap来实现。链表的下单和成交都是O(1),而用HashMap维护一下元素的引用,可以让撤单也O(1)完成,这个数据结构应该已经是最优的了。

实现

撮合引擎的堆实现参考了Rust标准库的BinaryHeap,原理可以参照网上的文章。BinaryHeap只基于一个Vec,唯一需要的unsafe操作是swap,直接用Vec::swap就好了,也不需要自己写。我们的Heap里增加了一个HashMap<u64,usize>,记录Order#id到Vec中元素的索引。删除操作时,只需要先找到索引,将对应元素和Vec尾部元素交换,弹出尾部元素,然后再对原位置的元素执行sift_up或sift_down操作即可,堆将回复有序。

实现队列的时候,我简单地把订单以Order#id => Node都存在HashMap中,然后实现一个存放管理id的链表,对链表的读写最终会反映在HashMap里。这样也许不是最优的,只是为了避免写unsafe代码,将容易复制的整数id作为键使用了,以后可以继续优化。

性能

目前经过粗略地测试(包含了JSON解析的时间),这个rust实现的撮合引擎,在我本地可以达到36w订单一秒,这个数字应该还有很大的优化空间,Github上搜到的同类产品可以达到百万以上的处理量。