交易所撮合引擎原理及實現代碼

交易撮合引擎(Matching/Trading Engine),顧名思義是用來撮合交易的軟件,普遍地應用在金融、證券、加密貨幣交易等領域。交易引擎負責管理加密資產市場中全部的開口訂單(Open Orders),並在發現匹配的訂單對(Trading Pair)時自動執行交易。本文將首先介紹有關加密資產交易撮合引擎的基本概念,例如委託單、交易委託帳本等,而後使用Golang實現一個原理性的撮合引擎。若是你正在考慮實現相似交易所(Exchange)這樣的產品,相信本文會對你有很大的幫助。php

能夠這樣先思考一下:若是你要實現一個供人們將以太幣兌換爲比特幣的市場,那麼你就須要跟蹤一些信息,例如以太幣的買/賣價格(以比特幣計算)、哪些買單或賣單尚未執行等等,同時還要處理新進來的委託單。將這一思路擴展到多個交易對,而後再集成錢包功能,你就實現了一個完整的交易引擎,就像幣安同樣。前端

本文的完整源碼下載地址:https://github.com/ezpod/crypto-exchange-enginejava

一、基本概念與術語

在開始打造交易撮合引擎以前,讓咱們首先熟悉相關的基本概念與術語。node

撮合/交易引擎

正如前面所述,交易撮合引擎是用來撮合交易的軟件,能夠先把交易撮合引擎看做一個黑盒子,它有一些輸入和輸出。python

例如,可能的輸入包括:android

  • 建立新的委託單(NewOrder):一個新的委託單能夠做爲交易撮合引擎的輸入,引擎會嘗試將其與已有的 委託單進行撮合。
  • 取消已有的委託單(CancelOrder):用戶也能夠取消一個以前輸入的委託單,若是它尚未執行的話,即開口訂單。

固然你能夠定義其餘的輸入,出於簡化考慮,咱們如今只定義上述兩個輸入。git

交易撮合引擎的輸出是一些事件,以便及時通知其餘應用處理。例如,當引擎撮合了一筆交易後,就會觸發一個TradesGenerated事件;而當取消了一個已有的委託單後,引擎就會觸發rderCancelled。一樣,你能夠根據本身的需求來定義引擎的輸出,這裏咱們仍是簡單點,只定義這兩個輸出事件。程序員

交易委託帳本

交易委託帳本(Order Book)就是一個買方委託單或買方委託單的列表,一般按照價格和時間排序。github

當一個新的買方(買方)委託單進入引擎後,引擎就會將嘗試其與現有的賣方(買方)委託帳本 進行匹配,看是否存在執行交易的可能。若是找到了匹配的對手單,引擎就能夠執行這兩個委託單了,也就是撮合成功了。web

委託單

在任何交易引擎中,均可能有多種類型的委託單供用戶選擇。其中常見的類型包括:

  • 限價委託單

限價委託單是在當前的加密貨幣交易環境中最經常使用的委託類型。這種委託單容許用戶指定一個價格,只有當撮合引擎找到一樣價格甚至更好價格的對手單時才執行交易。

對於一個買方委託單而言,這意味着若是你的委託價格是¥100,那麼該委託單將會在任何不高於¥100的價格成交 —— 買到指定的價格或者更便宜的價格;而對於一個賣方委託單而言,一樣的委託價格意味着該委託單將在任何不低於¥100的價格成交—— 賣出指定的價格或者更高的價格。

  • 市價委託單

市價委託單的撮合會徹底忽略價格因素,而致力於有限完成指定數量的成交。市價委託單在交易委託帳本中有較高的優先級,在流動性充足的市場中市價單能夠保證成交。

例如,當用戶委託購買2個以太幣時,該委託單能夠在¥900、¥1000、¥2000或任何其餘價位成交,這依賴於市場中當前的敞口委託單的狀況。

  • 止損委託單

止損委託單盡在市場價格到達指訂價位時才被激活,所以它的執行方式與市價委託單相反。一旦止損委託單激活,它們能夠自動轉化爲市價委託單或限價委託單。

