嗯,好久没更新了,因为我最近找到了一家高频的实习~ 入职一周以来,看到了同事大神们写的交易系统,发现自己写的确实还是Naive,也存在一些问题,但 best practice 的具体细节确实没法分享了。所以呢,后续文章内容会有一点变化,会重点讲交易所API的使用和数字货币交易规则。
最近找工作也接触不少数字货币团队,聊下来发现大家主要都集中在4~5个流动性好交易所,期货基本上是 OKEX, bitfinix, bitMEX,现货基本上是火币和币安。
数字货币的交易规则与合约设置与传统期货相比有很多不同的地方,这个内容要单开一篇文章来讲了,这里只列一下,有个大概的理解即可。
数字货币交易所接口设计基本上都差不多,这里以bitmex为例。目前为止bitmex是唯一一个提供测试环境的主流数字货币交易所,也是公认的用户体验最好、开发文档最详细的交易所。我们注册一个测试环境账号,里面自动会有0.1个XBT,后续还可以继续获取免费的测试用途的比特币。
初次接触数字货币程序化交易的同学,建议先看官方的API文档,官方文档是最好的教程。另外主流交易所基本上都会给出自家api的官方示例,例如bitmex官方API实现
数字货币交易所API基本上都采用了 REST + Websocket的方式。REST和Websocket技术上区别这里就不多讲了(主要是怕说错 捂脸),我们只要知道:
对于DataScientist/Quant来说,REST api 的使用方式比较简单直观,说穿了就是一个Http请求。而Websocket的用法可能需要稍微拐点弯,接受一种新的思路。我们先从REST开始,直奔主题,用一个最简单的http请求发出一张委托单。
我们可以使用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 ,其中
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链接主要接收两方面的信息:一是行情信息,二是订单状态信息。前者不需要身份验证,后者需要。我们从简单的开始,先尝试接入行情数据。
首先,如果你没有接触过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": "", "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": []}
"""
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上传)
运行效果(先把订阅委托状态变化的程序开起来,然后手动在网页端下单,程序这边就能看到输出啦)
这里插一句,如果是接触过CTP接口封装的同学,可能会觉得上述websocket的方式与Spi有异曲同工之妙,事实上确实是这样。本来我也打算写一篇CTP接入的文章(唔,怎么坑越挖越大),里面具体讲一下API和SPI,以及他们在CTP接口中是怎么被使用的,这里就不展开说了。
现在我们已经实现的功能有
有了这些储备,理论上说就已经能写交易系统了。由于策略类型的不同,交易系统的架构也各不相同,需要根据你自己的策略类型来定制化设计。我之前的开源项目的架构是按照CTA策略来设计的,而现在做的高频策略,使用的公司的交易平台架构又完全不同。
因此,这个系列文章就暂时告一段落啦,后续大家可以根据自己的实际需求,自行发挥与创造(额,其实主要是因为工作太忙没时间写下去了/(ㄒoㄒ)/~~,而且可能会涉及到保密之类的问题 O__O "…)
PS0. 后面会写一个数字货币交易规则的文章。打算在一个月之内写出来,算是我接触数字货币半年以来的一个总结。
PS1. 还想写一个CTP接入的笔记,但估计要很久。
PS2. 最近喜欢上了宏观,也可能会先写几篇宏观的读书笔记。