<audio id="audio" title="34 | RESTful & Socket: 搭建交易执行层核心" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e1/a1/e1ec2c9c2f946689f0f84445da687ea1.mp3"></audio>

你好，我是景霄。

上一节，我们简单介绍了量化交易的历史、严谨的定义和它的基本组成结构。有了这些高层次的基本知识，接下来我们就分模块，开始讲解量化交易系统中具体的每部分。

从这节课开始，我们将实打实地从代码出发，一步步设计出一套清晰完整、易于理解的量化交易系统。

一个量化交易系统，可以说是一个黑箱。这个黑箱连接交易所获取到的数据，通过策略运算，然后再连接交易所进行下单操作。正如我们在输入输出那节课说的那样，黑箱的特性是输入和输出。每一个设计网络交互的同学，都需要在大脑中形成清晰的交互状态图：

- 知道包是怎样在网络间传递的；
- 知道每一个节点是如何处理不同的输入包，然后输出并分发给下一级的。

在你搞不明白的时候，可以先在草稿纸上画出交互拓扑图，标注清楚每个节点的输入和输出格式，然后想清楚网络是怎么流动的。这一点，对网络编程至关重要。

现在，我假设你对网络编程只有很基本的了解。所以接下来，我将先从 REST 的定义讲起，然后过渡到具体的交互方式——如何通过 Python 和交易所进行交互，从而执行下单、撤单、查询订单等网络交互方式。

## REST 简介

什么是 REST API？什么是 Socket？有过网络编程经验的同学，一定对这两个词汇不陌生。

REST的全称是表征层状态转移（REpresentational State Transfer），本意是指一种操作资源方法。不过，你不用纠结于这个绕口的名字。换种方式来说，REST的实质可以理解为：通过URL定位资源，用GET、POST、PUT、DELETE等动词来描述操作。而满足REST要求的接口，就被称为RESTful的接口。

为了方便你更容易理解这些概念，这里我举个例子来类比。小明同学不是很聪明但很懂事，每天会在他的妈妈下班回来后给妈妈泡茶。刚开始，他的妈妈会发出这样的要求：

```
用红色杯子，去厨房泡一杯放了糖的37.5度的普洱茶。


```

可是小明同学不够聪明，很难理解这个定语很多的句子。于是，他妈妈为了让他更简单明白需要做的事情，把这个指令设计成了更简洁的样子：

```
泡厨房的茶，


要求：
类型=普洱；
杯子=红色；
放糖=True；
温度=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

```

<img src="https://static001.geekbang.org/resource/image/ae/89/ae9a7db308f54b3ca4f8ef140e3f9189.png" alt=""><br>
注：Private API Invocation的网址链接为[https://docs.gemini.com/rest-api/#private-api-invocation](https://docs.gemini.com/rest-api/#private-api-invocation)

你会发现，这个接口不够“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 = &quot;https://api.sandbox.gemini.com&quot;
endpoint = &quot;/v1/order/new&quot;
url = base_url + endpoint

gemini_api_key = &quot;account-zmidXEwP72yLSSybXVvn&quot;
gemini_api_secret = &quot;375b97HfE7E4tL8YaP3SJ239Pky9&quot;.encode()

t = datetime.datetime.now()
payload_nonce = str(int(time.mktime(t.timetuple())*1000))

payload = {
   &quot;request&quot;: &quot;/v1/order/new&quot;,
   &quot;nonce&quot;: payload_nonce,
   &quot;symbol&quot;: &quot;btcusd&quot;,
   &quot;amount&quot;: &quot;5&quot;,
   &quot;price&quot;: &quot;3633.00&quot;,
   &quot;side&quot;: &quot;buy&quot;,
   &quot;type&quot;: &quot;exchange limit&quot;,
   &quot;options&quot;: [&quot;maker-or-cancel&quot;]
}

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': &quot;text/plain&quot;,
    'Content-Length': &quot;0&quot;,
    'X-GEMINI-APIKEY': gemini_api_key,
    'X-GEMINI-PAYLOAD': b64,
    'X-GEMINI-SIGNATURE': signature,
    'Cache-Control': &quot;no-cache&quot;
}

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](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 ）服务器传输超文本到本地浏览器的传送协议。而 TCP（Transmission 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？为什么？欢迎留言写下你的思考，也欢迎你把这篇文章分享出去。