若是你但願打造一個高級的交易所,那麼還有其餘一些須要瞭解的概念,例如流動性、多/空交易、FIX/FAST協議等等,可是一樣出於簡化考慮,咱們將這些內容留給你本身去發現。

二、系統架構

如今,對於交易撮合引擎的構成咱們已經有了一些瞭解,那麼讓咱們看一下整個系統的架構,以及咱們將要使用的技術:

在這裏插入圖片描述 正如你上面看到的,咱們的系統將包含引擎的多個客戶端,這些客戶端能夠是交易所繫統中的其餘組件,例如接收終端用戶委託請求的App等等。

在客戶端和引擎之間的通訊是使用Apache Kafka做爲消息總線來實現的,每一個交易對都對應Kafka的一個主題,這樣咱們能夠確保當消息隊列接收到用戶委託單時,引擎將以一樣的前後順序處理委託單。這保證了即便引擎崩潰重啓咱們也能夠重建交易委託帳本。

引擎將監聽Kafka主題,執行委託帳本命令並將引擎的輸出事件發佈到消息隊列中。固然若是可以監測委託單的處理速度以及交易的執行狀況會更酷。咱們可使用Prometheus來採集性能指標,使用grafana來實現一個監視儀表盤。

三、開發語言選擇

能夠選擇你熟悉的開發語言,不過因爲交易撮合引擎計算量巨大,一般咱們應當選擇底層系列的語言,例如:C/C++、GoLang、Rust、Java等等。在這個教程中,咱們使用Golang,由於它很快、容易理解、併發實現簡單,並且我也有很久沒有用C++了。

四、開發交易撮合引擎

咱們將按照如下的步驟來開發交易撮合引擎:

  • 基礎類型定義
  • Consumer實現
  • Order Book實現
  • Producer實現
  • Monitoring實現

4.1 基礎類型定義

咱們須要首先定義一些基礎類型,這包括Order、OrderBook和Trade,分別表示委託單、交易委託帳本和交易:

下面是engine/order.go文件的內容:

package engine

import "encoding/json"

type Order struct {
	Amount uint64 `json:"amount"`
	Price  uint64 `json:"price"`
	ID     string `json:"id"`
	Side   int8   `json:"side"`
}

func (order *Order) FromJSON(msg []byte) error {
	return json.Unmarshal(msg, order)
}

func (order *Order) ToJSON() []byte {
	str, _ := json.Marshal(order)
	return str
}

這裏咱們就是簡單地建立了一個結構用來記錄訂單的主要信息,而後添加了一個方法用於快速的JSON轉換。

相似地engine/trade.go文件的內容:

package engine

import "encoding/json"

type Trade struct {
	TakerOrderID string `json:"taker_order_id"`
	MakerOrderID string `json:"maker_order_id"`
	Amount       uint64 `json:"amount"`
	Price        uint64 `json:"price"`
}

func (trade *Trade) FromJSON(msg []byte) error {
	return json.Unmarshal(msg, trade)
}

func (trade *Trade) ToJSON() []byte {
	str, _ := json.Marshal(trade)
	return str
}

如今咱們已經定義了基本的輸入和輸出類型,如今看看交易委託帳本engine/order_book.go文件的內容:

package engine

// OrderBook type
type OrderBook struct {
	BuyOrders  []Order
	SellOrders []Order
}

// Add a buy order to the order book
func (book *OrderBook) addBuyOrder(order Order) {
	n := len(book.BuyOrders)
	var i int
	for i := n - 1; i >= 0; i-- {
		buyOrder := book.BuyOrders[i]
		if buyOrder.Price < order.Price {
			break
		}
	}
	if i == n-1 {
		book.BuyOrders = append(book.BuyOrders, order)
	} else {
		copy(book.BuyOrders[i+1:], book.BuyOrders[i:])
		book.BuyOrders[i] = order
	}
}

