28

撮合引擎开发:完结篇

 4 years ago
source link: https://segmentfault.com/a/1190000021200355
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

欢迎关注「Keegan小钢」公众号获取更多文章

本小节是该系列文章的最后一篇了,将讲解剩下的一些东西,包括交易委托账本中订单队列的实现逻辑、更多订单类型的实现逻辑。另外,不少朋友在问,完结后所有代码是否会开源放上 Github?我只能说,长期大概率会开源,但短期内还没打算开源。

订单队列

交易委托账本其实就是由两个订单队列组成的,一个买单队列,一个卖单队列。任何对交易委托账本的查询和操作,实际上都是查询和操作这两个队列。订单队列的设计也直接影响了撮合的性能,前面文章讲数据结构设计时也有简单聊了订单队列的设计,我们主要是用二维链接结合 Map 来保存所有订单的,依赖的是 container/list 包。

订单队列的结构体如下:

type orderQueue struct {
    sortBy     enum.SortDirection
    parentList *list.List
    elementMap map[string]*list.Element
}

sortBy指定价格排序的方向,买单队列是降序的,而卖单队列则是升序的。 parentList 保存整个二维链表的所有订单,第一维以价格排序,第二维以时间排序。 elementMap 则是 Key 为价格、Value 为第二维订单链表的键值对。

初始化函数就比较简单了,对几个字段赋值而已,代码如下:

func (q *orderQueue) init(sortBy enum.SortDirection) {
    q.sortBy = sortBy
    q.parentList = list.New()
    q.elementMap = make(map[string]*list.Element)
}

除了初始化函数,还提供了另外五个函数:

  • addOrder(order) :添加订单
  • getHeadOrder() :读取头部订单
  • popHeadOrder() :读取并删除头部订单
  • removeOrder(order) :移除订单
  • getDepthPrice(depth) :读取深度价格

以上五个函数就只有第一个函数会比较复杂,为了让处理流程更容易理解,我就不贴代码了,画一个完整的流程图给大家看看:

aEJB7nZ.png!web

这个流程确实有一点复杂,可以多看几遍好好消化,最好自己动手将其转为代码实现。

其他几个函数就简单了,关于最后一个函数需要补充说明一下。读取深度价格是为了方便处理 market-opponent、market-top5、market-top10 等类型的订单时判断上限价格。请看该函数的代码以理解该函数的逻辑和用法:

func (q *orderQueue) getDepthPrice(depth int) (string, int) {
    if q.parentList.Len() == 0 {
        return "", 0
    }
    p := q.parentList.Front()
    i := 1
    for ; i < depth; i++ {
        t := p.Next()
        if t != nil {
            p = t
        } else {
            break;
        }
    }
    o := p.Value.(*list.List).Front().Value.(*Order)
    return o.Price.String(), i
}

多种订单类型

我们引擎总共支持了六种订单类型,之前的文章有简单介绍过,但没有深入讲解这几种不同类型的具体业务逻辑应该是怎样的,因此,在此将这部分内容补充上。

1. limit

普通限价是最简单的,前文也已经展示过代码实现,为了加深理解,我再给大家画一张图:

fMFf6nv.png!web

处理逻辑就是:

  1. 判断新订单是买单还是卖单。
  2. 如果是买单,那从 OrderBook 中读取出头部卖单,即卖单队列中的头部订单;如果是卖单,那从 OrderBook 中读取出头部买单,即买单队列中的头部订单。
  3. 新订单为买单时,如果头部订单为空,或者新订单小于头部订单,即无法成交,那就把新订单添加到买单队列中,处理结束;新订单为卖单时,如果头部订单为空,或者新订单大于头部订单,即无法成交,那就把新订单添加到卖单队列中,处理结束。
  4. 否则,符合匹配条件,新订单和头部订单进行撮合成交。
  5. 撮合完成后,如果新订单剩余数量为零则结束,如果还大于零,则回到第2步继续取下一个头部订单,如此循环。

2. limit-ioc

IOC 限价与普通限价不同的地方只有一个,如果新订单和头部订单不匹配时,普通限价单会被添加到订单队列中,而 IOC 限价则是作撤单处理,请看下图:

EvEZ3um.png!web

3. market

默认市价单的逻辑也比较简单,它不需要判断价格,只要头部订单不为空,就直接和头部订单匹配成交,其处理逻辑如下图:

yyEN3ub.png!web

4. market-top5/market-top10

最优五档/十档市价单与默认市价单的逻辑也是类似的,不同点在于:默认市价的成交价格没有上限或下限,但最优五档/十档市价则存在价格上限或下限,超过上下限的委托单不会成交。画图太累,还是直接贴代码吧,以下是处理买单的:

func dealBuyMarketTop(order *Order, book *orderBook, lastTradePrice *decimal.Decimal, depth int) {
    priceStr, _ := book.getSellDepthPrice(depth)
    if priceStr == "" {
        cancelOrder(order)
        return
    }
    limitPrice, _ := decimal.NewFromString(priceStr)
LOOP:
    headOrder := book.getHeadSellOrder()
    if headOrder != nil && limitPrice.GreaterThanOrEqual(headOrder.Price) {
        matchTrade(headOrder, order, book, lastTradePrice)
        if order.Amount.IsPositive() {
            goto LOOP
        }
    } else {
        cancelOrder(order)
    }
}

5. market-opponent

最后一种类型,对手方最优价,该类型只与对手方一档的价位成交,但与最优五档/十档还有一点不一样:最优五档/十档未成交的部分是作撤单处理的,而对手方最优价最后未成交的部分则是转为限价单。请看代码:

func dealBuyMarketOpponent(order *Order, book *orderBook, lastTradePrice *decimal.Decimal) {
    priceStr, _ := book.getSellDepthPrice(1)
    if priceStr == "" {
        cancelOrder(order)
        return
    }
    limitPrice, _ := decimal.NewFromString(priceStr)
LOOP:
    headOrder := book.getHeadSellOrder()
    if headOrder != nil && limitPrice.GreaterThanOrEqual(headOrder.Price) {
        matchTrade(headOrder, order, book, lastTradePrice)
        if order.Amount.IsPositive() {
            goto LOOP
        }
    } else {
        order.Price = limitPrice
        order.Type = enum.TypeLimit
        book.addBuyOrder(order)
        cache.UpdateOrder(order.ToMap())
        log.Info("engine %s, a order has added to the orderbook: %s", order.Symbol, order.ToJson())
    }
}

完结

至此,整个系列就此完结。不过,我的撮合程序依然会继续迭代升级,另外,也将开始开发其他组件,将会和当前这个撮合引擎结合来用。欢迎关注后续动态。

作者的个人博客

扫描以下二维码即可关注公众号(公众号名称:Keegan小钢)

zUr2myY.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK