You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

245 lines
18 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 34 | RESTful & Socket搭建交易执行层核心
你好,我是景霄。
上一节,我们简单介绍了量化交易的历史、严谨的定义和它的基本组成结构。有了这些高层次的基本知识,接下来我们就分模块,开始讲解量化交易系统中具体的部分。
从这节课开始,我们将实打实地从代码出发,一步步设计出一套清晰完整、易于理解的量化交易系统。
一个量化交易系统,可以说是一个黑箱。这个黑箱连接交易所获取到的数据,通过策略运算,然后再连接交易所进行下单操作。正如我们在输入输出那节课说的那样,黑箱的特性是输入和输出。每一个设计网络交互的同学,都需要在大脑中形成清晰的交互状态图:
* 知道包是怎样在网络间传递的;
* 知道每一个节点是如何处理不同的输入包,然后输出并分发给下一级的。
在你搞不明白的时候,可以先在草稿纸上画出交互拓扑图,标注清楚每个节点的输入和输出格式,然后想清楚网络是怎么流动的。这一点,对网络编程至关重要。
现在,我假设你对网络编程只有很基本的了解。所以接下来,我将先从 REST 的定义讲起,然后过渡到具体的交互方式——如何通过 Python 和交易所进行交互,从而执行下单、撤单、查询订单等网络交互方式。
## REST 简介
什么是 REST API什么是 Socket有过网络编程经验的同学一定对这两个词汇不陌生。
REST的全称是表征层状态转移REpresentational State Transfer本意是指一种操作资源方法。不过你不用纠结于这个绕口的名字。换种方式来说REST的实质可以理解为通过URL定位资源用GET、POST、PUT、DELETE等动词来描述操作。而满足REST要求的接口就被称为RESTful的接口。
为了方便你更容易理解这些概念,这里我举个例子来类比。小明同学不是很聪明但很懂事,每天会在他的妈妈下班回来后给妈妈泡茶。刚开始,他的妈妈会发出这样的要求:
> 用红色杯子去厨房泡一杯放了糖的37.5度的普洱茶。
可是小明同学不够聪明,很难理解这个定语很多的句子。于是,他妈妈为了让他更简单明白需要做的事情,把这个指令设计成了更简洁的样子:
> 泡厨房的茶,要求如下:
>
> 1. 类型=普洱;
> 2. 杯子=红色;
> 3. 放糖=True
> 4. 温度=37.5度。
这里的“茶”就是资源,“**厨房的茶**”就是资源的地址URI“**泡**”是动词后面的要求都是接口参数。这样的一个接口就是小明提供的一个REST接口。
如果小明是一台机器那么解析这个请求就会非常容易而我们作为维护者查看小明的代码也很简单。当小明把这个接口暴露到网上时这就是一个RESTful的接口。
总的来说RESTful接口通常以HTTP GET和POST形式出现。但并非所有的GET、POST请求接口都是RESTful的接口。
这话可能有些拗口我们举个例子来看。上节课中我们获取了Gemini交易所中BTC对USD价格的ticker接口
```
GET https://api.gemini.com/v1/pubticker/btcusd
```
这里的“GET”是动词后边的URI是“Ticker”这个资源的地址。所以这是一个RESTful的接口。
但下面这样的接口就不是一个严格的RESTful接口
```
POST https://api.restful.cn/accounts/delete/:username
```
因为URI中包含动词“delete”删除所以这个URI并不是**指向一个资源**。如果要修改成严格的RESTful接口我们可以把它改成下面这样
```
DELETE https://api.rest.cn/accounts/:username
```
然后我们带着这个观念去看Gemini的取消订单接口
```
POST https://api.gemini.com/v1/order/cancel
```
![](https://static001.geekbang.org/resource/image/c6/8f/c6851d2560b5f67f29c750cyy018738f.png?wh=1132x614)
你会发现这个接口不够“RESTful”的地方有
* 动词设计不准确接口使用“POST”而不是重用HTTP动词“DELETE”
* URI里包含动词cancel
* ID代表的订单是**资源**但订单ID是放在**参数列表**而不是**URI**里的因此URI并没有指向资源。
所以严格来说这不是一个RESTful的接口。
此外如果我们去检查Gemini的其他私有接口Private私有接口是指需要附加身份验证信息才能访问的接口我们会发现那些接口的设计都不是严格RESTful的。不仅如此大部分的交易所比如Bitmex、Bitfinex、OKCoin等等它们提供的“REST接口”也都不是严格RESTful的。这些接口之所以还能被称为“REST接口”是因为他们大部分满足了REST接口的另一个重要要求**无状态**。
无状态的意思是每个REST请求都是独立的不需要服务器在会话Session中缓存中间状态来完成这个请求。简单来说如果服务器A接收到请求的时候宕机了而此时把这个请求发送给交易所的服务器B也能继续完成那么这个接口就是无状态的。
这里我再给你举一个简单的有状态的接口的例子。服务器要求在客户端请求取消订单的时候必须发送两次不一样的HTTP请求。并且第一次发送让服务器“等待取消”第二次发送“确认取消”。那么就算这个接口满足了RESTful的动词、资源分离原则也不是一个REST接口。
当然对于交易所的REST接口你并不需要过于纠结“RESTful”这个概念否则很容易就被这些名词给绕晕了。你只需要把握住最核心的一点**一个HTTP请求完成一次完整操作**。
## 交易所 API 简介
现在,你对 REST 和 Web Socket 应该有一个大致了解了吧。接下来,我们就开始做点有意思的事情。
首先,我来介绍一下交易所是什么。区块链交易所是个撮合交易平台: 它兼容了传统撮合规则撮合引擎,将资金托管和交割方式替换为区块链。数字资产交易所,则是一个中心化的平台,通过 Web 页面或 PC、手机客户端的形式让用户将数字资产充值到指定钱包地址交易所创建的钱包然后在平台挂买单、卖单以实现数字资产之间的兑换。
通俗来说,交易所就是一个买和卖的菜市场。有人在摊位上大声喊着:“二斤羊肉啊,二斤羊肉,四斤牛肉来换!”这种人被称为 maker挂单者。有的人则游走于不同摊位不动声色地掏出两斤牛肉顺手拿走一斤羊肉。这种人被称为 taker吃单者
交易所存在的意义,一方面是为 maker 和 taker 提供足够的空间活动;另一方面,让一个名叫撮合引擎的玩意儿,尽可能地把单子撮合在一起,然后收取一定比例的保护费…啊不对,是手续费,从而保障游戏继续进行下去。
市场显然是个很伟大的发明,这里我们就不进行更深入的哲学讨论了。
然后,我再来介绍一个叫作 Gemini 的交易所。Gemini双子星交易所全球首个获得合法经营许可的、首个推出期货合约的、专注于撮合大宗交易的数字货币交易所。Gemini 位于纽约是一家数字货币交易所和托管机构允许客户交易和存储数字资产并直接受纽约州金融服务部门NYDFS的监管。
Gemini 的界面清晰API 完整而易用,更重要的是,还提供了完整的测试网络,也就是说,功能和正常的 Gemini 完全一样。但是他家的交易采用虚拟币,非常方便从业者在平台上进行对接测试。
另一个做得很好的交易所,是 Bitmex他家的 API UI 界面和测试网络也是币圈一流。不过,鉴于这家是期货交易所,对于量化初学者来说有一定的门槛,我们还是选择 Gemini 更方便一些。
在进入正题之前我们最后再以比特币和美元之间的交易为例介绍四个基本概念orderbook 的概念这里就不介绍了,你也不用深究,你只需要知道比特币的价格是什么就行了)。
*buy用美元买入比特币的行为。
*sell用比特币换取美元的行为。
* 市价单market order给交易所一个方向买或者卖和一个数量交易所把给定数量的美元或者比特币换成比特币或者美元的单子。
* 限价单limit order给交易所一个价格、一个方向买或者卖和一个数量交易所在价格达到给定价格的时候把给定数量的美元或者比特币换成比特币或者美元的单子。
这几个概念都不难懂。其中,市价单和限价单,最大的区别在于,限价单多了一个给定价格。如何理解这一点呢?我们可以来看下面这个例子。
小明在某一天中午12:00:00告诉交易所我要用1000美元买比特币。交易所收到消息在 12:00:01 回复小明,现在你的账户多了 0.099 个比特币,少了 1000 美元,交易成功。这是一个市价买单。
而小强在某一天中午 11:59:00告诉交易所我要挂一个单子数量为 0.1 比特币1个比特币的价格为 10000 美元低于这个价格不卖。交易所收到消息在11:59:01 告诉小强,挂单成功,你的账户余额中 0.1 比特币的资金被冻结。又过了一分钟交易所告诉小强你的单子被完全执行了fully executed现在你的账户多了 1000 美元,少了 0.1 个比特币。这就是一个限价卖单。
(这里肯定有人发现不对了:貌似少了一部分比特币,到底去哪儿了呢?嘿嘿,你不妨自己猜猜看。)
显然,市价单,在交给交易所后,会立刻得到执行,当然执行价格也并不受你的控制。它很快,但是也非常不安全。而限价单,则限定了交易价格和数量,安全性相对高很多。缺点呢,自然就是如果市场朝相反方向走,你挂的单子可能没有任何人去接,也就变成了干吆喝却没人买。因为我没有讲解 orderbook所以这里的说辞不完全严谨但是对于初学者理解今天的内容已经够用了。
储备了这么久的基础知识想必你已经跃跃欲试了吧下面我们正式进入正题手把手教你使用API下单。
## 手把手教你使用 API 下单
手动挂单显然太慢,也不符合量化交易的初衷。我们就来看看如何用代码实现自动化下单吧。
第一步,你需要做的是,注册一个 Gemini Sandbox 账号。请放心,这个测试账号不需要你充值任何金额,注册后即送大量虚拟现金。这口吻是不是听着特像网游宣传语,接下来就是“快来贪玩蓝月里找我吧”?哈哈,不过这个设定确实如此,所以赶紧来注册一个吧。
注册后,为了满足好奇,你可以先尝试着使用 Web 界面自行下单。不过,事实上,未解锁的情况下是无法正常下单的,因此这样尝试并没啥太大意义。
所以第二步,我们需要来配置 API Key。菜单栏User Settings->API Settings然后点 GENERATE A NEW ACCOUNT API KEY记下 Key 和 Secret 这两串字符。因为窗口一旦消失,这两个信息就再也找不到了,需要你重新生成。
配置到此结束。接下来,我们来看具体实现。
先强调一点,在量化系统开发的时候,你的心中一定要有清晰的数据流图。下单逻辑是一个很简单的 RESTful 的过程,和你在网页操作的一样,构造你的请求订单、加密请求,然后 POST 给 gemini 交易所即可。
不过,因为涉及到的知识点较多,带你一步一步从零来写代码显然不太现实。所以,我们采用“先读懂后记忆并使用”的方法来学,下面即为这段代码:
```
import requests
import json
import base64
import hmac
import hashlib
import datetime
import time
base_url = "https://api.sandbox.gemini.com"
endpoint = "/v1/order/new"
url = base_url + endpoint
gemini_api_key = "account-zmidXEwP72yLSSybXVvn"
gemini_api_secret = "375b97HfE7E4tL8YaP3SJ239Pky9".encode()
t = datetime.datetime.now()
payload_nonce = str(int(time.mktime(t.timetuple())*1000))
payload = {
"request": "/v1/order/new",
"nonce": payload_nonce,
"symbol": "btcusd",
"amount": "5",
"price": "3633.00",
"side": "buy",
"type": "exchange limit",
"options": ["maker-or-cancel"]
}
encoded_payload = json.dumps(payload).encode()
b64 = base64.b64encode(encoded_payload)
signature = hmac.new(gemini_api_secret, b64, hashlib.sha384).hexdigest()
request_headers = {
'Content-Type': "text/plain",
'Content-Length': "0",
'X-GEMINI-APIKEY': gemini_api_key,
'X-GEMINI-PAYLOAD': b64,
'X-GEMINI-SIGNATURE': signature,
'Cache-Control': "no-cache"
}
response = requests.post(url,
data=None,
headers=request_headers)
new_order = response.json()
print(new_order)
########## 输出 ##########
{'order_id': '239088767', 'id': '239088767', 'symbol': 'btcusd', 'exchange': 'gemini', 'avg_execution_price': '0.00', 'side': 'buy', 'type': 'exchange limit', 'timestamp': '1561956976', 'timestampms': 1561956976535, 'is_live': True, 'is_cancelled': False, 'is_hidden': False, 'was_forced': False, 'executed_amount': '0', 'remaining_amount': '5', 'options': ['maker-or-cancel'], 'price': '3633.00', 'original_amount': '5'}
```
我们来深入看一下这段代码。
RESTful 的 POST 请求,通过 `requests.post` 来实现。post 接受三个参数url、data 和 headers。
这里的 url 等价于 `https://api.sandbox.gemini.com/v1/order/new`,但是在代码中分两部分写。第一部分是交易所 API 地址;第二部分,以斜杠开头,用来表示统一的 API endpoint。我们也可以在其他交易所的 API 中看到类似的写法,两者连接在一起,就构成了最终的 url。
而接下来大段命令的目的,是为了构造 request\_headers。
这里我简单说一下 HTTP request这是互联网中基于 TCP 的基础协议。HTTP 协议是 Hyper Text Transfer Protocol超文本传输协议的缩写用于从万维网WWW:World Wide Web服务器传输超文本到本地浏览器的传送协议。而 TCPTransmission Control Protocol则是面向连接的、可靠的、基于字节流的传输层通信协议。
多提一句,如果你开发网络程序,建议利用闲暇时间认真读一读《计算机网络:自顶向下方法》这本书,它也是国内外计算机专业必修课中广泛采用的课本之一。一边学习,一边应用,对于初学者的能力提升是全面而充分的。
回到 HTTP它的主要特点是连接简单、灵活可以使用“简单请求收到回复然后断开连接”的方式也是一种无状态的协议因此充分符合 RESTful 的思想。
HTTP 发送需要一个请求头request header也就是代码中的 request\_headers用 Python 的语言表示,就是一个 str 对 str 的字典。
这个字典里,有一些字段有特殊用途, `'Content-Type': "text/plain"``'Content-Length': "0"` 描述 Content 的类型和长度,这里的 Content 对应于参数 data。但是 Gemini 这里的 request 的 data 没有任何用处,因此长度为 0。
还有一些其他字段,例如 `'keep-alive'` 来表示连接是否可持续化等,你也可以适当注意一下。要知道,网络编程很多 bug 都会出现在不起眼的细节之处。
继续往下走看代码。payload 是一个很重要的字典,它用来存储下单操作需要的所有的信息,也就是业务逻辑信息。这里我们可以下一个 limit buy限价买单价格为 3633 刀。
另外,请注意 nonce这是个很关键并且在网络通信中很常见的字段。
因为网络通信是不可靠的,一个信息包有可能会丢失,也有可能重复发送,在金融操作中,这两者都会造成很严重的后果。丢包的话,我们重新发送就行了;但是重复的包,我们需要去重。虽然 TCP 在某种程度上可以保证但为了在应用层面进一步减少错误发生的机会Gemini 交易所要求所有的通信 payload 必须带有 nonce。
nonce 是个单调递增的整数。当某个后来的请求的 nonce比上一个成功收到的请求的 nouce 小或者相等的时候Gemini 便会拒绝这次请求。这样一来,重复的包就不会被执行两次了。另一方面,这样也可以在一定程度上防止中间人攻击:
* 一则是因为 nonce 的加入,使得加密后的同样订单的加密文本完全混乱;
* 二则是因为,这会使得中间人无法通过“发送同样的包来构造重复订单”进行攻击。
这样的设计思路是不是很巧妙呢?这就相当于每个包都增加了一个身份识别,可以极大地提高安全性。希望你也可以多注意,多思考一下这些巧妙的用法。
接下来的代码就很清晰了。我们要对 payload 进行 base64 和 sha384 算法非对称加密,其中 gemini\_api\_secret 为私钥;而交易所存储着公钥,可以对你发送的请求进行解密。最后,代码再将加密后的请求封装到 request\_headers 中,发送给交易所,并收到 response这个订单就完成了。
## 总结
这节课我们介绍了什么是 RESTful API带你了解了交易所的 RESTful API 是如何工作的,以及如何通过 RESTful API 来下单。同时,我简单讲述了网络编程中的一些技巧操作,希望你在网络编程中要注意思考每一个细节,尽可能在写代码之前,对业务逻辑和具体的技术细节有足够清晰的认识。
下一节,我们同样将从 Web Socket 的定义开始,讲解量化交易中数据模块的具体实现。
## 思考题
最后留一个思考题。今天的内容里,能不能使用 timestamp 代替 nonce为什么欢迎留言写下你的思考也欢迎你把这篇文章分享出去。