// Add a sell order to the order book
func (book *OrderBook) addSellOrder(order Order) {
	n := len(book.SellOrders)
	var i int
	for i := n - 1; i >= 0; i-- {
		sellOrder := book.SellOrders[i]
		if sellOrder.Price > order.Price {
			break
		}
	}
	if i == n-1 {
		book.SellOrders = append(book.SellOrders, order)
	} else {
		copy(book.SellOrders[i+1:], book.SellOrders[i:])
		book.SellOrders[i] = order
	}
}

// Remove a buy order from the order book at a given index
func (book *OrderBook) removeBuyOrder(index int) {
	book.BuyOrders = append(book.BuyOrders[:index], book.BuyOrders[index+1:]...)
}

// Remove a sell order from the order book at a given index
func (book *OrderBook) removeSellOrder(index int) {
	book.SellOrders = append(book.SellOrders[:index], book.SellOrders[index+1:]...)
}

在交易委託帳本中,除了建立保存買/賣方委託單的列表外,咱們還須要定義添加新委託單的方法。

委託單列表應當根據其類型按升序或降序排列:賣方委託單是按降序排列的,這樣在列表中序號最大的委託單價格最低;買方委託單是按升序排列的,所以在其列表中最後的委託單價格最高。

因爲絕大多數交易會在市場價格附近成交,咱們能夠輕鬆地從這些數組中插入或移除成員。

4.2 委託單處理

如今讓咱們來處理委託單。

在下面的代碼中咱們添加了一個命令來實現對限價委託單的處理。

文件engine/order_book_limit_order.go的內容:

package engine

// Process an order and return the trades generated before adding the remaining amount to the market
func (book *OrderBook) Process(order Order) []Trade {
	if order.Side == 1 {
		return book.processLimitBuy(order)
	}
	return book.processLimitSell(order)
}

// Process a limit buy order
func (book *OrderBook) processLimitBuy(order Order) []Trade {
	trades := make([]Trade, 0, 1)
	n := len(book.SellOrders)
	// check if we have at least one matching order
	if n != 0 || book.SellOrders[n-1].Price <= order.Price {
		// traverse all orders that match
		for i := n - 1; i >= 0; i-- {
			sellOrder := book.SellOrders[i]
			if sellOrder.Price > order.Price {
				break
			}
			// fill the entire order
			if sellOrder.Amount >= order.Amount {
				trades = append(trades, Trade{order.ID, sellOrder.ID, order.Amount, sellOrder.Price})
				sellOrder.Amount -= order.Amount
				if sellOrder.Amount == 0 {
					book.removeSellOrder(i)
				}
				return trades
			}
			// fill a partial order and continue
			if sellOrder.Amount < order.Amount {
				trades = append(trades, Trade{order.ID, sellOrder.ID, sellOrder.Amount, sellOrder.Price})
				order.Amount -= sellOrder.Amount
				book.removeSellOrder(i)
				continue
			}
		}
	}
	// finally add the remaining order to the list
	book.addBuyOrder(order)
	return trades
}

// Process a limit sell order
func (book *OrderBook) processLimitSell(order Order) []Trade {
	trades := make([]Trade, 0, 1)
	n := len(book.BuyOrders)
	// check if we have at least one matching order
	if n != 0 || book.BuyOrders[n-1].Price >= order.Price {
		// traverse all orders that match
		for i := n - 1; i >= 0; i-- {
			buyOrder := book.BuyOrders[i]
			if buyOrder.Price < order.Price {
				break
			}
			// fill the entire order
			if buyOrder.Amount >= order.Amount {
				trades = append(trades, Trade{order.ID, buyOrder.ID, order.Amount, buyOrder.Price})
				buyOrder.Amount -= order.Amount
				if buyOrder.Amount == 0 {
					book.removeBuyOrder(i)
				}
				return trades
			}
			// fill a partial order and continue
			if buyOrder.Amount < order.Amount {
				trades = append(trades, Trade{order.ID, buyOrder.ID, buyOrder.Amount, buyOrder.Price})
				order.Amount -= buyOrder.Amount
				book.removeBuyOrder(i)
				continue
			}
		}
	}
	// finally add the remaining order to the list
	book.addSellOrder(order)
	return trades
}

