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:
- Determine whether the new order is a pay or sell order.
- 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.
- 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.
- Otherwise, the matching condition is met, and the new order and the head order are matched.
- 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)