黑箱引擎
我们的撮合引擎作为一个相对通用的组件,其实就是一个黑箱,如果想将它应用到各种不同的交易系统,只要有标准的输入和输出,对接是很容易的。
写作此文时的撮合引擎为 1.3 版本,我将其编译构建成可在 Linux amd64 环境运行的可执行文件,并与依赖的配置文件一起压缩成一个压缩包 matching.zip。这就成为一个黑箱引擎了。
不过,该黑箱引擎除了对运行系统有要求,还对 Redis 有要求。由于使用了 Redis 的新版 MQ 功能,即 stream 数据结构,要求 Redis 至少是 5.0 版本。
其实,我也可以编译构建成其他系统环境的可执行文件,如 Windows 或 Mac 系统。但作为一款商业软件以及对性能的一些要求,运行在 Linux 环境更合适。
后面,我们就来看看,如果要将这个黑箱引擎应用到自己的交易系统中,如何对接?
安装部署
安装部署的系统环境需是 Linux amd64 的,另外,如果要让撮合性能更快,建议 Redis 与撮合引擎可以使用同一服务器,这可以减少不同服务器之前的传输耗时。
按照以下步骤操作就可以将撮合引擎安装部署到运行环境了:
1.将 matching.zip 压缩包上传到运行环境;2.在运行环境解压 matching.zip 压缩包,解压后有一个可执行文件和一个文件夹:
•matching:这是撮合引擎程序的可执行文件•conf:存放配置文件的目录,里面就一个配置文件 config.yaml
修改配置文件为自己想要的配置值:
server: port: :9466 //撮合引擎程序启动监听的端口 log: //输出日志配置 fileDir: logs //输出日志存放的目录 fileName: matching //日志文件名,会按日期分割 prefix: //日志消息前缀 level: debug //日志级别,由低到高为:debug、info、warn、error redis: addr: 127.0.0.1:6379 //Redis地址
1.如果使用默认配置,请确认运行环境本地已安装并启动了Redis并运行在6379端口;2.如果不用默认配置,依然需要确认Redis能正确连接;3.运行以下命令,后台启动撮合引擎程序:
./matching &
1.运行以下命令,检查程序是否启动成功:
ps aux|grep matching
1.程序启动成功后,会在配置的日志目录生成日志文件,默认为与 matching 可执行文件同目录下的 logs/matching.log;2.至此,撮合引擎程序安装部署成功。
对接输入
接入撮合引擎只需要对接三个 HTTP 接口,接口统一采用 POST 方法,参数统一用 json 格式,传 body。
1. 开启撮合
开启指定交易标的(交易对)的撮合功能。
HTTP请求
•POST /openMatching
请求参数
•symbol:字符串类型,必传字段,交易标的(交易对)的标识,如 BTC_USDT•price:数字类型,非必传字段,默认为 0,开盘价
请求示例
POST /openMatching Body: { "symbol": "BTC_USDT", "price": 8219.85 }
响应数据
{"code":0,"msg":"OK"}
2. 关闭撮合
关闭指定交易标的(交易对)的撮合功能。
HTTP请求
•POST /closeMatching
请求参数
•symbol:字符串类型,必传字段,交易标的(交易对)的标识,如 BTC_USDT
请求示例
- 响应数据
POST /closeMatching Body: { "symbol": "BTC_USDT" }
响应数据
{"code":0,"msg":"OK"}
3. 处理订单
接收下单和撤单请求。
HTTP请求
•POST /handleOrder
请求参数
•symbol:字符串类型,必传字段,交易标的(交易对)的标识,如 BTC_USDT•action:字符串类型,必传字段,订单动作,下单=create,撤单=cancel•orderId:字符串类型,必传字段,订单ID•side:字符串类型,必传字段,买卖方向,买入=buy,卖出=sell•type:字符串类型,必传字段,订单类型,包括:limit、limit-ioc、market、market-top5、market-top10、market-opponent,说明见下文•amount:数字类型,非必传字段,默认为 0,订单交易量,下单时必传,撤单时可不传•price:数字类型,非必传字段,默认为 0,委托价格,订单类型为市价时可不传
订单类型说明:
•limit:普通限价•limit-ioc:IOC限价-即时成交剩余撤销•market:默认市价-即时成交剩余撤销•market-top5:市价-最优五档即时成交剩余撤销•market-top10:市价-最优十档即时成交剩余撤销•market-opponent:市价-对手方最优价
请求示例
- 响应数据
POST /handleOrder Body: { "symbol": "BTC_USDT", "action": "create", "orderId": "a0001", "side": "buy", "type": "limit", "amount": 0.012, "price": 8230.74 }
响应数据
{"code":0,"msg":"OK"}
对接输出
撮合引擎有两种输出:撤单结果和成交记录。统一用 MQ 的方式进行输入,MQ 是保存为 Redis 5.0 版本之后引入的新数据结构 Stream 类型。每个消息队列其实就是一条 stream,关于 stream 的具体用法我就不展开了,大伙自行去网上搜索学习即可。
1. 撤单结果
每个不同的 symbol 设置一个 stream,其 key 的格式为:matching:cancelresults:{symbol},value 包含两个字段:
•orderId:订单号•ok:1=成功;0=失败
2. 成交记录
每个不同的 symbol 也设置一个 MQ,key 的格式为:matching:trades:{symbol},value 包含的字段如下:
•makerId:maker订单ID•takerId:taker订单ID•takerSide:taker买卖方向•amount:成交数量•price:成交价格•timestamp:成交时间
下游服务就可以通过订阅监听这两种输出,之后做后续的处理,比如 K 线行情服务订阅监听成交记录来生成 K 线数据。等撮合引擎完成之后,我下一个要开发的组件就是 K 线行情服务。
项目结构
最后,先给大伙看看我们这个黑箱内部的整个 Go 项目的文件目录结构:
├── conf # 配置文件存放目录,在1.1版本时增加 │ ├── config.yaml # 配置文件,在1.1版本时增加 ├── engine # 引擎包 │ ├── init.go # 初始化 │ ├── order.go # 委托单 │ ├── order_book.go # 交易委托账本 │ ├── order_queue.go # 订单队列 │ ├── run.go # 具体交易对的撮合引擎启动入口 │ └── trade.go # 成交记录 ├── enum # 枚举类型的包 │ ├── order_action.go # 订单行为,create为下单,cancel为撤单 │ ├── order_side.go # 买卖方向,buy为买入,sell为卖出 │ ├── order_type.go # 订单类型,MVP版本(1.0版本)只支持limit,1.3版本共支持7种类型 │ └── sort_direction.go # 排序方向,asc为升序,desc为降序 ├── errcode # │ ├── code.go # 定义了各种不同的错误码 │ └── errcode.go # 错误码的数据结构,包括code和msg两个字段 ├── handler # │ ├── close_matching.go # 接收关闭撮合的请求 │ ├── handle_order.go # 接收处理订单的请求 │ └── open_matching.go # 接收开启撮合的请求 ├── log # 日志包,在1.2版本增加 │ ├── log.go # 日志输出,在1.2版本增加 ├── main.go # Go程序唯一入口 ├── middleware # 中间件的包 │ ├── cache # 缓存包 │ │ └── cache.go # 缓存操作 │ ├── mq # 消息队列包 │ │ └── mq.go # MQ操作 │ └── redis.go # 主要做Redis初始化操作 └── process # ├── close_engine.go # 关闭引擎 ├── dispatch.go # 分发订单 ├── init.go # 初始化 └── new_engine.go # 启动新引擎
包括 main 包,整个项目总共划分为了 10 个包和 1 个配置文件目录:
•conf:存放配置文件的目录。•main:main 包只有一个 main.go 文件,定义了程序入口函数。•enum:枚举包实现了几个枚举类型的数据结构,包括订单行为、买卖方向、订单类型和排序方向。•errcode:存放错误码的包,errcode.go 定义了错误码的数据结构,有 code 和 msg 两个属性;code.go 则定义了一些错误码对象。•handler:接收 HTTP 请求的函数都放在这个包里,目前也就只有三个 handler 函数。•process:启动、关闭引擎和分发订单的处理过程都在这个包里,包里还维护着不同交易对的订单通道,用来分发不同交易对的订单。•engine:引擎包,包含了委托单、交易委托账本、订单队列、成交记录这几个核心的数据结构,以及用于处理交易对撮合的入口函数。•middleware:存放中间件的包,目前只用到了 Redis 一个中间件。•cache:缓存包,只有一个 cache.go 文件,缓存操作都在这个文件里定义。•mq:消息队列包,也只有一个 mq.go 文件,消息的发送就定义在这里。•log:日志包,实现了日志消息按日期分割并输出到文件。
小结
本节我们了解到撮合引擎作为一个通用组件,有标准的输入和输出,而且这输入和输出都非常简单。也开始给大家展示了黑箱内部的文件目录结构,开始探究黑箱的内部逻辑,后续章节将陆续揭露这些内部实现逻辑。
本节预留的思考题:揣测下为什么我要将下单和撤单定义到同一个接口?而不是分开两个接口?分两个接口和用一个接口各有何优劣?