Quant的数字货币程序化交易系统开发笔记 – 2

Quant的数字货币程序化交易系统开发笔记 – 2

Quant的数字货币程序化交易系统开发笔记 – 2

作者:贾茹

嗯,好久没更新了,因为我最近找到了一家高频的实习~ 入职一周以来,看到了同事大神们写的交易系统,发现自己写的确实还是Naive,也存在一些问题,但 best practice 的具体细节确实没法分享了。所以呢,后续文章内容会有一点变化,会重点讲交易所API的使用和数字货币交易规则。

数字货币交易规则

最近找工作也接触不少数字货币团队,聊下来发现大家主要都集中在4~5个流动性好交易所,期货基本上是 OKEX, bitfinix, bitMEX,现货基本上是火币和币安。

数字货币的交易规则与合约设置与传统期货相比有很多不同的地方,这个内容要单开一篇文章来讲了,这里只列一下,有个大概的理解即可。

反向合约/非线性合约交割合约与永续合约手续费 maker-taker机制仓位与杠杆、强平问题

数字货币交易接口概述

数字货币交易所接口设计基本上都差不多,这里以bitmex为例。目前为止bitmex是唯一一个提供测试环境的主流数字货币交易所,也是公认的用户体验最好、开发文档最详细的交易所。我们注册一个测试环境账号,里面自动会有0.1个XBT,后续还可以继续获取免费的测试用途的比特币。

初次接触数字货币程序化交易的同学,建议先看官方的API文档,官方文档是最好的教程。另外主流交易所基本上都会给出自家api的官方示例,例如bitmex官方API实现

REST 与 websocket

数字货币交易所API基本上都采用了 REST + Websocket的方式。REST和Websocket技术上区别这里就不多讲了(主要是怕说错 \捂脸),我们只要知道:

REST是每发一次请求,服务器端给你一次应答,适用于主动请求类功能,一般是主动查询或下单;websocket是当你订阅了某个主题后,服务器连续推送信息给你,适用于被动接收信息的功能,最常用的两个功能的是接收行情和接收成交回报。
REST和websocket API 各自的功能

对于DataScientist/Quant来说,REST api 的使用方式比较简单直观,说穿了就是一个Http请求。而Websocket的用法可能需要稍微拐点弯,接受一种新的思路。我们先从REST开始,直奔主题,用一个最简单的http请求发出一张委托单。

REST API

我们可以使用BitMEX提供的 交互式 REST API 浏览器 来方便的查看每种请求参数格式和返回值示例。REST api 的 endpoint 如图所示,我在图中标出了常用的几个endpoint:

其中绿色的是跟行情相关的endpoint,不需要身份验证即可查询。例如我们想要查最近5天的XBTUSD日度K线数据,参考 trade 这个endpoint的参数传入格式,直接在命令行:

curl -X GET -header Accept: application/json https://testnet.bitmex.com/api/v1/trade/bucketed?binSize=1d&partial=false&symbol=XBTUSD&count=100&reverse=false&startTime=2018-12-01&endTime=2018-12-10

即可得到返回的json数据。

下面我们重点说说下单的问题。程序化下单本身也没啥稀奇,就是发一个POST请求,撤单就是一个DEL请求,改单是一个PUT请求,查询委托单是一个GET请求:

比较麻烦的地方在于身份认证。所有跟账户和交易相关的操作(下单、查成交、查钱包余额)肯定需要证明你是你,而在CS领域证明你是你的方法是:签名验证算法。

感兴趣的同学可以自行google理解签名算法的原理。这里简单的说一下bitmex采用的HMAC签名加密算法:HMAC签名算法可以理解为一个函数,它接收两个参数:apiSecret、你的请求内容(即消息,message);函数返回一个固定长度字符串的签名signature。有了签名,我们再把请求内容和签名通过http请求发送给服务器。服务器端也存储了你的apiSecret,会用同样的算法根据你的message生成签名,与你传来的签名进行比较,如果相同则校验通过,否则校验不通过。

签名的意义在于,不需要通过明文传递apiSecret,也能验证message确实是由你发出的;且通过signature和message不能反推出apiSecret,保证了安全性。

HMAC签名算法

下面我们来具体看一下如何用python实现下单。首先注册账号,在这里为自己的账号生成一对apiKey, apiSecret。接下来参考api签名的官方python实现文档,为一个下单请求生成签名。这里有几个地方要注意:

1.message的格式定义为 verb + path + nonce + data ,其中

verb 为http请求method: GET, POST etc. 这里下单的verb为POSTpath = base_url + endpoint, 其中base_url为 https://testnet.bitmex.com/api/v1;endpoint 为/ordernonce 这里就是expiry, 即请求过期时间,格式为Unix时间戳data为post字典的url编码。这里我们以市价单买入20手XBTUSD合约,url-encoding为 ?symbol=XBTUSD&side=Buy&orderQty=20&ordType=Market