看起來咱們將一個方法變成了兩個,分別處理買方委託單和賣方委託單。這兩個方法在每一個方面 都很類似,除了處理的市場側不一樣。

算法很是簡單。咱們將一個買方委託單與全部的賣方委託單進行匹配,找出任何與買方委託價格 一致甚至更低的賣方委託單。當這一條件不能知足時,或者該買方委託單完成後,咱們返會撮合 的交易。

4.3 接入Kafka

如今就快完成咱們的交易引擎了,還須要接入Apache Kafka服務器,而後開始監聽委託單。

main.go文件的內容:

package main

import (
	"engine/engine"
	"log"

	"github.com/Shopify/sarama"
	cluster "github.com/bsm/sarama-cluster"
)

func main() {

	// create the consumer and listen for new order messages
	consumer := createConsumer()

	// create the producer of trade messages
	producer := createProducer()

	// create the order book
	book := engine.OrderBook{
		BuyOrders:  make([]engine.Order, 0, 100),
		SellOrders: make([]engine.Order, 0, 100),
	}

	// create a signal channel to know when we are done
	done := make(chan bool)

	// start processing orders
	go func() {
		for msg := range consumer.Messages() {
			var order engine.Order
			// decode the message
			order.FromJSON(msg.Value)
			// process the order
			trades := book.Process(order)
			// send trades to message queue
			for _, trade := range trades {
				rawTrade := trade.ToJSON()
				producer.Input() <- &sarama.ProducerMessage{
					Topic: "trades",
					Value: sarama.ByteEncoder(rawTrade),
				}
			}
			// mark the message as processed
			consumer.MarkOffset(msg, "")
		}
		done <- true
	}()

	// wait until we are done
	<-done
}

//
// Create the consumer
//

func createConsumer() *cluster.Consumer {
	// define our configuration to the cluster
	config := cluster.NewConfig()
	config.Consumer.Return.Errors = false
	config.Group.Return.Notifications = false
	config.Consumer.Offsets.Initial = sarama.OffsetOldest

	// create the consumer
	consumer, err := cluster.NewConsumer([]string{"127.0.0.1:9092"}, "myconsumer", []string{"orders"}, config)
	if err != nil {
		log.Fatal("Unable to connect consumer to kafka cluster")
	}
	go handleErrors(consumer)
	go handleNotifications(consumer)
	return consumer
}

func handleErrors(consumer *cluster.Consumer) {
	for err := range consumer.Errors() {
		log.Printf("Error: %s\n", err.Error())
	}
}

func handleNotifications(consumer *cluster.Consumer) {
	for ntf := range consumer.Notifications() {
		log.Printf("Rebalanced: %+v\n", ntf)
	}
}

//
// Create the producer
//

func createProducer() sarama.AsyncProducer {
	config := sarama.NewConfig()
	config.Producer.Return.Successes = false
	config.Producer.Return.Errors = true
	config.Producer.RequiredAcks = sarama.WaitForAll
	producer, err := sarama.NewAsyncProducer([]string{"127.0.0.1:9092"}, config)
	if err != nil {
		log.Fatal("Unable to connect producer to kafka server")
	}
	return producer
}

利用Golang的Sarama Kafka客戶端開發庫,咱們能夠分別建立一個接入Kafka的消費者和生產者。

消費者將在指定的Kafka主題上等待新的委託單,而後進行撮合處理。生成的交易接下來使用生產者發送到指定的交易主題。

Kafka消息採用字節數組編碼,所以咱們須要將其解碼。反之,將交易傳入消息隊列時,咱們還須要進行必要的編碼。

五、結語

如今你有了一個可伸縮的交易引擎!完整的代碼能夠在GITHUB下載:crypto-exchange-engine

不過這個引擎的目的是教學,另外代碼還支持不少進一步的優化,例如:

  • 使用一種更高效的匹配算法
  • 添加取消訂單的功能
  • 加強通訊能力
  • 委託帳本的備份與恢復
  • 添加監視功能

