前言
自从有人在微信群里开价5万求购Golang版的撮合引擎之后,我就想自己开发一款,毕竟,以我的经验来说,开发个高性能的撮合引擎并没什么难度。
说干就干,于是,利用业余时间慢慢开发出了一款Golang版的高性能撮合引擎,前前后后花了大概一个月的时间。再想想自己好久没更新文章了,我的个人IP都已经生锈了,也应该发大招磨一磨了。因此决定,干脆就以连载的方式,分享下我是如何设计与实现这款价值超5万的撮合引擎的。
本来,想发成掘金小册,收点稿费,毕竟这是个具有很大商业价值的软件,但问了掘金的人员,他们目前不接收这类主题。最终决定免费发布,还可以多发几个渠道,说不定还能给我多带来些关注量。
好了,下面开始进入撮合引擎系列的正题。
撮合引擎简介
撮合引擎是所有撮合交易系统的核心组件,不管是股票交易系统——包括现货交易、期货交易、期权交易等,还是数字货币交易系统——包括币币交易、合约交易、杠杆交易等,以及各种不同的贵金属交易系统、大宗商品交易系统等,虽然各种不同交易系统的交易标的不同,但只要都是采用撮合交易模式,都离不开撮合引擎。
撮合引擎是可以具有通用性的,一套具有通用性的撮合引擎实现理论上可以应用到任何撮合交易系统中,而无需做任何代码上的调整。即是说,同一套撮合引擎实现,既可以应用在股票交易系统,也可以应用在数字货币交易系统,可以用于现货交易,也可以用于合约交易等。
那么,一套具有通用性的撮合引擎应该具备哪些功能呢?确定该问题的答案之前,我们先简单梳理一下一个完整的交易流程是怎样的?一般会包括以下步骤:
- 系统开放某个交易标的的交易功能。
- 用户提交该交易标的的买卖申报,即委托单。
- 系统验证委托单是否有效,包括交易标的是否处于可交易的状态、订单的价格和数量是否符合要求等。
- 确定该委托单的**挂单(Maker)费率和吃单(Taker)**费率。
- 检查用户的资产账户情况,包括账户状态是否交易受限,是否有足够资金用于下单等。
- 将详细的委托单数据持久化到数据库,并冻结用户账户中相应数量的资金。
- 将委托单进行撮合处理,即在**交易委托账本(OrderBook)**中寻找能与该委托单匹配成交的订单,匹配的结果可能是:全部成交、部分成交或无匹配。全部成交或部分成交时,可能在交易委托账本中存在一个或多个匹配的订单,即会产生一条或多条成交记录。当无匹配或部分成交时,委托单的部分数据包括剩余未成交的数量会暂时保存到交易委托账本中,等待与后续的委托单匹配撮合。
- 将撮合产生的成交记录持久化到数据库,并根据历史成交记录生成市场数据,如K线数据、今日涨跌幅等。
- 更新数据库中所有成交订单的委托单数据,以及更新订单用户的资产账户余额。
- 将更新的订单数据、市场数据等发送给到前台。
整个交易流程中涉及到多个服务,包括用户服务、账户服务、订单服务、撮合服务、市场数据服务等。其中,只有第7步是撮合引擎处理的。从单一职责原则来说,撮合引擎就应该只做一件事,那就是负责撮合订单。撮合之前的委托单持久化、冻结资金等,以及撮合之后生成K线数据等,都不应该属于撮合引擎的职责。
撮合竞价方式
撮合竞价方式一般有两种,一是集合竞价,二是连续竞价。股票交易系统一般会在不同交易时间段采用不同的竞价方式,比如在开盘或收盘时采用集合竞价,从而产生开盘价或收盘价,其余时间采用连续竞价。而大多数字货币交易系统则没有集合竞价,只有连续竞价,开盘价一般是在开始交易之前就设定好的。
集合竞价
所谓集合竞价,是指对一段时间内接收的买卖委托单一次性集中撮合的竞价方式。以深沪的股票交易系统为例,在每个交易日的 9:15~9:25 期间是集合竞价时间。在该时间段内,系统陆续接收到的委托单不会即时成交,而是先将所有委托单按照价格优先、时间优先的原则排序,并在此基础上,找出一个基准价格,使它能同时满足以下三个条件:
- 可实现最大成交量的价格;
- 高于该价格的买单与低于该价格的卖单能全部成交的价格;
- 与该价格相同的买方或卖方至少有一方全部成交的价格。
在 9:25 分结束的时候,该基准价格就被确定为成交价格,所有高于该价格的买单与低于该价格的卖单都将以该价格成交。未能成交的委托单,则自动转入连续竞价。
不过,如果满足以上三个条件的价格存在两个或两个以上呢?对此,深交所和上交所的处理方案有所不同,深交所会取距前收盘价最近的价格为成交价,而上交所则取使未成交量最小的价格为成交价,如果未成交量最小的价格仍不止一个,则取中间价为成交价。
集合竞价的主要目的就是为了确定开盘价或收盘价。
连续竞价
所谓连续竞价,也是我们所熟悉的竞价方式,是指对买卖委托单逐笔连续撮合的竞价方式。用户的挂单,只要满足成交条件,就能即时成交。而集合竞价,则要等到最后一刻才会成交。
连续竞价时,依然要满足价格优先、时间优先的成交原则:
- 价格优先:买单则价格较高者能优先成交,卖单则是价格较低者能优先成交。
- 时间优先:买卖方向和价格相同的委托单,先申报的委托单会比后申报的委托单优先成交。
另外,买入价必须大于或等于卖出价才能撮合成交。当买入价等于卖出价时,成交价就是买入价或卖出价。当买入价大于卖出价时,则还要参考前一笔成交价来确定最新成交价。假设买入价为 B,卖出价为 S,前一笔成交价为 P,最新成交价为 N,那么:
- 如果 P >= B,则 N = B
- 如果 P <= S,则 N = S
- 如果 B > P > S,则 N = P
一套通用的撮合引擎应该两种竞价方式都支持,但对于同一交易标的来说,两种竞价方式不能同时进行,因此设计上需要考虑如何在两种竞价方式之间切换,具体的实现思路在后续章节我们再展开来讲。
质量需求
我们的撮合引擎除了要满足以上所说的功能需求,还应该满足一些质量需求,尤其对可用性、可伸缩性和性能的要求较高。另外,为了达到通用,也要满足可复用性的需求。
先说下可复用性,我们期望的是该撮合引擎既能用于股票交易系统,也能用于数字货币交易系统,既能用于币币交易,也能用于合约交易。因此,该撮合引擎要避免引入与具体系统强相关的业务逻辑,以加强它的可复用性。
再看看性能,要衡量一个撮合引擎的性能,就看它处理每个交易对的 TPS 有多高,即每秒钟能处理多少笔相同交易对的委托单。以前,基于数据库的撮合技术,TPS 一般只有10笔/秒。而现在基本都是采用内存撮合技术,TPS 很容易就能达到1000笔/秒,如果使用独占的高性能服务器,1万笔/秒甚至更高的 TPS 都不难达到。
接着谈谈可伸缩性,我们的每一个撮合引擎既可以同时处理多个交易标的,也可以只处理单个交易标的。当交易标的和并发量增多的时候,可以增加服务器,部署成撮合引擎集群,分别用来处理不同的交易标的,从而能够实现负载均衡。
最后聊聊可用性,高可用主要体现在两点,一是故障率要低,二是对故障维修的时间要短。要降低故障率,那撮合引擎就需要有较高的健壮性,对于可能导致引擎出故障的各种异常情况要考虑好并设计好解决方案。另外,还可以采用多机热备份技术来提高可用性,而且要保证互备服务器之间的数据一致,那就需要引入内存状态机复制方案,实现上会复杂很多。
不过,我们并非一下子就要达到很高的质量要求,因为要求越高,其架构和实现会越复杂。我们可以先从简单的版本开始,然后不断升级迭代。
小结
我们目的是实现一套通用的撮合引擎,要支持集合竞价和连续竞价,还要实现一些质量需求,提高系统的可复用性、性能、可伸缩性、可用性等。后续章节会对这些需求不断深入探讨其设计与实现。另外,我们将采用不断升级迭代的方式来设计和实现多个版本的撮合引擎。
留两个思考题:
- 集合竞价结束的时候,如果不存在符合那三个条件的基准价格,那开盘价又将如何确定?
- 对于单个交易对,是否可通过横向增加服务器的方式提高其性能?