2.签名生成函数直接调用python hmax库:

import hmac signature = hmac.new(apiSecret, msg, digestmod=hashlib.sha256).hexdigest()

3.请求过期时间要在程序中动态的生成,例如

expires = int(round(time.time()) + 50)

考虑到网络情况可以多加一些时间。

4.签名生成之后,要把签名和apiKey加入http请求头,连同原始的请求一起发给交易所。

如下是生成签名的代码,从官方示例中抄过来的,修改了部分bug。注意这个用python2运行:

# -*- coding: utf-8 -*- import time import hashlib import hmac from urlparse import urlparse # 签名是 HMAC_SHA256(secret, verb + path + expires + data),十六进制编码。 # verb 必须是大写的,url 是相对的,nonce 必须是一个递增的 64 位整数 # 并且数据(如果存在的话)必须是 JSON 格式,并且键值之间没有空格。 def generate_signature(secret, verb, url, expires, data): “””Generate a request signature compatible with BitMEX.””” # 解析该 url 来移除基础地址而得到 path parsedURL = urlparse(url) path = parsedURL.path if parsedURL.query: path = path + ? + parsedURL.query if isinstance(data, (bytes, bytearray)): data = data.decode(utf8) print(“Computing HMAC: %s % verb + path + str(expires) + data) message = verb + path + str(expires) + data signature = hmac.new(bytes(secret), bytes(message), digestmod=hashlib.sha256).hexdigest() return signature expires = 1518064236 # 或者你可以像以下这样生成: expires = int(round(time.time()) + 5) apiKey = # <<– 这里填入你的apiKey apiSecret = # <<– 这里填入你的apiSecret signature = generate_signature(apiSecret, GET, /api/v1/order, expires, symbol=XBTUSD&side=Buy&orderQty=20&ordType=Market) print(signature)

运行结果为

$ python2 demo.py Computing HMAC: GET/api/v1/order1545209108symbol=XBTUSD&side=Buy&orderQty=20&ordType=Market 91e146d17cb2f282f298cca9e053f738e9cea8bdbbfa23458d677ba1499407c2 # <– 这就是生成的签名

上图的代码只是一个示例。接下来要做的事情是把生成的签名塞到正常的请求中去,发送给交易所。

具体的代码我就不写了,这里贴一张从vnpy中封装bitmex-api的代码段,基本原理就是在请求header中加入api-signature,连同api-key和api-expires一起。

下单成功之后,你会在网页上看到委托回报,也许还有成交回报;同时,如果你还有一个Websocket链接并订阅了相关主题,那么在websockt中也会收到委托回报和成交回报,接下来我们就来看看websocket API。

Websocket API

在一个交易系统中,Websocket链接主要接收两方面的信息:一是行情信息,二是订单状态信息。前者不需要身份验证,后者需要。我们从简单的开始,先尝试接入行情数据。

websocket基本用法

首先,如果你没有接触过websocket,关于websocket本身的用法,有几点需要知道:

1.一个websocket连接的构造方式为:

import websocket ws = websocket.WebSocketApp(url, on_message, on_close, on_open, on_error)

其中url为连接地址,四个回调函数on_XXX 分别为收到各种信息时的回调处理函数,其中最重要的就是 on_message 这个函数,我们收到的所有正常的信息都是交由这个函数进行第一步的处理。在实践中,这个函数中只做信息的分类和转发,经过几层转发,才会触及到真正的信息的处理逻辑。举个交易系统的栗子,我们收到了原始的orderbook变动信息,首先会给到行情模块的on_data函数,将原始的json数据处理成我们交易系统标准化的行情数据类 eg.OrderbookDepth类,再将其给到订阅了这个标的的策略实例,策略的on_depth函数会被调用,调用栈如下:

ws.on_message(message) # 原始message,解析发现其为行情数据,调用trader.on_data() trader.on_data(json) # 行情数据引擎处理json数据,组装成交易系统标准的行情数据类,喂给策略。 strategy.on_depth(OrderbookDepthData) # 策略计算

2.通过 ws.run_forever(),启动一个websocket连接并让它持续运行。注意这个ws实例需要单开一个线程运行,否则程序会卡在run_forever()这里。示例如下:(对多线程不熟悉的同学,自行google理解下面代码的含义)

import threading td = threading.Thread(target=ws.run_forever) td.start() # on_exit: td.join()

3.向websocket发送信息的方式为:

ws.send(json)

实践中,我们发送的信息基本上就两类:订阅和取消订阅。采取“订阅”的方式来告诉服务器你需要哪些信息,没有订阅的信息不会推送给你。查阅bitmex websocket文档,订阅操作发送的数据格式为:

