交易所撮合引擎原理及实现代码

交易撮合引擎(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语言工程师快速入门区块链开发的最佳选择。

原文连接:交易撮合引擎原理与实现 — 汇智网

相关文章
相关标签/搜索