Please follow the public account “Keegan Xiaogang” for more articles


Matching engine development: the beginning

Matchmaking engine development: MVP version

Matchmaking engine development: data structure design

Matching engine development: docking black box

Matching engine development: decrypting the black box process

Matching engine development: Code implementation of the process

Matching engine development: Cache and MQ

Matching engine development: log output

Matchmaking engine development: the end


This section, the final article in the series, covers the remaining implementation logic for order queues in the trade trust account and for more order types. In addition, many friends are asking whether all the code will be open source and put on Github after the completion. All I can say is that it will be open source in the long run, but there are no plans to open source in the short run.

The order queue

The order book is actually made up of two order queues, a buy queue and a sell queue. Any query or operation on the transaction trust book is actually a query or operation on these two queues. The design of the order queue also directly affects the performance of matchmaking. The previous article discussed the design of the order queue briefly when discussing the design of the data structure. We mainly use the two-dimensional link combined with the Map to save all the orders, relying on the Container/List package.

The order queue structure is as follows:

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

SortBy specifies the direction in which the price is ordered. The buy queue is in descending order and the sell order queue is in ascending order. ParentList holds all orders of the entire 2-D linked list. The first dimension is sorted by price, and the second dimension is sorted by time. ElementMap is the key-value pair where Key is the price and Value is the second dimensional order list.

The initialization function is simple, just assigning values to a few fields, as follows:

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

In addition to the initialization function, five other functions are provided:

  • AddOrder (Order) : Adds an order
  • GetHeadOrder () : Reads the header order
  • PopHeadOrder () : Reads and deletes the header order
  • RemoveOrder (Order) : Remove the order
  • GetDepthPrice (depth) : read depth price

Among the above five functions, only the first one is more complicated. In order to make the process easier to understand, I will not post the code, and draw a complete flow chart for you to see:

The process is a bit complicated and can be digested several times, preferably by translating it into code yourself.

The other functions are simple, but the last one needs a little clarification. Reading the depth price is to facilitate the judgment of the upper limit price when processing the orders of market-opponent, market-top5, market-top10 and other types. Look at the code for this function to understand its logic and usage:

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()
		ift ! =nil {
			p = t
		} else {
			break;
		}
	}
	o := p.Value.(*list.List).Front().Value.(*Order)
	return o.Price.String(), i
}
Copy the code

Multiple order types

Our engine supports a total of six order types, which were briefly covered in the previous article, but did not go into detail about what the specific business logic for each of these different types should be, so I will add to that.

1. limit

The common price limit is the simplest, and the code implementation has already been shown. To further understand, LET me draw a picture:

The processing logic is:

  1. Determine whether the new order is a pay or sell order.
  2. If it is a buy order, then read out the head order from OrderBook, that is, the head order in the sell order queue; If it is a sell order, then read out the head order from OrderBook, that is, the head order in the pay queue.
  3. When the new order is paid, if the head order is empty or the new order is smaller than the head order, that is, the transaction cannot be completed, then the new order is added to the pay queue, and the processing is finished; When the new order is a sell order, if the header order is empty or the new order is larger than the header order, the new order is added to the sell order queue, and the processing is finished.
  4. Otherwise, the matching condition is met, and the new order and the head order are matched.
  5. After matching, if the remaining number of new orders is zero, then the end, if still greater than zero, then go back to step 2 to continue to take the next head order, and so on.

2. limit-ioc

There is only one difference between the IOC limit and the ordinary limit. If the new order and the head order do not match, the ordinary limit will be added to the order queue, while the IOC limit will be cancelled. See the following figure:

3. market

Default market price order logic is also relatively simple, it does not need to judge the price, as long as the head order is not empty, it will directly match the head order transaction, its processing logic is as follows:

4. market-top5/market-top10

The logic of the optimal 5-tier / 10-tier market order is similar to that of the default market order. The difference is that there is no upper or lower limit for the transaction price of the default market price, but there is a upper or lower limit for the optimal 5-tier / 10-tier market price, and the order above the upper or lower limit will not be traded. Drawing too tired, or directly post code, the following is to deal with the bill:

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()
	ifheadOrder ! =nil && limitPrice.GreaterThanOrEqual(headOrder.Price) {
		matchTrade(headOrder, order, book, lastTradePrice)
		if order.Amount.IsPositive() {
			goto LOOP
		}
	} else {
		cancelOrder(order)
	}
}
Copy the code

5. market-opponent

The last type, the counterparty best offer, deals only with the first rung of the counterparty’s price, but is different from the fifth/tenth rung: the untraded portion of the fifth/tenth rung is withdrawn, while the last untraded portion of the counterparty’s best offer is converted into a limit order. Look at the code:

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()
	ifheadOrder ! =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())
	}
}
Copy the code

The end

This is the end of the series. However, my matchmaker will continue to iterate and develop other components that will work with the current matchmaker engine. Welcome to follow up.


The author’s personal blog

Scan the following QR code to follow the public account (public account name: Keegan Xiaosteel)