{“op”: “<command>”, “args”: [“arg1”, “arg2”, “arg3”]}

例如,我们订阅XBTUSD的10档深度行情,发送的数据为

{“op”: “subscribe”, “args”: [“orderBook10:XBTUSD”]}

更多的订阅主题和示例,请参阅文档。

4.心跳。如果我们的系统长时间没有向服务器发送信息,或是很长时间没有收到服务器发来的信息,服务器会认为我们的连接已经失效并关闭连接。所以需要再单开一个线程,每隔一定的时间向服务器发送一个ping信息,并处理服务器发来的ping信息。

5.频率限制。多数交易所都有流控限制,即单位时间内的请求数不能超过一定限额,超过了会返回错误(具体参见bitmex的错误信息文档)。每个请求返回时也会附带当前剩余多少次请求限额的信息。

接收行情(无需身份验证)

这里直接给出一个最简单的的bitmexWebsocketAPI封装:(可直接运行,此代码参考的是bitmex交易所官方示例项目

import websocket import threading import json import time class bitmexWS(object): “””bitMEX WebSocket””” def __init__(self): self.ws_url = wss://testnet.bitmex.com/realtime self.ws = None # websocket链接实例 self.wst = None # 运行ws.runforever()的线程 def connect(self): # 构造websocket连接实例 self.ws = websocket.WebSocketApp(self.ws_url, on_message=self.__on_message, on_close=self.__on_close, on_open=self.__on_open, on_error=self.__on_error) # 单开一个线程运行 ws.run_forever() self.wst = threading.Thread(target=lambda: self.ws.run_forever()) self.wst.start() print(ws thread start) def __on_message(self, ws, message): “””on_message回调函数””” print(“========================== MESSAGE ==========================”) print(message) def __on_error(self, ws, error): print(Calling ws.__on_error()) print(error) def __on_close(self, ws): print(Calling ws.__on_close()) del self.wst def __on_open(self, ws): print(Calling ws.__on_open()) def subscribe_topic(self, topic): “””订阅主题 格式为:{“op”: “subscribe”, “args”: [<SubscriptionTopic>]} “”” self.__send_command(subscribe, [topic]) def __send_command(self, command, args=None) “””发送请求””” if args is None: args = [] self.ws.send(json.dumps({op: command, args: args})) bmws = bitmexWS() bmws.connect() time.sleep(10) print(****************) bmws.subscribe_topic(quote:XBTUSD)

运行结果如下:

bitmex Websocket 行情数据推送

接收委托状态变化与成交回报(需要身份验证)

订阅委托状态变化与成交回报与订阅行情没什么两样,唯一的不同是,在建立websocket连接时,需要传送一些身份验证相关的headers,具体方式很简单,就是在 WebsocketAPP()中加一个header参数,header的生成方式与之前REST的生成方式相同。

一旦连接建立起来,后续的发送和接收都不需要再次验证身份,订阅和取消订阅、on_message处理等都和接收行情没什么两样。(这跟REST不同,REST每发一次请求都需要验证身份,即便是长连接也是如此)。

这里我截一张官方示例的图,并附上我自己的全套实现代码(可运行)。

我的代码:(稍后github上传)

运行效果(先把订阅委托状态变化的程序开起来,然后手动在网页端下单,程序这边就能看到输出啦)

api vs spi

这里插一句,如果是接触过CTP接口封装的同学,可能会觉得上述websocket的方式与Spi有异曲同工之妙,事实上确实是这样。本来我也打算写一篇CTP接入的文章(唔,怎么坑越挖越大),里面具体讲一下API和SPI,以及他们在CTP接口中是怎么被使用的,这里就不展开说了。

Whats next?

现在我们已经实现的功能有

接收行情下单接收成交回报

有了这些储备,理论上说就已经能写交易系统了。由于策略类型的不同,交易系统的架构也各不相同,需要根据你自己的策略类型来定制化设计。我之前的开源项目的架构是按照CTA策略来设计的,而现在做的高频策略,使用的公司的交易平台架构又完全不同。

因此,这个系列文章就暂时告一段落啦,后续大家可以根据自己的实际需求,自行发挥与创造(额,其实主要是因为工作太忙没时间写下去了/(ㄒoㄒ)/~~,而且可能会涉及到保密之类的问题 O__O “…)

PS0. 后面会写一个数字货币交易规则的文章。打算在一个月之内写出来,算是我接触数字货币半年以来的一个总结。

PS1. 还想写一个CTP接入的笔记,但估计要很久。

PS2. 最近喜欢上了宏观,也可能会先写几篇宏观的读书笔记。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Proudly powered by WordPress | Theme: HoneyWaves by SpiceThemes