若是你想學習區塊鏈並在Blockchain Technologies創建職業生涯,那麼請查看咱們分享的一些以太坊、比特幣、EOS、Fabric等區塊鏈相關的交互式在線編程實戰教程:

  • java以太坊開發教程,主要是針對java和android程序員進行區塊鏈以太坊開發的web3j詳解。
  • python以太坊,主要是針對python工程師使用web3.py進行區塊鏈以太坊開發的詳解。
  • php以太坊,主要是介紹使用php進行智能合約開發交互,進行帳號建立、交易、轉帳、代幣開發以及過濾器和交易等內容。
  • 以太坊入門教程,主要介紹智能合約與dapp應用開發,適合入門。
  • 以太坊開發進階教程,主要是介紹使用node.js、mongodb、區塊鏈、ipfs實現去中心化電商DApp實戰,適合進階。
  • ERC721以太坊通證明戰,課程以一個數字藝術品創做與分享DApp的實戰開發爲主線,深刻講解以太坊非同質化通證的概念、標準與開發方案。內容包含ERC-721標準的自主實現,講解OpenZeppelin合約代碼庫二次開發,實戰項目採用Truffle,IPFS,實現了通證以及去中心化的通證交易所。
  • C#以太坊,主要講解如何使用C#開發基於.Net的以太坊應用,包括帳戶管理、狀態與交易、智能合約開發與交互、過濾器和交易等。
  • java比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Java代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Java工程師不可多得的比特幣開發學習課程。
  • php比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在Php代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是Php工程師不可多得的比特幣開發學習課程。
  • c#比特幣開發教程,本課程面向初學者,內容即涵蓋比特幣的核心概念,例如區塊鏈存儲、去中心化共識機制、密鑰與腳本、交易與UTXO等,同時也詳細講解如何在C#代碼中集成比特幣支持功能,例如建立地址、管理錢包、構造裸交易等,是C#工程師不可多得的比特幣開發學習課程。
  • EOS入門教程,本課程幫助你快速入門EOS區塊鏈去中心化應用的開發,內容涵蓋EOS工具鏈、帳戶與錢包、發行代幣、智能合約開發與部署、使用代碼與智能合約交互等核心知識點,最後綜合運用各知識點完成一個便籤DApp的開發。
  • 深刻淺出玩轉EOS錢包開發,本課程以手機EOS錢包的完整開發過程爲主線,深刻學習EOS區塊鏈應用開發,課程內容即涵蓋帳戶、計算資源、智能合約、動做與交易等EOS區塊鏈的核心概念,同時也講解如何使用eosjs和eosjs-ecc開發包訪問EOS區塊鏈,以及如何在React前端應用中集成對EOS區塊鏈的支持。課程內容深刻淺出,很是適合前端工程師深刻學習EOS區塊鏈應用開發。
  • Hyperledger Fabric 區塊鏈開發詳解,本課程面向初學者,內容即包含Hyperledger Fabric的身份證書與MSP服務、權限策略、信道配置與啓動、鏈碼通訊接口等核心概念,也包含Fabric網絡設計、nodejs鏈碼與應用開發的操做實踐,是Nodejs工程師學習Fabric區塊鏈開發的最佳選擇。
  • Hyperledger Fabric java 區塊鏈開發詳解,課程面向初學者,內容即包含Hyperledger Fabric的身份證書與MSP服務、權限策略、信道配置與啓動、鏈碼通訊接口等核心概念,也包含Fabric網絡設計、java鏈碼與應用開發的操做實踐,是java工程師學習Fabric區塊鏈開發的最佳選擇。
  • tendermint區塊鏈開發詳解,本課程適合但願使用tendermint進行區塊鏈開發的工程師,課程內容即包括tendermint應用開發模型中的核心概念,例如ABCI接口、默克爾樹、多版本狀態庫等,也包括代幣發行等豐富的實操代碼,是go語言工程師快速入門區塊鏈開發的最佳選擇。

原文連接:交易撮合引擎原理與實現 — 匯智網

相關文章
相關標籤/搜索