This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="33 | 带你初探量化世界" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2c/27/2c9f214d71c1df7b61e05cfe3b62dd27.mp3"></audio>
你好,我是景霄。
在2000 年顶峰时期,高盛雇佣了 600 名交易员为机构客户买卖现金股票。可如今,这个数字只有 2 名Ref. 经济学人。到了2009 年,金融危机余音未散,专家面对股票和证券交易中越来越多的机器参与提出了警告,因为机器的崛起,逐渐导致了手操交易工作的消失。
很久之前瑞银集团UBS的交易大厅是下面这样的
<img src="https://static001.geekbang.org/resource/image/38/f7/3814a699e324b7958a08c793a5d58df7.png" alt="">
8 年之后,交易大厅就已经只有这些人了:
<img src="https://static001.geekbang.org/resource/image/59/af/59390119c9f7cb17ca9642bf2d30b3af.jpg" alt="">
事实上,随着数据处理技术的飞速发展,和量化交易模型研究理论的逐渐成熟,现金股票交易、债券市场、期货市场以及投行的相当一部分业务,都在朝着自动化的方向迈进。
而发展到2017 年WannyCry 席卷全球,随之而来的比特币,在短短几个月内从小众玩家走入了公众视野,币价也是一飞冲天,很多炒币的人赚得盆满钵满。更有一部分人,将金融业的量化策略应用其中,无论是搬砖(套利),还是波段,在不成熟的市场初期都赚了个爽快。
这节课开始,我们就来探索一下量化的世界。作为我们 Python 专栏的综合实践模块,希望你能在这一部分内容中,收获自己独特的东西。
## 交易是什么?
市场,是人类有史以来最伟大的发明之一。亚当·斯密在国富论中,用“看不见的手”这个概念,生动形象地阐释了市场和理性人之间是如何交互,最终让整个社会受益的。
而市场的核心,是交换。人类发展最开始是物物交换,原始的“以物易物”的方式产生于货币诞生之前。不过,这种方式非常低效,不便于流通交换,经常会出现的情况是,要走很长的交换链条才能拿到自己想要的物品。于是,一般等价物出现了,社会分工也逐渐出现了。人们把自己生产的商品换成一般等价物,然后再换成自己需要的其他商品。
而交换的核心,就是买和卖。当买卖双方对价格预期相等的时候,交易达成。随着金融和技术的发展,逐渐出现了股票、债券、期权、期货等越来越多的金融工具,金融衍生品也朝着复杂的方向发展。
在我们经常听到的投资银行中,量化基金交易员这种角色,所做的事情,就是在这些复杂的衍生品基础上,分析投资标的的价值,然后以某种策略来管理持有仓位,进行买进和卖出。
为什么交易能赚钱,是很多人疑惑不解的地方。市场究竟有没有规律可循呢?可以肯定是有的,但虽有迹可循却无法可依。交易的多样性和人性的复杂性,使得金融数据的噪音极大,我们无法简单地从某一两个因子来确定地推导行情变化。
所以交易员这个行业本身,对自身素质要求是极高的。除了要具备扎实的专业素养(包括金融功底、数理逻辑、分析能力、决策能力),对心理素质的要求也非常高。这种直接和钱打交道、并直面人性深处欲望的行业,也因此吸引了无数高手的参与,很多人因此暴富,也有不少人破产,一无所有。
那么,有什么办法可以规避这种,因为心理素质原因带来的风险呢?
## 量化交易
回答这个问题之前我先插一句题外话。刚接触量化交易的朋友都很容易被这几个词绕晕量化交易Quantitative Trading、程序化交易Program Trading、算法交易Algo-Trading、高频交易High Frequency Trading和自动化交易平台Automated Trading System
虽然我遇到过不少行业内的人也混用这词,但是作为初学者来说,厘清这些术语还是很有帮助的。至少,在别人说出这些高大上的词时,我们心里不用犯怵了。
先来看程序化交易,它通常用计算机程序代替交易员,来具体执行金融产品的买卖。比如,一个基金经理需要卖出大量股票。如果直接挂一个大的卖单,可能会影响市场,那就用计算机程序拆分成小单慢慢执行。所以,量化交易的下层通常是程序交易。
而算法交易通常用于高频交易中。它指的是,通过算法快速判定买卖的时间点,快速买卖多个产品。
量化交易则通常是指使用数学、统计甚至机器学习的方法去找寻合适的买卖时机。所以在这个维度的定义之下算法交易、高频交易还有统计套利Statistical Arbitrage都可以算作量化交易。
简单而言,我们可以认为量化交易的涵盖范围最大。因此,**当你不确定用哪个词的时候,用量化交易就行了。**
回到我们刚刚的问题,规避心理素质原因带来的风险的方法,自然就是量化交易了。量化交易的好处显而易见。最直观来看,计算机不眠不休,不需要交易员实时操盘,满足了人们“躺着挣钱”的愿景。当然,这只是美好的想象,真要这么做的话,不久之后就要回工地搬砖了。现实场景中,成熟的量化交易也需要有人蹲守,适时干预,防止算法突然失效造成巨额的交易亏损。
在数字货币领域的交易,这一点更加显著。数字货币的交易在全球许多交易所进行,和股票不同,一支股票可能只在少数几个交易所交易,而一种数字货币可以在所有的交易所同时进行交易。同时,因为没有股市的开盘、收盘限制,数字货币的交易通常是 7 x 24 小时不眠不休,比前世的 “996 福报”凶残多了。要是真有交易员能在这个市场活下来,我们尊称一声“神仙”也不为过了。
多交易所交易也意味着全球数字货币市场每时每刻都是紧密相连的。一个市场、一个局部的巨大变动都会影响所有的市场。比如2017年朝鲜氢弹炸了的当天新闻还没出来隔壁韩国、日本的比特币价格马上拉升了一波再比如当比特币的负面消息半夜里传出来的时候其价格也马上跟着暴跌一波。
<img src="https://static001.geekbang.org/resource/image/bf/02/bfae52c5cc5252e006652d73a1e1b502.png" alt="">
因此,我们经常看到比特币的价格波动巨大。很有可能今天还是财富自由状态,明天的财富就没那么自由了。显然,在这种市场中交易,人力很难持久支撑,而量化交易就很合适了。
通常的电子盘交易比如股票数字货币是通过券商或者软件直接把买卖请求发送给交易所而算法交易的底层就是让程序来自动实现这类操作。券商或者交易所通常也会提供API接口给投资者。比如盈透证券Interactive Broker的接口就可以支持股票、期权的行情数据获取和交易而 Gemini、OKCoin等交易所也提供了对应的接口进行数字货币行情获取和交易。
Gemini交易所的[公开行情API](https://api.gemini.com/v1/pubticker/btcusd)就可以通过下面这种简单的HTTP GET请求来获取最近的比特币BTC对美元USD的价格和最近的成交量。
```
########## GEMINI行情接口 ##########
## https://api.gemini.com/v1/pubticker/:symbol
import json
import requests
gemini_ticker = 'https://api.gemini.com/v1/pubticker/{}'
symbol = 'btcusd'
btc_data = requests.get(gemini_ticker.format(symbol)).json()
print(json.dumps(btc_data, indent=4))
########## 输出 ##########
{
&quot;bid&quot;: &quot;8825.88&quot;,
&quot;ask&quot;: &quot;8827.52&quot;,
&quot;volume&quot;: {
&quot;BTC&quot;: &quot;910.0838782726&quot;,
&quot;USD&quot;: &quot;7972904.560901317851&quot;,
&quot;timestamp&quot;: 1560643800000
},
&quot;last&quot;: &quot;8838.45&quot;
}
```
对算法交易系统来说API只是最下层的结构。通常而言一个基本的交易系统应该包括行情模块、策略模块和执行模块。为了辅助策略的开发通常还有回测系统辅助。它们的分工示意图大致如下
<img src="https://static001.geekbang.org/resource/image/d2/f7/d290da8da3bb149858ce1a0be79215f7.png" alt="">
其中,
- 行情模块的主要功能是,尝试获取市场的行情数据,通常也负责获取交易账户的状态。
- 策略模块的主要功能是,订阅市场的数据,根据设定的算法发出买、卖指令给执行模块。
- 执行模块的主要功能是,接受并把策略模块发过来的买、卖指令封装并转发到交易所;同时,监督并确保策略买卖的完整执行。
## Python算法交易
了解了这么多关于量化交易的知识接下来我们就来说说Python算法交易。Python 在金融行业的许多方面都有用到,在算法交易领域,更是发挥了日益重要的作用。 Python 之所以能在这个行业这么流行,主要是因为下面四个原因。
### 数据分析能力
第一个原因是Python的数据分析能力。算法交易领域的一个基本需求就是高效数据处理能力而数据处理则是Python的强项。特别是NumPy+Pandas的组合简直让算法交易开发者的生活质量直线上升。
我们可以用一个简单的例子来展示一下如何抓取、格式化和绘制比特币过去一个小时在Gemini交易所的价格曲线。相关的代码我都附了详细注释这里就不再多讲你阅读了解一下即可。
```
import matplotlib.pyplot as plt
import pandas as pd
import requests
# 选择要获取的数据时间段
periods = '3600'
# 通过Http抓取btc历史价格数据
resp = requests.get('https://api.cryptowat.ch/markets/gemini/btcusd/ohlc',
params={
'periods': periods
})
data = resp.json()
# 转换成pandas data frame
df = pd.DataFrame(
data['result'][periods],
columns=[
'CloseTime',
'OpenPrice',
'HighPrice',
'LowPrice',
'ClosePrice',
'Volume',
'NA'])
# 输出DataFrame的头部几行
print(df.head())
# 绘制btc价格曲线
df['ClosePrice'].plot(figsize=(14, 7))
plt.show()
########### 输出 ###############
CloseTime OpenPrice HighPrice ... ClosePrice Volume NA
0 1558843200 8030.55 8046.30 ... 8011.20 11.642968 93432.459964
1 1558846800 8002.76 8050.33 ... 8034.48 8.575682 68870.145895
2 1558850400 8031.61 8036.14 ... 8000.00 15.659680 125384.519063
3 1558854000 8000.00 8016.29 ... 8001.46 38.171420 304342.048892
4 1558857600 8002.69 8023.11 ... 8009.24 3.582830 28716.385009
```
通过执行这样的一段代码,我们便可以得到下面这张图所示的价格曲线。
<img src="https://static001.geekbang.org/resource/image/8c/1f/8c7d9f54db86181f87422a3543747a1f.png" alt="">
### 大量专有库
除了强大的数据处理能力之外Python 还有许许多多已经开发成熟的算法交易库可供使用。比如你可以使用Zipline进行策略回测或者用Pyfolio进行投资组合分析。而许多交易所也都提供了基于Python的API客户端。
### 便利的交易平台
第三个原因,是因为便利的交易平台。有一些算法交易平台可以执行自定义 Python 策略,无需搭建量化交易框架。算法交易平台,实际上等效于帮用户完成了行情模块和执行模块。用户只需要在其中定义策略模块,即可进行算法交易和回测。
比如Quantopian就提供了基于Zipline的标准回测环境。用户可以选择Python作为开发语言并且和社区的网友分享自己的策略。此外国内也有诸如BigQuant、果仁网等类似平台提供不同市场和金融产品的交易。
### 广泛的行业应用
最后一个原因则是Python本身广泛的行业应用了。目前越来越多投资机构的交易部门都开始使用Python因此也对优秀的Python开发者产生了更多的需求。自然这也让学习Python成为了更有意义的“投资”。
## 总结
这一节课,我们介绍了交易,以及算法交易中的基本概念,也简单介绍了为什么要学习 Python 来搭建量化交易系统。量化交易是交易行业的大趋势;同时, Python 作为最适合量化从业者的语言之一,对于初学者而言也有着非常重要的地位。
接下来的几节课,我们将从细节深入量化交易的每一个模块,由浅入深地为你揭开量化交易神秘的面纱。
## 思考题
最后给你留一道思考题。高频交易和中低频交易,哪个更适合使用 Python为什么欢迎在留言区写下你的想法也欢迎你把这篇文章分享给更多对量化交易感兴趣的人我们一起交流和探讨。

View File

@@ -0,0 +1,256 @@
<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 )服务器传输超文本到本地浏览器的传送协议。而 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为什么欢迎留言写下你的思考也欢迎你把这篇文章分享出去。

View File

@@ -0,0 +1,328 @@
<audio id="audio" title="35 | RESTful & Socket: 行情数据对接和抓取" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/3c/60da5550a2124b3d0860e589af9ebf3c.mp3"></audio>
你好,我是景霄。
上一节课我们介绍了交易所的交易模式数字货币交易所RESTful接口的常见概念以及如何调用RESTful接口进行订单操作。众所周知买卖操作的前提是你需要已知市场的最新情况。这节课里我将介绍交易系统底层另一个最重要的部分行情数据的对接和抓取。
行情数据最重要的是实时性和有效性。市场的情况瞬息万变合适的买卖时间窗口可能只有几秒。在高频交易里合适的买卖机会甚至在毫秒级别。要知道一次从北京发往美国的网络请求即使是光速传播都需要几百毫秒的延迟。更别提用Python这种解释型语言建立HTTP连接导致的时间消耗。
经过上节课的学习你对交易应该有了基本的了解这也是我们今天学习的基础。接下来我们先从交易所撮合模式讲起然后介绍行情数据有哪些之后我将带你基于Websocket的行情数据来抓取模块。
## 行情数据
回顾上一节我们提到的,交易所是一个买方、卖方之间的公开撮合平台。买卖方把需要/可提供的商品数量和愿意出/接受的价格提交给交易所,交易所按照公平原则进行撮合交易。
那么撮合交易是怎么进行的呢?假设你是一个人肉比特币交易所,大量的交易订单往你这里汇总,你应该如何选择才能让交易公平呢?
显然,最直观的操作就是,把买卖订单分成两个表,按照价格由高到低排列。下面的图,就是买入和卖出的委托表。
<img src="https://static001.geekbang.org/resource/image/6b/2d/6bade6ffe3b8d439b7826cbe6d84a22d.png" alt="">
<img src="https://static001.geekbang.org/resource/image/0d/4c/0d7f5bcbb766097b84b7ad36d2b26a4c.png" alt="">
如果最高的买入价格小于最低的卖出价格,那就不会有任何交易发生。这通常是你看到的委托列表的常态。
如果最高的买入价格和最低的卖出价格相同那么就尝试进行撮合。比如BTC在9002.01就会发生撮合最后按照9002.01的价格成交0.0330个BTC。当然交易完成后小林未完成部分的订单余下0.1126 - 0.0330 = 0.0796 个 BTC 未卖出),还会继续在委托表里。
不过你可能会想,如果买入和卖出的价格有交叉,那么成交价格又是什么呢?事实上,这种情况并不会发生。我们来试想一下下面这样的场景。
如果你尝试给一个委托列表里加入一个新买入订单,它的价格比所有已有的最高买入价格高,也比所有的卖出价格高。那么此时,它会直接从最低的卖出价格撮合。等到最低价格的卖出订单吃完了,它便开始吃价格第二低的卖出订单,直到这个买入订单完全成交。反之亦然。所以,委托列表价格不会出现交叉。
当然,请注意,这里我说的只是限价订单的交易方式。而对于市价订单,交易规则会有一些轻微的区别,这里我就不详细解释了,主要是让你有个概念。
其实说到这里所谓的“交易所行情”概念就呼之欲出了。交易所主要有两种行情数据委托账本Order Book和活动行情Tick data
我们把委托表里的具体用户隐去,相同价格的订单合并,就得到了下面这种委托账本。我们主要观察右边的数字部分,其中:
- 上半部分里第一列红色数字代表BTC的卖出价格中间一列数字是这个价格区间的订单BTC总量最右边一栏是从最低卖出价格到当前价格区间的积累订单量。
- 中间的大字部分9994.10 USD是当前的市场价格也就是上一次成交交易的价格。
- 下面绿色部分的含义与上半部分类似,不过指的是买入委托和对应的数量。
<img src="https://static001.geekbang.org/resource/image/74/1e/740e88e95dcab334652f8761ca58171e.png" alt="">
这张图中,最低的卖出价格比最高的买入价格要高 6.51 USD这个价差通常被称为Spread。这里验证了我们前面提到的委托账本的价格永不交叉 同时Spread很小也能说明这是一个非常活跃的交易所。
每一次撮合发生意味着一笔交易Trade的发生。卖方买方都很开心于是交易所也很开心地通知行情数据的订阅者刚才发生了一笔交易交易的价格是多少成交数量是多少。这个数据就是活动行情Tick。
有了这些数据,我们也就掌握了这个交易所的当前状态,可以开始搞事情了。
## Websocket介绍
在本文的开头我们提到过行情数据很讲究时效性。所以行情从交易所产生到传播给我们的程序之间的延迟应该越低越好。通常交易所也提供了REST的行情数据抓取接口。比如下面这段代码
```
import requests
import timeit
def get_orderbook():
orderbook = requests.get(&quot;https://api.gemini.com/v1/book/btcusd&quot;).json()
n = 10
latency = timeit.timeit('get_orderbook()', setup='from __main__ import get_orderbook', number=n) * 1.0 / n
print('Latency is {} ms'.format(latency * 1000))
###### 输出 #######
Latency is 196.67642089999663 ms
```
我在美国纽约附近城市的一个服务器上测试了这段代码你可以看到平均每次访问orderbook的延迟有0.25秒左右。显然,如果在国内,这个延迟只会更大。按理说,这两个美国城市的距离很短,为什么延迟会这么大呢?
这是因为REST接口本质上是一个HTTP接口在这之下是TCP/TLS套接字Socket连接。每一次REST请求通常都会重新建立一次TCP/TLS握手然后在请求结束之后断开这个链接。这个过程比我们想象的要慢很多。
举个例子来验证这一点在同一个城市我们试验一下。我从纽约附近的服务器和Gemini在纽约的服务器进行连接TCP/SSL握手花了多少时间呢
```
curl -w &quot;TCP handshake: %{time_connect}s, SSL handshake: %{time_appconnect}s\n&quot; -so /dev/null https://www.gemini.com
TCP handshake: 0.072758s, SSL handshake: 0.119409s
```
结果显示HTTP连接构建的过程就占了一大半时间也就是说我们每次用REST请求都要浪费一大半的时间在和服务器建立连接上这显然是非常低效的。很自然的你会想到我们能否实现一次连接、多次通信呢
事实上Python的某些HTTP请求库也可以支持重用底层的TCP/SSL连接。但那种方法一来比较复杂二来也需要服务器的支持。该怎么办呢其实在有WebSocket的情况下我们完全不需要舍近求远。
我先来介绍一下WebSocket。WebSocket是一种在单个TCP/TLS连接上进行全双工、双向通信的协议。WebSocket可以让客户端与服务器之间的数据交换变得更加简单高效服务端也可以主动向客户端推送数据。在WebSocket API中浏览器和服务器只需要完成一次握手两者之间就可以直接创建持久性的连接并进行双向数据传输。
概念听着很痛快,不过还是有些抽象。为了让你快速理解刚刚的这段话,我们还是来看两个简单的例子。二话不说,先看一段代码:
```
import websocket
import thread
# 在接收到服务器发送消息时调用
def on_message(ws, message):
print('Received: ' + message)
# 在和服务器建立完成连接时调用
def on_open(ws):
# 线程运行函数
def gao():
# 往服务器依次发送0-4每次发送完休息0.01秒
for i in range(5):
time.sleep(0.01)
msg=&quot;{0}&quot;.format(i)
ws.send(msg)
print('Sent: ' + msg)
# 休息1秒用于接收服务器回复的消息
time.sleep(1)
# 关闭Websocket的连接
ws.close()
print(&quot;Websocket closed&quot;)
# 在另一个线程运行gao()函数
thread.start_new_thread(gao, ())
if __name__ == &quot;__main__&quot;:
ws = websocket.WebSocketApp(&quot;ws://echo.websocket.org/&quot;,
on_message = on_message,
on_open = on_open)
ws.run_forever()
#### 输出 #####
Sent: 0
Sent: 1
Received: 0
Sent: 2
Received: 1
Sent: 3
Received: 2
Sent: 4
Received: 3
Received: 4
Websocket closed
```
这段代码尝试和`wss://echo.websocket.org`建立连接。当连接建立的时候就会启动一条线程连续向服务器发送5条消息。
通过输出可以看出我们在连续发送的同时也在不断地接受消息。这并没有像REST一样每发送一个请求要等待服务器完成请求、完全回复之后再进行下一个请求。换句话说**我们在请求的同时也在接受消息**,这也就是前面所说的”全双工“。
<img src="https://static001.geekbang.org/resource/image/7b/b6/7bbb7936b56dcae7f1e5dfbc644b4fb6.png" alt="">
<img src="https://static001.geekbang.org/resource/image/9d/4c/9d4072dedfd5944a08e3bbee5059194c.png" alt="">
再来看第二段代码。为了解释”双向“我们来看看获取Gemini的委托账单的例子。
```
import ssl
import websocket
import json
# 全局计数器
count = 5
def on_message(ws, message):
global count
print(message)
count -= 1
# 接收了5次消息之后关闭websocket连接
if count == 0:
ws.close()
if __name__ == &quot;__main__&quot;:
ws = websocket.WebSocketApp(
&quot;wss://api.gemini.com/v1/marketdata/btcusd?top_of_book=true&amp;offers=true&quot;,
on_message=on_message)
ws.run_forever(sslopt={&quot;cert_reqs&quot;: ssl.CERT_NONE})
###### 输出 #######
{&quot;type&quot;:&quot;update&quot;,&quot;eventId&quot;:7275473603,&quot;socket_sequence&quot;:0,&quot;events&quot;:[{&quot;type&quot;:&quot;change&quot;,&quot;reason&quot;:&quot;initial&quot;,&quot;price&quot;:&quot;11386.12&quot;,&quot;delta&quot;:&quot;1.307&quot;,&quot;remaining&quot;:&quot;1.307&quot;,&quot;side&quot;:&quot;ask&quot;}]}
{&quot;type&quot;:&quot;update&quot;,&quot;eventId&quot;:7275475120,&quot;timestamp&quot;:1562380981,&quot;timestampms&quot;:1562380981991,&quot;socket_sequence&quot;:1,&quot;events&quot;:[{&quot;type&quot;:&quot;change&quot;,&quot;side&quot;:&quot;ask&quot;,&quot;price&quot;:&quot;11386.62&quot;,&quot;remaining&quot;:&quot;1&quot;,&quot;reason&quot;:&quot;top-of-book&quot;}]}
{&quot;type&quot;:&quot;update&quot;,&quot;eventId&quot;:7275475271,&quot;timestamp&quot;:1562380982,&quot;timestampms&quot;:1562380982387,&quot;socket_sequence&quot;:2,&quot;events&quot;:[{&quot;type&quot;:&quot;change&quot;,&quot;side&quot;:&quot;ask&quot;,&quot;price&quot;:&quot;11386.12&quot;,&quot;remaining&quot;:&quot;1.3148&quot;,&quot;reason&quot;:&quot;top-of-book&quot;}]}
{&quot;type&quot;:&quot;update&quot;,&quot;eventId&quot;:7275475838,&quot;timestamp&quot;:1562380986,&quot;timestampms&quot;:1562380986270,&quot;socket_sequence&quot;:3,&quot;events&quot;:[{&quot;type&quot;:&quot;change&quot;,&quot;side&quot;:&quot;ask&quot;,&quot;price&quot;:&quot;11387.16&quot;,&quot;remaining&quot;:&quot;0.072949&quot;,&quot;reason&quot;:&quot;top-of-book&quot;}]}
{&quot;type&quot;:&quot;update&quot;,&quot;eventId&quot;:7275475935,&quot;timestamp&quot;:1562380986,&quot;timestampms&quot;:1562380986767,&quot;socket_sequence&quot;:4,&quot;events&quot;:[{&quot;type&quot;:&quot;change&quot;,&quot;side&quot;:&quot;ask&quot;,&quot;price&quot;:&quot;11389.22&quot;,&quot;remaining&quot;:&quot;0.06204196&quot;,&quot;reason&quot;:&quot;top-of-book&quot;}]}
```
可以看到在和Gemini建立连接后我们并没有向服务器发送任何消息没有任何请求但是服务器却源源不断地向我们推送数据。这可比REST接口“每请求一次获得一次回复”的沟通方式高效多了
因此相对于REST来说Websocket是一种更加实时、高效的数据交换方式。当然缺点也很明显因为请求和回复是异步的这让我们程序的状态控制逻辑更加复杂。这一点后面的内容里我们会有更深刻的体会。
## 行情抓取模块
有了 Websocket 的基本概念,我们就掌握了和交易所连接的第二种方式。
事实上Gemini 提供了两种 Websocket 接口,一种是 Public 接口,一种为 Private 接口。
Public 接口,即公开接口,提供 orderbook 服务,即每个人都能看到的当前挂单价和深度,也就是我们这节课刚刚详细讲过的 orderbook。
而 Private 接口,和我们上节课讲的挂单操作有关,订单被完全执行、被部分执行等等其他变动,你都会得到通知。
我们以 orderbook 爬虫为例,先来看下如何抓取 orderbook 信息。下面的代码详细写了一个典型的爬虫,同时使用了类进行封装,希望你不要忘记我们这门课的目的,了解 Python 是如何应用于工程实践中的:
```
import copy
import json
import ssl
import time
import websocket
class OrderBook(object):
BIDS = 'bid'
ASKS = 'ask'
def __init__(self, limit=20):
self.limit = limit
# (price, amount)
self.bids = {}
self.asks = {}
self.bids_sorted = []
self.asks_sorted = []
def insert(self, price, amount, direction):
if direction == self.BIDS:
if amount == 0:
if price in self.bids:
del self.bids[price]
else:
self.bids[price] = amount
elif direction == self.ASKS:
if amount == 0:
if price in self.asks:
del self.asks[price]
else:
self.asks[price] = amount
else:
print('WARNING: unknown direction {}'.format(direction))
def sort_and_truncate(self):
# sort
self.bids_sorted = sorted([(price, amount) for price, amount in self.bids.items()], reverse=True)
self.asks_sorted = sorted([(price, amount) for price, amount in self.asks.items()])
# truncate
self.bids_sorted = self.bids_sorted[:self.limit]
self.asks_sorted = self.asks_sorted[:self.limit]
# copy back to bids and asks
self.bids = dict(self.bids_sorted)
self.asks = dict(self.asks_sorted)
def get_copy_of_bids_and_asks(self):
return copy.deepcopy(self.bids_sorted), copy.deepcopy(self.asks_sorted)
class Crawler:
def __init__(self, symbol, output_file):
self.orderbook = OrderBook(limit=10)
self.output_file = output_file
self.ws = websocket.WebSocketApp('wss://api.gemini.com/v1/marketdata/{}'.format(symbol),
on_message = lambda ws, message: self.on_message(message))
self.ws.run_forever(sslopt={'cert_reqs': ssl.CERT_NONE})
def on_message(self, message):
# 对收到的信息进行处理,然后送给 orderbook
data = json.loads(message)
for event in data['events']:
price, amount, direction = float(event['price']), float(event['remaining']), event['side']
self.orderbook.insert(price, amount, direction)
# 整理 orderbook排序只选取我们需要的前几个
self.orderbook.sort_and_truncate()
# 输出到文件
with open(self.output_file, 'a+') as f:
bids, asks = self.orderbook.get_copy_of_bids_and_asks()
output = {
'bids': bids,
'asks': asks,
'ts': int(time.time() * 1000)
}
f.write(json.dumps(output) + '\n')
if __name__ == '__main__':
crawler = Crawler(symbol='BTCUSD', output_file='BTCUSD.txt')
###### 输出 #######
{&quot;bids&quot;: [[11398.73, 0.96304843], [11398.72, 0.98914437], [11397.32, 1.0], [11396.13, 2.0], [11395.95, 2.0], [11395.87, 1.0], [11394.09, 0.11803397], [11394.08, 1.0], [11393.59, 0.1612581], [11392.96, 1.0]], &quot;asks&quot;: [[11407.42, 1.30814001], [11407.92, 1.0], [11409.48, 2.0], [11409.66, 2.0], [11412.15, 0.525], [11412.42, 1.0], [11413.77, 0.11803397], [11413.99, 0.5], [11414.28, 1.0], [11414.72, 1.0]], &quot;ts&quot;: 1562558996535}
{&quot;bids&quot;: [[11398.73, 0.96304843], [11398.72, 0.98914437], [11397.32, 1.0], [11396.13, 2.0], [11395.95, 2.0], [11395.87, 1.0], [11394.09, 0.11803397], [11394.08, 1.0], [11393.59, 0.1612581], [11392.96, 1.0]], &quot;asks&quot;: [[11407.42, 1.30814001], [11407.92, 1.0], [11409.48, 2.0], [11409.66, 2.0], [11412.15, 0.525], [11412.42, 1.0], [11413.77, 0.11803397], [11413.99, 0.5], [11414.28, 1.0], [11414.72, 1.0]], &quot;ts&quot;: 1562558997377}
{&quot;bids&quot;: [[11398.73, 0.96304843], [11398.72, 0.98914437], [11397.32, 1.0], [11396.13, 2.0], [11395.95, 2.0], [11395.87, 1.0], [11394.09, 0.11803397], [11394.08, 1.0], [11393.59, 0.1612581], [11392.96, 1.0]], &quot;asks&quot;: [[11407.42, 1.30814001], [11409.48, 2.0], [11409.66, 2.0], [11412.15, 0.525], [11412.42, 1.0], [11413.77, 0.11803397], [11413.99, 0.5], [11414.28, 1.0], [11414.72, 1.0]], &quot;ts&quot;: 1562558997765}
{&quot;bids&quot;: [[11398.73, 0.96304843], [11398.72, 0.98914437], [11397.32, 1.0], [11396.13, 2.0], [11395.95, 2.0], [11395.87, 1.0], [11394.09, 0.11803397], [11394.08, 1.0], [11393.59, 0.1612581], [11392.96, 1.0]], &quot;asks&quot;: [[11407.42, 1.30814001], [11409.48, 2.0], [11409.66, 2.0], [11412.15, 0.525], [11413.77, 0.11803397], [11413.99, 0.5], [11414.28, 1.0], [11414.72, 1.0]], &quot;ts&quot;: 1562558998638}
{&quot;bids&quot;: [[11398.73, 0.97131753], [11398.72, 0.98914437], [11397.32, 1.0], [11396.13, 2.0], [11395.95, 2.0], [11395.87, 1.0], [11394.09, 0.11803397], [11394.08, 1.0], [11393.59, 0.1612581], [11392.96, 1.0]], &quot;asks&quot;: [[11407.42, 1.30814001], [11409.48, 2.0], [11409.66, 2.0], [11412.15, 0.525], [11413.77, 0.11803397], [11413.99, 0.5], [11414.28, 1.0], [11414.72, 1.0]], &quot;ts&quot;: 1562558998645}
{&quot;bids&quot;: [[11398.73, 0.97131753], [11398.72, 0.98914437], [11397.32, 1.0], [11396.13, 2.0], [11395.87, 1.0], [11394.09, 0.11803397], [11394.08, 1.0], [11393.59, 0.1612581], [11392.96, 1.0]], &quot;asks&quot;: [[11407.42, 1.30814001], [11409.48, 2.0], [11409.66, 2.0], [11412.15, 0.525], [11413.77, 0.11803397], [11413.99, 0.5], [11414.28, 1.0], [11414.72, 1.0]], &quot;ts&quot;: 1562558998748}
```
代码比较长,接下来我们具体解释一下。
这段代码的最开始,封装了一个叫做 orderbook 的 class专门用来存放与之相关的数据结构。其中的 bids 和 asks 两个字典,用来存储当前时刻下的买方挂单和卖方挂单。
此外,我们还专门维护了一个排过序的 bids_sorted 和 asks_sorted。构造函数有一个参数 limit用来指示 orderbook 的 bids 和 asks 保留多少条数据。对于很多策略top 5 的数据往往足够,这里我们选择的是前 10 个。
再往下看insert() 函数用于向 orderbook 插入一条数据。需要注意,这里的逻辑是,如果某个 price 对应的 amount 是 0那么意味着这一条数据已经不存在了删除即可。insert 的数据可能是乱序的,因此在需要的时候,我们要对 bids 和 asks 进行排序,然后选取前面指定数量的数据。这其实就是 sort_and_truncate() 函数的作用,调用它来对 bids 和 asks 排序后截取,最后保存回 bids 和 asks。
接下来的 get_copy_of_bids_and_asks()函数,用来返回排过序的 bids 和 asks 数组。这里使用深拷贝,是因为如果直接返回,将会返回 bids_sorted 和 asks_sorted 的指针;那么,在下一次调用 sort_and_truncate() 函数的时候,两个数组的内容将会被改变,这就造成了潜在的 bug。
最后来看一下 Crawler 类。构造函数声明 orderbook然后定义 Websocket 用来接收交易所数据。这里需要注意的一点是,回调函数 on_message() 是一个类成员函数。因此,应该你注意到了,它的第一个参数是 self这里如果直接写成 `on_message = self.on_message` 将会出错。
为了避免这个问题,我们需要将函数再次包装一下。这里我使用了前面学过的匿名函数,来传递中间状态,注意我们只需要 message因此传入 message 即可。
剩下的部分就很清晰了on_message 回调函数在收到一个新的 tick 时,先将信息解码,枚举收到的所有改变;然后插入 orderbook排序最后连同 timestamp 一并输出即可。
虽然这段代码看起来挺长,但是经过我这么一分解,是不是发现都是学过的知识点呢?这也是我一再强调基础的原因,如果对你来说哪部分内容变得陌生了(比如面向对象编程的知识点),一定要记得及时往前复习,这样你学起新的更复杂的东西,才能轻松很多。
回到正题。刚刚的代码,主要是为了抓取 orderbook 的信息。事实上Gemini 交易所在建立数据流 Websocket 的时候,第一条信息往往非常大,因为里面包含了那个时刻所有的 orderbook 信息。这就叫做初始数据。之后的消息,都是基于初始数据进行修改的,直接处理即可。
## 总结
这节课我们继承上一节,从委托账本讲起,然后讲述了 WebSocket 的定义、工作机制和使用方法,最后以一个例子收尾,带你学会如何爬取 Orderbook 的信息。希望你在学习这节课的内容时,能够和上节课的内容联系起来,仔细思考 Websocket 和 RESTFul 的区别,并试着总结网络编程中不同模型的适用范围。
## 思考题
最后给你留一道思考题。WebSocket 会丢包吗?如果丢包的话, Orderbook 爬虫又会发生什么?这一点应该如何避免呢?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。

View File

@@ -0,0 +1,566 @@
<audio id="audio" title="36 | Pandas & Numpy: 策略与回测系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/b1/34f231fbb696483eba5c1f1dfa12a2b1.mp3"></audio>
大家好,我是景霄。
上节课我们介绍了交易所的数据抓取特别是orderbook和tick数据的抓取。今天这节课我们考虑的是怎么在这些历史数据上测试一个交易策略。
首先我们要明确对于很多策略来说我们上节课抓取的密集的orderbook和tick数据并不能简单地直接使用。因为数据量太密集包含了太多细节而且长时间连接时网络随机出现的不稳定会导致丢失部分tick数据。因此我们还需要进行合适的清洗、聚合等操作。
此外为了进行回测我们需要一个交易策略还需要一个测试框架。目前已存在很多成熟的回测框架但是为了Python学习我决定带你搭建一个简单的回测框架并且从中简单一窥Pandas的优势。
## OHLCV数据
了解过一些股票交易的同学可能知道K线这种东西。K线又称“蜡烛线”是一种反映价格走势的图线。它的特色在于一个线段内记录了多项讯息相当易读易懂且实用有效因此被广泛用于股票、期货、贵金属、数字货币等行情的技术分析。下面便是一个K线示意图。
<img src="https://static001.geekbang.org/resource/image/47/9b/470a68b8eaff3807efd89bc616e5659b.png" alt="">
其中每一个小蜡烛都代表着当天的开盘价Open、最高价High、最低价Low和收盘价Close也就是我画的第二张图表示的这样。
<img src="https://static001.geekbang.org/resource/image/58/57/58ce87e32aa4655211da02ce88223757.png" alt="">
类似的除了日K线之外还有周K线、小时K线、分钟K线等等。那么这个K线是怎么计算来的呢
我们以小时K线图为例还记得我们当时抓取的tick数据吗也就是每一笔交易的价格和数量。那么如果从上午10:00开始我们开始积累tick的交易数据以10:00开始的第一个交易作为Open数据11:00前的最后一笔交易作为Close值并把这一个小时最低和最高的成交价格分别作为High和Low的值我们就可以绘制出这一个小时对应的“小蜡烛”形状了。
如果再加上这一个小时总的成交量Volumn就得到了OHLCV数据。
所以如果我们一直抓取着tick底层原始数据我们就能在上层聚合出1分钟K线、小时K线以及日、周k线等等。如果你对这一部分操作有兴趣可以把此作为今天的课后作业来实践。
接下来我们将使用Gemini从2015年到2019年7月这个时间内BTC对USD每个小时的OHLCV数据作为策略和回测的输入。你可以在[这里](https://github.com/caunion/simple_backtesting/blob/master/BTCUSD_GEMINI.csv)下载数据。
数据下载完成后我们可以利用Pandas读取比如下面这段代码。
```
def assert_msg(condition, msg):
if not condition:
raise Exception(msg)
def read_file(filename):
# 获得文件绝对路径
filepath = path.join(path.dirname(__file__), filename)
# 判定文件是否存在
assert_msg(path.exists(filepath), &quot;文件不存在&quot;)
# 读取CSV文件并返回
return pd.read_csv(filepath,
index_col=0,
parse_dates=True,
infer_datetime_format=True)
BTCUSD = read_file('BTCUSD_GEMINI.csv')
assert_msg(BTCUSD.__len__() &gt; 0, '读取失败')
print(BTCUSD.head())
########## 输出 ##########
Time Symbol Open High Low Close Volume
Date
2019-07-08 00:00:00 BTCUSD 11475.07 11540.33 11469.53 11506.43 10.770731
2019-07-07 23:00:00 BTCUSD 11423.00 11482.72 11423.00 11475.07 32.996559
2019-07-07 22:00:00 BTCUSD 11526.25 11572.74 11333.59 11423.00 48.937730
2019-07-07 21:00:00 BTCUSD 11515.80 11562.65 11478.20 11526.25 25.323908
2019-07-07 20:00:00 BTCUSD 11547.98 11624.88 11423.94 11515.80 63.211972
```
这段代码提供了两个工具函数。
- 一个是read_file它的作用是用pandas读取csv文件。
- 另一个是assert_msg它的作用类似于assert如果传入的条件contidtion为否就会抛出异常。不过你需要提供一个参数用于指定要抛出的异常信息。
## 回测框架
说完了数据我们接着来看回测数据。常见的回测框架有两类。一类是向量化回测框架它通常基于Pandas+Numpy来自己搭建计算核心后端则是用MySQL或者MongoDB作为源。这种框架通过Pandas+Numpy对OHLC数组进行向量运算可以在较长的历史数据上进行回测。不过因为这类框架一般只用OHLC所以模拟会比较粗糙。
另一类则是事件驱动型回测框架。这类框架本质上是针对每一个tick的变动或者orderbook的变动生成事件然后再把一个个事件交给策略进行执行。因此虽然它的拓展性很强可以允许更加灵活的策略但回测速度是很慢的。
我们想要学习量化交易,使用大型成熟的回测框架,自然是第一选择。
- 比如Zipline就是一个热门的事件驱动型回测框架背后有大型社区和文档的支持。
- PyAlgoTrade也是事件驱动的回测框架文档相对完整整合了知名的技术分析Techique Analysis库TA-Lib。在速度和灵活方面它比Zipline 强。不过,它的一大硬伤是不支持 Pandas 的模块和对象。
显然对于我们Python学习者来说第一类也就是向量型回测框架才是最适合我们练手的项目了。那么我们就开始吧。
首先,我先为你梳理下回测流程,也就是下面五步:
1. 读取OHLC数据
1. 对OHLC进行指标运算
1. 策略根据指标向量决定买卖;
1. 发给模拟的”交易所“进行交易;
1. 最后,统计结果。
对此,使用之前学到的面向对象思维方式,我们可以大致抽取三个类:
- 交易所类( ExchangeAPI负责维护账户的资金和仓位以及进行模拟的买卖
- 策略类Strategy负责根据市场信息生成指标根据指标决定买卖
- 回测类框架Backtest包含一个策略类和一个交易所类负责迭代地对每个数据点调用策略执行。
接下来我们先从最外层的大框架开始。这样的好处在于我们是从上到下、从外往内地思考虽然还没有开始设计依赖项Backtest的依赖项是ExchangeAPI和Strategy但我们可以推测出它们应有的接口形式。推测接口的本质其实就是推测程序的输入。
这也是我在一开始提到过的,对于程序这个“黑箱”,你在一开始设计的时候,就要想好输入和输出。
回到最外层Backtest类。我们需要知道输出是最后的收益那么显然输入应该是初始输入的资金数量cash
此外为了模拟得更加真实我们还要考虑交易所的手续费commission。手续费的多少取决于券商broker或者交易所比如我们买卖股票的券商手续费可能是万七那么就是0.0007。但是在比特币交易领域手续费通常会稍微高一点可能是千分之二左右。当然无论怎么多一般也不会超过5 %。否则我们大家交易几次就破产了,也就不会有人去交易了。
这里说一句题外话,不知道你有没有发现,无论数字货币的价格是涨还是跌,总有一方永远不亏,那就是交易所。因为只要有人交易,他们就有白花花的银子进账。
回到正题至此我们就确定了Backtest的输入和输出。
它的输入是:
- OHLC数据
- 初始资金;
- 手续费率;
- 交易所类;
- 策略类。
输出则是:
- 最后剩余市值。
对此,你可以参考下面这段代码:
```
class Backtest:
&quot;&quot;&quot;
Backtest回测类用于读取历史行情数据、执行策略、模拟交易并估计
收益。
初始化的时候调用Backtest.run来时回测
instance, or `backtesting.backtesting.Backtest.optimize` to
optimize it.
&quot;&quot;&quot;
def __init__(self,
data: pd.DataFrame,
strategy_type: type(Strategy),
broker_type: type(ExchangeAPI),
cash: float = 10000,
commission: float = .0):
&quot;&quot;&quot;
构造回测对象。需要的参数包括:历史数据,策略对象,初始资金数量,手续费率等。
初始化过程包括检测输入类型,填充数据空值等。
参数:
:param data: pd.DataFrame pandas Dataframe格式的历史OHLCV数据
:param broker_type: type(ExchangeAPI) 交易所API类型负责执行买卖操作以及账户状态的维护
:param strategy_type: type(Strategy) 策略类型
:param cash: float 初始资金数量
:param commission: float 每次交易手续费率。如2%的手续费此处为0.02
&quot;&quot;&quot;
assert_msg(issubclass(strategy_type, Strategy), 'strategy_type不是一个Strategy类型')
assert_msg(issubclass(broker_type, ExchangeAPI), 'strategy_type不是一个Strategy类型')
assert_msg(isinstance(commission, Number), 'commission不是浮点数值类型')
data = data.copy(False)
# 如果没有Volumn列填充NaN
if 'Volume' not in data:
data['Volume'] = np.nan
# 验证OHLC数据格式
assert_msg(len(data.columns &amp; {'Open', 'High', 'Low', 'Close', 'Volume'}) == 5,
(&quot;输入的`data`格式不正确,至少需要包含这些列:&quot;
&quot;'Open', 'High', 'Low', 'Close'&quot;))
# 检查缺失值
assert_msg(not data[['Open', 'High', 'Low', 'Close']].max().isnull().any(),
('部分OHLC包含缺失值请去掉那些行或者通过差值填充. '))
# 如果行情数据没有按照时间排序,重新排序一下
if not data.index.is_monotonic_increasing:
data = data.sort_index()
# 利用数据,初始化交易所对象和策略对象。
self._data = data # type: pd.DataFrame
self._broker = broker_type(data, cash, commission)
self._strategy = strategy_type(self._broker, self._data)
self._results = None
def run(self):
&quot;&quot;&quot;
运行回测,迭代历史数据,执行模拟交易并返回回测结果。
Run the backtest. Returns `pd.Series` with results and statistics.
Keyword arguments are interpreted as strategy parameters.
&quot;&quot;&quot;
strategy = self._strategy
broker = self._broker
# 策略初始化
strategy.init()
# 设定回测开始和结束位置
start = 100
end = len(self._data)
# 回测主循环,更新市场状态,然后执行策略
for i in range(start, end):
# 注意要先把市场状态移动到第i时刻然后再执行策略。
broker.next(i)
strategy.next(i)
# 完成策略执行之后,计算结果并返回
self._results = self._compute_result(broker)
return self._results
def _compute_result(self, broker):
s = pd.Series()
s['初始市值'] = broker.initial_cash
s['结束市值'] = broker.market_value
s['收益'] = broker.market_value - broker.initial_cash
return s
```
这段代码有点长,但是核心其实就两部分。
- 初始化函数(**init**传入必要参数对OHLC数据进行简单清洗、排序和验证。我们从不同地方下载的数据可能格式不一样而排序的方式也可能是从前往后。所以这里我们把数据统一设置为按照时间从之前往现在的排序。
- 执行函数run这是回测框架的主要循环部分核心是更新市场还有更新策略的时间。迭代完成所有的历史数据后它会计算收益并返回。
你应该注意到了此时我们还没有定义策略和交易所API的结构。不过通过回测的执行函数我们可以确定这两个类的接口形式。
策略类Strategy的接口形式为
- 初始化函数init()根据历史数据进行指标Indicator计算。
- 步进函数next(),根据当前时间和指标,决定买卖操作,并发给交易所类执行。
交易所类ExchangeAPI的接口形式为
- 步进函数next(),根据当前时间,更新最新的价格;
- 买入操作buy(),买入资产;
- 卖出操作sell(),卖出资产。
## 交易策略
接下来我们来看交易策略。交易策略的开发是一个非常复杂的学问。为了达到学习的目的,我们来想一个简单的策略——移动均值交叉策略。
为了了解这个策略我们先了解一下什么叫做简单移动均值Simple Moving Average简称为SMA以下皆用SMA表示简单移动均值。我们知道N个数的序列 x[0]、x[1] .…… x[N] 的均值就是这N个数的和除以N。
现在我假设一个比较小的数K比N小很多。我们用一个K大小的滑动窗口在原始的数组上滑动。通过对每次框住的K个元素求均值我们就可以得到原始数组的窗口大小为K的SMA了。
SMA实质上就是对原始数组进行了一个简单平滑处理。比如某支股票的价格波动很大那么我们用SMA平滑之后就会得到下面这张图的效果。
<img src="https://static001.geekbang.org/resource/image/b5/0f/b543927903fbbaa33980a2046651530f.png" alt="">
你可以看出如果窗口大小越大那么SMA应该越平滑变化越慢反之如果SMA比较小那么短期的变化也会越快地反映在SMA上。
于是我们想到能不能对投资品的价格设置两个指标呢这俩指标一个是小窗口的SMA一个是大窗口的SMA。
- 如果小窗口的SMA曲线从下面刺破或者穿过大窗口SMA那么说明这个投资品的价格在短期内快速上涨同时这个趋势很强烈可能是一个买入的信号
- 反之如果大窗口的SMA从下方突破小窗口SMA那么说明投资品的价格在短期内快速下跌我们应该考虑卖出。
下面这幅图,就展示了这两种情况。
<img src="https://static001.geekbang.org/resource/image/40/66/408ff342683f6ac1af798ba3d488c266.png" alt="">
明白了这里的概念和原理后接下来的操作就不难了。利用Pandas我们可以非常简单地计算SMA和SMA交叉。比如你可以引入下面两个工具函数
```
def SMA(values, n):
&quot;&quot;&quot;
返回简单滑动平均
&quot;&quot;&quot;
return pd.Series(values).rolling(n).mean()
def crossover(series1, series2) -&gt; bool:
&quot;&quot;&quot;
检查两个序列是否在结尾交叉
:param series1: 序列1
:param series2: 序列2
:return: 如果交叉返回True反之False
&quot;&quot;&quot;
return series1[-2] &lt; series2[-2] and series1[-1] &gt; series2[-1]
```
如代码所示对于输入的一个数组Pandas的rolling(k)函数可以方便地计算窗内口大小为K的SMA数组而想要检查某个时刻两个SMA是否交叉你只需要查看两个数组末尾的两个元素即可。
那么,基于此,我们就可以开发出一个简单的策略了。下面这段代码表示策略的核心思想,我做了详细的注释,你理解起来应该没有问题:
```
def next(self, tick):
# 如果此时快线刚好越过慢线,买入全部
if crossover(self.sma1[:tick], self.sma2[:tick]):
self.buy()
# 如果是慢线刚好越过快线,卖出全部
elif crossover(self.sma2[:tick], self.sma1[:tick]):
self.sell()
# 否则,这个时刻不执行任何操作。
else:
pass
```
说完策略的核心思想,我们开始搭建策略类的框子。
首先我们要考虑到策略类Strategy应该是一个可以被继承的类同时应该包含一些固定的接口。这样回测器才能方便地调用。
于是我们可以定义一个Strategy抽象类包含两个接口方法init和next分别对应我们前面说的指标计算和步进函数。不过注意抽象类是不能被实例化的。所以我们必须定义一个具体的子类同时实现了init和next方法才可以。
这个类的定义,你可以参考下面代码的实现:
```
import abc
import numpy as np
from typing import Callable
class Strategy(metaclass=abc.ABCMeta):
&quot;&quot;&quot;
抽象策略类,用于定义交易策略。
如果要定义自己的策略类,需要继承这个基类,并实现两个抽象方法:
Strategy.init
Strategy.next
&quot;&quot;&quot;
def __init__(self, broker, data):
&quot;&quot;&quot;
构造策略对象。
@params broker: ExchangeAPI 交易API接口用于模拟交易
@params data: list 行情数据数据
&quot;&quot;&quot;
self._indicators = []
self._broker = broker # type: _Broker
self._data = data # type: _Data
self._tick = 0
def I(self, func: Callable, *args) -&gt; np.ndarray:
&quot;&quot;&quot;
计算买卖指标向量。买卖指标向量是一个数组,长度和历史数据对应;
用于判定这个时间点上需要进行&quot;买&quot;还是&quot;卖&quot;。
例如计算滑动平均:
def init():
self.sma = self.I(utils.SMA, self.data.Close, N)
&quot;&quot;&quot;
value = func(*args)
value = np.asarray(value)
assert_msg(value.shape[-1] == len(self._data.Close), '指示器长度必须和data长度相同')
self._indicators.append(value)
return value
@property
def tick(self):
return self._tick
@abc.abstractmethod
def init(self):
&quot;&quot;&quot;
初始化策略。在策略回测/执行过程中调用一次,用于初始化策略内部状态。
这里也可以预计算策略的辅助参数。比如根据历史行情数据:
计算买卖的指示器向量;
训练模型/初始化模型参数
&quot;&quot;&quot;
pass
@abc.abstractmethod
def next(self, tick):
&quot;&quot;&quot;
步进函数执行第tick步的策略。tick代表当前的&quot;时间&quot;。比如data[tick]用于访问当前的市场价格。
&quot;&quot;&quot;
pass
def buy(self):
self._broker.buy()
def sell(self):
self._broker.sell()
@property
def data(self):
return self._data
```
为了方便访问成员我们还定义了一些Python property。同时我们的买卖请求是由策略类发出、由交易所API来执行的所以我们的策略类里依赖于ExchangeAPI类。
现在有了这个框架我们实现移动均线交叉策略就很简单了。你只需要在init函数中定义计算大小窗口SMA的逻辑同时在next函数中完成交叉检测和买卖调用就行了。具体实现你可以参考下面这段代码
```
from utils import assert_msg, crossover, SMA
class SmaCross(Strategy):
# 小窗口SMA的窗口大小用于计算SMA快线
fast = 10
# 大窗口SMA的窗口大小用于计算SMA慢线
slow = 20
def init(self):
# 计算历史上每个时刻的快线和慢线
self.sma1 = self.I(SMA, self.data.Close, self.fast)
self.sma2 = self.I(SMA, self.data.Close, self.slow)
def next(self, tick):
# 如果此时快线刚好越过慢线,买入全部
if crossover(self.sma1[:tick], self.sma2[:tick]):
self.buy()
# 如果是慢线刚好越过快线,卖出全部
elif crossover(self.sma2[:tick], self.sma1[:tick]):
self.sell()
# 否则,这个时刻不执行任何操作。
else:
pass
```
## 模拟交易
到这里,我们的回测就只差最后一块儿了。胜利就在眼前,我们继续加油。
我们前面提到过交易所类负责模拟交易而模拟的基础就是需要当前市场的价格。这里我们可以用OHLC中的Close作为那个时刻的价格。
此外,为了简化设计,我们假设买卖操作都利用的是当前账户的所有资金、仓位,且市场容量足够大。这样,我们的下单请求就能够马上完全执行。
也别忘了手续费这个大头。考虑到有手续费的情况,此时,我们最核心的买卖函数应该怎么来写呢?
我们一起来想这个问题。假设我们现在有1000.0元此时BTC的价格是100.00元当然没有这么好的事情啊这里只是假设并且交易手续费为1%。那么我们能买到多少BTC呢
我们可以采用这种算法:
```
买到的数量 = 投入的资金 * (1.0 - 手续费) / 价格
```
那么此时你就能收到9.9个BTC。
类似的,卖出的时候结算方式如下,也不难理解:
```
卖出的收益 = 持有的数量 * 价格 * (1.0 - 手续费)
```
所以,最终模拟交易所类的实现,你可以参考下面这段代码:
```
from utils import read_file, assert_msg, crossover, SMA
class ExchangeAPI:
def __init__(self, data, cash, commission):
assert_msg(0 &lt; cash, &quot;初始现金数量大于0输入的现金数量{}&quot;.format(cash))
assert_msg(0 &lt;= commission &lt;= 0.05, &quot;合理的手续费率一般不会超过5%,输入的费率:{}&quot;.format(commission))
self._inital_cash = cash
self._data = data
self._commission = commission
self._position = 0
self._cash = cash
self._i = 0
@property
def cash(self):
&quot;&quot;&quot;
:return: 返回当前账户现金数量
&quot;&quot;&quot;
return self._cash
@property
def position(self):
&quot;&quot;&quot;
:return: 返回当前账户仓位
&quot;&quot;&quot;
return self._position
@property
def initial_cash(self):
&quot;&quot;&quot;
:return: 返回初始现金数量
&quot;&quot;&quot;
return self._inital_cash
@property
def market_value(self):
&quot;&quot;&quot;
:return: 返回当前市值
&quot;&quot;&quot;
return self._cash + self._position * self.current_price
@property
def current_price(self):
&quot;&quot;&quot;
:return: 返回当前市场价格
&quot;&quot;&quot;
return self._data.Close[self._i]
def buy(self):
&quot;&quot;&quot;
用当前账户剩余资金,按照市场价格全部买入
&quot;&quot;&quot;
self._position = float(self._cash / (self.current_price * (1 + self._commission)))
self._cash = 0.0
def sell(self):
&quot;&quot;&quot;
卖出当前账户剩余持仓
&quot;&quot;&quot;
self._cash += float(self._position * self.current_price * (1 - self._commission))
self._position = 0.0
def next(self, tick):
self._i = tick
```
其中的current_price当前价格可以方便地获得模拟交易所当前时刻的商品价格而market_value则可以获得当前总市值。在初始化函数的时候我们检查手续费率和输入的现金数量是不是在一个合理的范围。
有了所有的这些部分,我们就可以来模拟回测啦!
首先我们设置初始资金量为10000.00美元交易所手续费率为0。这里你可以猜一下如果我们从2015年到现在都按照SMA来买卖现在应该有多少钱呢
```
def main():
BTCUSD = read_file('BTCUSD_GEMINI.csv')
ret = Backtest(BTCUSD, SmaCross, ExchangeAPI, 10000.0, 0.00).run()
print(ret)
if __name__ == '__main__':
main()
```
铛铛铛,答案揭晓,程序将输出:
```
初始市值 10000.000000
结束市值 576361.772884
收益 566361.772884
```
结束时我们将有57万美元翻了整整57倍啊简直不要太爽。不过等等这个手续费率为0实在是有点碍眼因为根本不可能啊。我们现在来设一个比较真实的值吧大概千分之三然后再来试试
```
初始市值 10000.000000
结束市值 2036.562001
收益 -7963.437999
```
什么鬼我们变成赔钱了只剩下2000美元了这是真的吗
这是真的,也是假的。
我说的“真”是指如果你真的用SMA交叉这种简单的方法去交易那么手续费摩擦和滑点等因素确实可能让你的高频策略赔钱。
而我说是“假”是指,这种模拟交易的方式非常粗糙。真实的市场情况,并非这么理想——比如买卖请求永远马上执行;再比如,我们在市场中进行交易的同时不会影响市场价格等,这些理想情况都是不可能的。所以,很多时候,回测永远赚钱,但实盘马上赔钱。
## 总结
这节课,我们继承上一节,介绍了回测框架的分类、数据的格式,并且带你从头开始写了一个简单的回测系统。你可以把今天的代码片段“拼”起来,这样就会得到一个简化的回测系统样例。同时,我们实现了一个简单的交易策略,并且在真实的历史数据上运行了回测结果。我们观察到,在加入手续费后,策略的收益情况发生了显著的变化。
## 思考题
最后给你留一个思考题。之前我们介绍了如何抓取tick数据你可以根据抓取的tick数据生成5分钟、每小时和每天的OHLCV数据吗欢迎在留言区写下你的答案和问题也欢迎你把这篇文章分享出去。

View File

@@ -0,0 +1,237 @@
<audio id="audio" title="37 | Kafka & ZMQ自动化交易流水线" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/88/39/889949b1dc34a9190972799211da0339.mp3"></audio>
你好,我是景霄。
在进行这节课的学习前,我们先来回顾一下,前面三节课,我们学了些什么。
第 34 讲,我们介绍了如何通过 RESTful API 在交易所下单;第 35 讲,我们讲解了如何通过 Websocket ,来获取交易所的 orderbook 数据;第 36 讲,我们介绍了如何实现一个策略,以及如何对策略进行历史回测。
事实上,到这里,一个简单的、可以运作的量化交易系统已经成型了。你可以对策略进行反复修改,期待能得到不错的 PnL。但是对于一个完善的量化交易系统来说只有基本骨架还是不够的。
在大型量化交易公司,系统一般是分布式运行的,各个模块独立在不同的机器上,然后互相连接来实现。即使是个人的交易系统,在进行诸如高频套利等算法时,也需要将执行层布置在靠近交易所的机器节点上。
所以,从今天这节课开始,我们继续回到 Python 的技术栈,从量化交易系统这个角度切入,为你讲解如何实现分布式系统之间的复杂协作。
## 中间件
我们先来介绍一下中间件这个概念。中间件,是将技术底层工具和应用层进行连接的组件。它要实现的效果则是,让我们这些需要利用服务的工程师,不必去关心底层的具体实现。我们只需要拿着中间件的接口来用就好了。
这个概念听起来并不难理解,我们再举个例子让你彻底明白。比如拿数据库来说,底层数据库有很多很多种,从关系型数据库 MySQL 到非关系型数据库 NoSQL从分布式数据库 Spanner 到内存数据库 Redis不同的数据库有不同的使用场景也有着不同的优缺点更有着不同的调用方式。那么中间件起什么作用呢
中间件,等于在这些不同的数据库上加了一层逻辑,这一层逻辑专门用来和数据库打交道,而对外只需要暴露同一个接口即可。这样一来,上层的程序员调用中间件接口时,只需要让中间件指定好数据库即可,其他参数完全一致,极大地方便了上层的开发;同时,下层技术栈在更新换代的时候,也可以做到和上层完全分离,不影响程序员的使用。
它们之间的逻辑关系,你可以参照下面我画的这张图。我习惯性把中间件的作用调侃为:没有什么事情是加一层解决不了的;如果有,那就加两层。
<img src="https://static001.geekbang.org/resource/image/f4/50/f49f221a8191d8fa95eeea146bbbf550.png" alt="">
当然,这只是其中一个例子,也只是中间件的一种形式。事实上,比如在阿里,中间件主要有分布式关系型数据库 DRDS、消息队列和分布式服务这么三种形式。而我们今天主要会用到消息队列因为它非常符合量化交易系统的应用场景即事件驱动模型。
## 消息队列
那么,什么是消息队列呢?一如其名,消息,即互联网信息传递的个体;而队列,学过算法和数据结构的你,应该很清楚这个 FIFO先进先出的数据结构吧。如果算法基础不太牢建议你可以学习极客时间平台上王争老师的“数据结构与算法之美”专栏[第 09讲](https://time.geekbang.org/column/article/41330)即为队列知识)
简而言之,消息队列就是一个临时存放消息的容器,有人向消息队列中推送消息;有人则监听消息队列,发现新消息就会取走。根据我们刚刚对中间件的解释,清晰可见,消息队列也是一种中间件。
目前,市面上使用较多的消息队列有 RabbitMQ、Kafka、RocketMQ、ZMQ 等。不过今天,我只介绍最常用的 ZMQ 和 Kafka。
我们先来想想,消息队列作为中间件有什么特点呢?
首先是严格的时序性。刚刚说了,队列是一种先进先出的数据结构,你丢给它 `1, 2, 3`,然后另一个人从里面取数据,那么取出来的一定也是 `1, 2, 3`,严格保证了先进去的数据先出去,后进去的数据后出去。显然,这也是消息机制中必须要保证的一点,不然颠三倒四的结果一定不是我们想要的。
说到队列的特点,简单提一句,与“先进先出“相对的是栈这种数据结构,它是先进后出的,你丢给它 `1, 2, 3`,再从里面取出来的时候,拿到的就是`3, 2, 1`了,这一点一定要区分清楚。
其次,是分布式网络系统的老生常谈问题。如何保证消息不丢失?如何保证消息不重复?这一切,消息队列在设计的时候都已经考虑好了,你只需要拿来用就可以,不必过多深究。
不过,很重要的一点,消息队列是如何降低系统复杂度,起到中间件的解耦作用呢?我们来看下面这张图。
<img src="https://static001.geekbang.org/resource/image/f6/f0/f6a5e84033070f5ee7746ca85680aef0.png" alt="">
消息队列的模式是发布和订阅,一个或多个消息发布者可以发布消息,一个或多个消息接受者可以订阅消息。 从图中你可以看到,消息发布者和消息接受者之间没有直接耦合,其中,
- 消息发布者将消息发送到分布式消息队列后,就结束了对消息的处理;
- 消息接受者从分布式消息队列获取该消息后,即可进行后续处理,并不需要探寻这个消息从何而来。
至于新增业务的问题,只要你对这类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,所以也就实现了业务的可扩展性设计。
讲了这么多概念层的东西,想必你迫不及待地想看具体代码了吧。接下来,我们来看一下 ZMQ 的实现。
### ZMQ
先来看 ZMQ这是一个非常轻量级的消息队列实现。
>
作者 Pieter Hintjens 是一位大牛他本人的经历也很传奇2010年诊断出胆管癌并成功做了手术切除。但2016年4月却发现癌症大面积扩散到了肺部已经无法治疗。他写的最后一篇通信模式是关于死亡协议的之后在比利时选择接受安乐死。
ZMQ 是一个简单好用的传输层,它有三种使用模式:
- Request - Reply 模式;
- Publish - Subscribe 模式;
- Parallel Pipeline 模式。
第一种模式很简单client 发消息给 serverserver 处理后返回给 client完成一次交互。这个场景你一定很熟悉吧没错和 HTTP 模式非常像,所以这里我就不重点介绍了。至于第三种模式,与今天内容无关,这里我也不做深入讲解。
我们需要详细来看的是第二种即“PubSub”模式。下面是它的具体实现代码很清晰你应该很容易理解
```
# 订阅者 1
import zmq
def run():
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect('tcp://127.0.0.1:6666')
socket.setsockopt_string(zmq.SUBSCRIBE, '')
print('client 1')
while True:
msg = socket.recv()
print(&quot;msg: %s&quot; % msg)
if __name__ == '__main__':
run()
########## 输出 ##########
client 1
msg: b'server cnt 1'
msg: b'server cnt 2'
msg: b'server cnt 3'
msg: b'server cnt 4'
msg: b'server cnt 5'
```
```
# 订阅者 2
import zmq
def run():
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect('tcp://127.0.0.1:6666')
socket.setsockopt_string(zmq.SUBSCRIBE, '')
print('client 2')
while True:
msg = socket.recv()
print(&quot;msg: %s&quot; % msg)
if __name__ == '__main__':
run()
########## 输出 ##########
client 2
msg: b'server cnt 1'
msg: b'server cnt 2'
msg: b'server cnt 3'
msg: b'server cnt 4'
msg: b'server cnt 5'
```
```
# 发布者
import time
import zmq
def run():
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind('tcp://*:6666')
cnt = 1
while True:
time.sleep(1)
socket.send_string('server cnt {}'.format(cnt))
print('send {}'.format(cnt))
cnt += 1
if __name__ == '__main__':
run()
########## 输出 ##########
send 1
send 2
send 3
send 4
send 5
```
这里要注意的一点是,如果你想要运行代码,请先运行两个订阅者,然后再打开发布者。
接下来,我来简单讲解一下。
对于订阅者,我们要做的是创建一个 zmq Context连接 socket 到指定端口。其中setsockopt_string() 函数用来过滤特定的消息,而下面这行代码:
```
socket.setsockopt_string(zmq.SUBSCRIBE, '')
```
则表示不过滤任何消息。最后,我们调用 socket.recv() 来接受消息就行了,这条语句会阻塞在这里,直到有新消息来临。
对于发布者,我们同样要创建一个 zmq Context绑定到指定端口不过请注意这里用的是 bind 而不是 connect。因为在任何情况下同一个地址端口 bind 只能有一个,但却可以有很多个 connect 链接到这个地方。初始化完成后,再调用 socket.send_string ,即可将我们想要发送的内容发送给 ZMQ。
当然,这里还有几个需要注意的地方。首先,有了 send_string我们其实已经可以通过 JSON 序列化,来传递几乎我们想要的所有数据结构,这里的数据流结构就已经很清楚了。
另外,把发布者的 time.sleep(1) 放在 while 循环的最后,严格来说应该是不影响结果的。这里你可以尝试做个实验,看看会发生什么。
你还可以思考下另一个问题,如果这里是多个发布者,那么 ZMQ 应该怎么做呢?
### Kafka
接着我们再来看一下 Kafka。
通过代码实现你也可以发现ZMQ 的优点主要在轻量、开源和方便易用上,但在工业级别的应用中,大部分人还是会转向 Kafka 这样的有充足支持的轮子上。
相比而言Kafka 提供了点对点网络和发布订阅模型的支持,这也是用途最广泛的两种消息队列模型。而且和 ZMQ 一样Kafka 也是完全开源的,因此你也能得到开源社区的充分支持。
Kafka的代码实现和ZMQ大同小异这里我就不专门讲解了。关于Kafka的更多内容极客时间平台也有对 Kafka 的专门详细的介绍,对此有兴趣的同学,可以在极客时间中搜索“[Kafka核心技术与实战](https://time.geekbang.org/column/intro/191)”,这个专栏里,胡夕老师用详实的篇幅,讲解了 Kafka 的实战和内核,你可以加以学习和使用。
<img src="https://static001.geekbang.org/resource/image/d4/27/d401e91a10826773067857e3c9974a27.png" alt="">
## 基于消息队列的 Orderbook 数据流
最后回到我们的量化交易系统上。
量化交易系统中,获取 orderbook 一般有两种用途:策略端获取实时数据,用来做决策;备份在文件或者数据库中,方便让策略和回测系统将来使用。
如果我们直接单机监听交易所的消息,风险将会变得很大,这在分布式系统中叫做 Single Point Failure。一旦这台机器出了故障或者网络连接突然中断我们的交易系统将立刻暴露于风险中。
于是,一个很自然的想法就是,我们可以在不同地区放置不同的机器,使用不同的网络同时连接到交易所,然后将这些机器收集到的信息汇总、去重,最后生成我们需要的准确数据。相应的拓扑图如下:
<img src="https://static001.geekbang.org/resource/image/ab/dc/ab08e31f805d1f7ea596ae4ccdea48dc.png" alt="">
当然,这种做法也有很明显的缺点:因为要同时等待多个数据服务器的数据,再加上消息队列的潜在处理延迟和网络延迟,对策略服务器而言,可能要增加几十到数百毫秒的延迟。如果是一些高频或者滑点要求比较高的策略,这种做法需要谨慎考虑。
但是对于低频策略、波段策略这种延迟换来的整个系统的稳定性和架构的解耦性还是非常值得的。不过你仍然需要注意这种情况下消息队列服务器有可能成为瓶颈也就是刚刚所说的Single Point Failure一旦此处断开依然会将系统置于风险之中。
事实上我们可以使用一些很成熟的系统例如阿里的消息队列AWS 的 Simple Queue Service 等等,使用这些非常成熟的消息队列系统,风险也将会最小化。
## 总结
这节课,我们分析了现代化软件工程领域中的中间件系统,以及其中的主要应用——消息队列。我们讲解了最基础的消息队列的模式,包括点对点模型、发布者订阅者模型,和一些其他消息队列自己支持的模型。
在真实的项目设计中,我们要根据自己的产品需求,来选择使用不同的模型;同时也要在编程实践中,加深对不同技能点的了解,对系统复杂性进行解耦,这才是设计出高质量系统的必经之路。
## 思考题
今天的思考题文中我也提到过这里再专门列出强调一下。在ZMQ 那里,我提出了两个问题:
- 如果你试着把发布者的 time.sleep(1) 放在 while 循环的最后,会发生什么?为什么?
- 如果有多个发布者ZMQ 应该怎么做呢?
欢迎留言写下你的思考和疑惑,也欢迎你把这篇文章分享给更多的人一起学习。

View File

@@ -0,0 +1,239 @@
<audio id="audio" title="38 | MySQL日志和数据存储系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/7e/f2/7e7bad741a80ef01d67fa2afaa2435f2.mp3"></audio>
你好,我是景霄。今天这节课,我们来聊聊日志和存储系统。
在互联网公司中,日志系统是一个非常重要的技术底层。在每一次重要的交互行为中,关键信息都会被记录下来存档,以供日后线下分析,或者线上实时分析。这些数据,甚至可以说是硅谷互联网大公司的命脉所在。
有了它们,你才能建立机器学习模型来预测用户的行为,从而可以精确描绘用户画像,然后针对性地使用推荐系统、分类器,将用户进一步留下,并精准推送广告来盈利。
在量化交易中,日志同样有着非常重要的作用。一如前面所讲,我们重要的数据有:行情数据、策略信号、执行情况、仓位信息等等非常多的信息。
对于简单的、小规模的数据,例如 orderbook 信息,我们完全可以把数据存在 txt、csv 文件中,这样做简单高效。不过,缺点是,随着数据量上升,一个文件将会变得非常大,检索起来也不容易。这时,一个很直观的方式出现了,我们可以把每天的数据存在一个文件中,这样就暂时缓解了尴尬。
但是,随着数据量的上升,或者是你的算法逐渐来到高频交易领域时,简单地把数据存在文件上,已经不足以满足新的需求,更无法应对分布式量化交易系统的需求。于是,一个显而易见的想法就是,我们可以把日志存在数据库系统中。
这节课,我们就以 MySQL 这种传统型关系数据库为例,讲解一下数据库在日志中的运用。
## 快速理解MySQL
担心一些同学没有数据库的基础,我先来简单介绍一下 MySQL 数据库。
MySQL 属于典型的关系型数据库RDBMS所谓的关系型数据库就是指建立在关系模型基础上的数据库借助于集合代数等数学概念和方法来处理数据库中的数据。基本上任何学习资料都会告诉你它有着下面这几个特征
1. 数据是以表格的形式出现的;
1. 每一行是各种记录名称;
1. 每一列是记录名称所对应的数据域;
1. 许多的行和列,组成一张表单;
1. 若干的表单组成数据库database这个整体。
不过,抛开这些抽象的特征不谈,你首先需要掌握的,是下面这些术语的概念。
- 数据库,是一些关联表的集合;而数据表则是数据的矩阵。在一个数据库中,数据表看起来就像是一个简单的电子表格。
- 在数据表中,每一列包含的是相同类型的数据;每一行则是一组相关的数据。
- 主键也是数据表中的一个列,只不过,这一列的每行元素都是唯一的,且一个数据表中只能包含一个主键;而外键则用于关联两个表。
除此之外,你还需要了解索引。索引是对数据库表中一列或多列的值进行排序的一种结构。使用索引,我们可以快速访问数据库表中的特定信息。一般来说,你可以对很多列设置索引,这样在检索指定列的时候,就大大加快了速度,当然,代价是插入数据会变得更慢。
至于操作 MySQL一般用的是结构化查询语言SQL。SQL是一种典型的领域专用语言domain-specific language简称DSL这里我就不做过多介绍了如果你感兴趣可以学习极客时间平台上的“[SQL必知必会](https://time.geekbang.org/column/intro/192)”专栏。
接下来,我们就来简单看一下,如何使用 Python 来操作 MySQL 数据库。
Python 连接数据库的方式有好多种,这里我简单介绍其中两种。我们以 Ubuntu 为例,假设你的系统中已经安装过 MySQL Server。安装 MySQL可以参考这篇文章 [https://www.jianshu.com/p/3111290b87f4](https://www.jianshu.com/p/3111290b87f4),或者你可以自行搜索解决)
### mysqlclient
事实上, Python 连接 MySQL 最流行的一个驱动是 MySQL-python又叫 MySQLdb很多框架都也是基于此库进行开发。不过遗憾的是它只支持 Python2.x而且安装的时候有很多前置条件。因为它是基于C开发的库在 Windows 平台安装非常不友好经常出现失败的情况。所以现在我们基本不再推荐使用取代者是它的衍生版本——mysqlclient。
mysqlclient 完全兼容 MySQLdb同时支持 Python3.x是 Django ORM的依赖工具。如果你想使用原生 SQL 来操作数据库,那么我优先推荐使用这个框架。
它的安装方式很简单:
```
sudo apt-get install python3-dev
pip install mysqlclient
```
我们来看一个样例代码:
```
import MySQLdb
def test_pymysql():
conn = MySQLdb.connect(
host='localhost',
port=3306,
user='your_username',
passwd=your_password,
db='mysql'
)
cur = conn.cursor()
cur.execute('''
CREATE TABLE price (
timestamp TIMESTAMP NOT NULL,
BTCUSD FLOAT(8,2),
PRIMARY KEY (timestamp)
);
''')
cur.execute('''
INSERT INTO price VALUES(
&quot;2019-07-14 14:12:17&quot;,
11234.56
);
''')
conn.commit()
conn.close()
test_pymy
```
代码的思路很清晰明了,首先是通过 connect 命令连接数据库,来创建一个连接;之后,通过 conn.cursor() 函数创建一个游标。这里你可能会问,为什么要使用游标呢?
一个主要的原因就是,这样可以把集合操作转换成单个记录处理的方式。如果用 SQL 语言从数据库中检索数据,结果会放在内存的一块区域中,并且这个结果往往是一个含有多个记录的集合。而游标机制,则允许用户在 MySQL 内逐行地访问这些记录,这样你就可以按照自己的意愿,来显示和处理这些记录。
继续回到代码中,再往下走,我们创建了一个 price table同时向里面插入一条 orderbook 数据。这里为了简化代码突出重点,我只保留了 timestamp 和 price。
最后,我们使用 conn.commit() 来提交更改,然后 close() 掉连接就可以了。
### peewee
不过,大家逐渐发现,写原生的 SQL 命令很麻烦。因为你需要根据特定的业务逻辑,来构造特定的插入和查询语句,这样可以说就完全抛弃了面向对象的思维。因此,又诞生了很多封装 wrapper 包和 ORM 框架。
这里所说的ORMObject Relational Mapping简称ORM ,是 Python 对象与数据库关系表的一种映射关系,有了 ORM 后,我们就不再需要写 SQL 语句,而可以直接使用 Python 的数据结构了。
ORM 框架的优点是提高了写代码的速度同时兼容多种数据库系统如SQLite、MySQL、PostgreSQL等这些数据库而付出的代价可能就是性能上的一些损失。
接下来要讲的peewee正是其中一种基于 Python 的 ORM 框架,它的学习成本非常低,可以说是 Python 中最流行的 ORM 框架。
它的安装方式也很简单:
```
pip install peewee
```
我们来看一个样例代码:
```
import peewee
from peewee import *
db = MySQLDatabase('mysql', user='your_username', passwd=your_password)
class Price(peewee.Model):
timestamp = peewee.DateTimeField(primary_key=True)
BTCUSD = peewee.FloatField()
class Meta:
database = db
def test_peewee():
Price.create_table()
price = Price(timestamp='2019-06-07 13:17:18', BTCUSD='12345.67')
price.save()
test_p
```
如果你写过 Django你会发现这个写法和 Django 简直一模一样。我们通过一个 Python class ,映射了 MySQL 中的一张数据表;只要对其中每一列数据格式进行定义,便可按照 Python 的方式进行操作。
显而易见peewee的最大优点就是让 SQL 语言瞬间变成强类型语言,这样不仅极大地增强了可读性,也能有效减少出 bug 的概率。
不过事实上作为一名数据科学家或者作为一名量化从业者quant ),你要处理的数据远比这些复杂很多。互联网工业界有大量的脏数据,金融行业的信噪比更是非常之低,数据处理只能算是基本功。
如果你对数据分析有兴趣和志向,在学生时期就应该先打牢数学和统计的基础,之后在实习和工作中快速掌握数据处理的方法。当然,如果你已经错过学生时期的话,现在开始也是个不错的选择,毕竟,逐渐形成自己的核心竞争力,才是我们每个人的正道。
## 量化数据分析系统
数据库有了量化数据存入后,接下来,我们便可以开始进行一些量化分析了。这一块儿也是一个很大的学术领域,叫做时间序列分析,不过就今天这节课的主题来说,我们仅做抛砖引玉,列举一个非常简单的例子,即求过去一个小时 BTC/USD 的最高价和最低价。
我们来看下面这段代码:
```
import MySQLdb
import numpy as np
def test_pymysql():
conn = MySQLdb.connect(
host='localhost',
port=3306,
user='your_username',
passwd='your_password',
db='mysql'
)
cur = conn.cursor()
cur.execute('''
SELECT
BTCUSD
FROM
price
WHERE
timestamp &gt; now() - interval 60 minute
''')
BTCUSD = np.array(cur.fetchall())
print(BTCUSD.max(), BTCUSD.min())
conn.close()
test_pym
```
代码看起来很简单吧!显然,通过 SQL 语句,我们可以抓取到过去一小时的时间序列片段,拿到我们想要的 BTC/USD 价格向量,然后通过 `numpy` 处理一下即可。不过这里需要注意一点,我们并不需要调用 conn.commit(),因为我们的操作是只读的,对数据库没有任何影响。
## 分布式日志系统
明白了上面的内容后,我们现在来看一下分布式日志系统。
对量化交易而言,我们需要的模块主要有数据系统、策略系统、交易执行系统、线下模型训练、线上风控系统以及实时监控系统。它们之间的对应关系,我画了一张图,你可以参考来理解。
<img src="https://static001.geekbang.org/resource/image/ca/17/ca5c501186fae46b50b9af9b88980617.png" alt="">
这里的每个子系统都是独立运行的,并且还有许多模块需要迭代更新,所以我们简单保存本地日志显然不是一个明智之举。于是,我们可以专门开一台服务器来运行 MySQL server并且开放指定端口和其他系统进行交互。
另外,图中的收集系统,其实类似于上一节我们所讲的消息队列体系,在各个上游系统中运行代理工具,负责将各个模块的 log 收集起来然后发送到收集系统中。收集系统整理过后再将信息存到日志系统。当然除了简单的消息队列我们还能用很多工具比如阿里云的Logtail、 Apache 的 Flume Agent等等。
而到了后期,对于日志系统来说,越来越需要注意的就是存储效率和分析效率。随着使用的增加,数据会越来越多,因此我们可以考虑对一些数据进行压缩和保存。而越是久远的数据,越是粗粒度的数据,被调用的概率也就越低,所以它们也就首当其冲,成了我们压缩、保存的目标。
## 日志分析
最后,我再来补充讲一讲日志的分析。前面提到过,分析一般分为两种,离线分析和在线分析。
在离线分析中,比较常见的是生成报告。
比如总结某天某月或某季度内的收益亏损情况PnL、最大回撤、夏普比率等数据。这种基于时间窗口的统计在关系型数据库中也能得到很方便的支持。
而另一类常见的离线使用方式,则是回测系统。在一个新策略研发的周期中,我们需要对历史数据进行回测,这样就可以得到历史数据中交易的收益率等数据。回测系统对于评估一个新的策略非常重要,然而,回测往往需要大量的资源,所以选取好数据库、数据存储方式,优化数据连接和计算,就显得至关重要。
在线分析,则更多应用于风控和警报系统。这种方式,对数据的实时性要求更高一些,于是,一种方法就是,从消息队列中直接拿最快的数据进行操作。当然,这个前提是时间窗口较小,这样你就不需要风控系统来维护大量的本地数据。
至于实时警报,最关键的依然是数据。
- 比如,数据系统异常停止,被监视的表没有更新;
- 或者,交易系统的连接出了故障,委托订单的某些状态超过了一定的阈值;
- 再或者,仓位信息出现了较大的、预计之外的变动。
这些情况都需要进行报警也就是硅谷大公司所说的“oncall”。一旦发生意外负责人会迅速收到电话、短信和邮件然后通过监控平台来确认是真的出了事故还是监控误报。
当然,现在已经有了不少开源的工具可以在云端使用,其中 AWS 属于全球领先的云计算平台。如果你的服务器架设在美国,那就可以考虑选择它家的各种各样的云服务。这样做的好处是,对于小型量化交易团队而言,避免自己搭建复杂的日志系统,而是把主要精力放在策略的开发迭代之上,提高了不少效率。
## 总结
这一节课,我从工程的角度,为你介绍了量化系统中的存储系统。我们从基础的 MySQL 的使用方法讲起,再讲到后面的量化系统框架。数据库和数据在绝大部分互联网行业都是核心,对量化从业者来说也是重要的生产资料。而搭建一套负载合理、数据可靠的数据系统,也需要一个量化团队长期打磨,并根据需求进行迭代。
## 思考题
最后给你留一道思考题。量化交易需要的数据量不是很大,但是有可能出现调用频率极高的情况,例如回测系统。那么,你能想到哪些优化手段,来降低调用代价吗?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。

View File

@@ -0,0 +1,370 @@
<audio id="audio" title="39 | Django搭建监控平台" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e8/fa/e82c2455d0c04f85c060957b9f3f0efa.mp3"></audio>
你好,我是景霄。
通过前几节课的学习,相信你对量化交易系统已经有了一个最基本的认知,也能通过自己的代码,搭建一个简单的量化交易系统来进行盈利。
前面几节课,我们的重点在后台代码、中间件、分布式系统和设计模式上。这节课,我们重点来看前端交互。
监控和运维,是互联网工业链上非常重要的一环。监控的目的就是防患于未然。通过监控,我们能够及时了解到企业网络的运行状态。一旦出现安全隐患,你就可以及时预警,或者是以其他方式通知运维人员,让运维监控人员有时间处理和解决隐患,避免影响业务系统的正常使用,将一切问题的根源扼杀在摇篮当中。
在硅谷互联网大公司中,监控和运维被称为 SRE是公司正常运行中非常重要的一环。作为 billion 级别的 Facebook内部自然也有着大大小小、各种各样的监控系统和运维工具有的对标业务数据有的对标服务器的健康状态有的则是面向数据库和微服务的控制信息。
不过,万变不离其宗,运维工作最重要的就是维护系统的稳定性。除了熟悉运用各种提高运维效率的工具来辅助工作外,云资源费用管理、安全管理、监控等,都需要耗费不少精力和时间。运维监控不是一朝一夕得来的,而是随着业务发展的过程中同步和发展的。
作为量化实践内容的最后一节,今天我们就使用 Django 这个 Web 框架,来搭建一个简单的量化监控平台。
## Django 简介和安装
Django 是用 Python 开发的一个免费开源的 Web 框架可以用来快速搭建优雅的高性能网站。它采用的是“MVC”的框架模式即模型 M、视图 V 和控制器 C。
Django 最大的特色,在于将网页和数据库中复杂的关系,转化为 Python 中对应的简单关系。它的设计目的是使常见的Web开发任务变得快速而简单。Django是开源的不是商业项目或者科研项目并且集中力量解决Web开发中遇到的一系列问题。所以Django 每天都会在现有的基础上进步,以适应不断更迭的开发需求。这样既节省了开发时间,也提高了后期维护的效率。
说了这么多,接下来,我们通过上手使用进一步来了解。先来看一下,如何安装和使用 Django。你可以先按照下面代码块的内容来操作安装Django
```
pip3 install Django
django-admin --version
########## 输出 ##########
2.2.3
```
接着,我们来创建一个新的 Django 项目:
```
django-admin startproject TradingMonitor
cd TradingMonitor/
python3 manage.py migrate
########## 输出 ##########
```
```
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying sessions.0001_initial... OK
```
这时,你能看到文件系统大概是下面这样的:
```
TradingMonitor/
├── TradingMonitor
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── db.sqlite3
└── manage.py
```
我简单解释一下它的意思:
- TradingMonitor/TradingMonitor表示项目最初的 Python 包;
- TradingMonitor/init.py表示一个空文件声明所在目录的包为一个 Python 包;
- TradingMonitor/settings.py管理项目的配置信息
- TradingMonitor/urls.py声明请求 URL 的映射关系;
- TradingMonitor/wsgi.py表示Python 程序和 Web 服务器的通信协议;
- manage.py表示一个命令行工具用来和 Django 项目进行交互;
- Db.sqlite3表示默认的数据库可以在设置中替换成其他数据库。
另外,你可能注意到了上述命令中的`python3 manage.py migrate`,这个命令表示创建或更新数据库模式。每当 model 源代码被改变后,如果我们要将其应用到数据库上,就需要执行一次这个命令。
接下来,我们为这个系统添加管理员账户:
```
python3 manage.py createsuperuser
########## 输出 ##########
Username (leave blank to use 'ubuntu'): admin
Email address:
Password:
Password (again):
Superuser created successfully.
```
然后,我们来启动 Django 的 debugging 模式:
```
python3 manage.py runserver
```
最后,打开浏览器输入:`http://127.0.0.1:8000`。如果你能看到下面这个画面,就说明 Django 已经部署成功了。
<img src="https://static001.geekbang.org/resource/image/6d/02/6d8244a8016c97139b3de4680ae2e802.png" alt="">
Django 的安装是不是非常简单呢?这其实也是 Python 一贯的理念,简洁,并简化入门的门槛。
OK现在我们再定位到 `http://127.0.0.1:8000/admin`,你会看到 Django 的后台管理网页,这里我就不过多介绍了。
<img src="https://static001.geekbang.org/resource/image/ef/93/ef29a801bd367263aa4792131eeae093.png" alt=""><br>
<img src="https://static001.geekbang.org/resource/image/31/25/311b3ebf2b0801e84de60d36a81c6925.png" alt="">
到此Django 就已经成功安装,并且正常启动啦。
## MVC 架构
刚刚我说过MVC 架构是 Django 设计模式的精髓。接下来,我们就来具体看一下这个架构,并通过 Django 动手搭建一个服务端。
### 设计模型 Model
在之前的日志和存储系统这节课中,我介绍过 peewee 这个库,它能避开通过繁琐的 SQL 语句来操作 MySQL直接使用 Python 的 class 来进行转换。事实上,这也是 Django 采取的方式。
Django 无需数据库就可以使用它通过对象关系映射器object-relational mapping仅使用Python代码就可以描述数据结构。
我们先来看下面这段 Model 代码:
```
# TradingMonitor/models.py
from django.db import models
class Position(models.Model):
asset = models.CharField(max_length=10)
timestamp = models.DateTimeField()
amount = models.DecimalField(max_digits=10, decimal_places=3)
```
models.py 文件主要用一个 Python 类来描述数据表,称为模型 。运用这个类,你可以通过简单的 Python 代码来创建、检索、更新、删除数据库中的记录而不用写一条又一条的SQL语句这也是我们之前所说的避免通过 SQL 操作数据库。
在这里,我们创建了一个 Position 模型,用来表示我们的交易仓位信息。其中,
- asset 表示当前持有资产的代码,例如 btc
- timestamp 表示时间戳;
- amount 则表示时间戳时刻的持仓信息。
### 设计视图 Views
在模型被定义之后,我们便可以在视图中引用模型了。通常,视图会根据参数检索数据,加载一个模板,并使用检索到的数据呈现模板。
设计视图,则是我们用来实现业务逻辑的地方。我们来看 render_positions 这个代码,它接受 request 和 asset 两个参数,我们先不用管 request。这里的 asset 表示指定一个资产名称,例如 btc然后这个函数返回一个渲染页面。
```
# TradingMonitor/views.py
from django.shortcuts import render
from .models import Position
def render_positions(request, asset):
positions = Position.objects.filter(asset = asset)
context = {'asset': asset, 'positions': positions}
return render(request, 'positions.html', context)
```
不过,这个函数具体是怎么工作的呢?我们一行行来看。
`positions = Position.objects.filter(asset = asset)`,这行代码向数据库中执行一个查询操作,其中, filter 表示筛选,意思是从数据库中选出所有我们需要的 asset 的信息。不过,这里我只是为你举例做示范;真正做监控的时候,我们一般会更有针对性地从数据库中筛选读取信息,而不是一口气读取出所有的信息。
`context = {'asset': asset, 'positions': positions}`,这行代码没什么好说的,封装一个字典。至于这个字典的用处,下面的内容中可以体现。
`return render(request, 'positions.html', context)`,最后这行代码返回一个页面。这里我们采用的模板设计,这也是 Django 非常推荐的开发方式,也就是让模板和数据分离,这样,数据只需要向其中填充即可。
最后的模板文件是 `position.html`,你应该注意到了, context 作为变量传给了模板,下面我们就来看一下设计模板的内容。
### 设计模板Templates
模板文件,其实就是 HTML 文件和部分代码的综合。你可以想象成这个HTML 在最终送给用户之前,需要被我们预先处理一下,而预先处理的方式就是找到对应的地方进行替换。
我们来看下面这段示例代码:
```
# TradingMonitor/templates/positions.html
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en-US&quot;&gt;
&lt;head&gt;
&lt;title&gt;Positions for {{asset}}&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Positions for {{asset}}&lt;/h1&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
{% for position in positions %}
&lt;tr&gt;
&lt;th&gt;{{position.timestamp}}&lt;/th&gt;
&lt;th&gt;{{position.amount}}&lt;/th&gt;
&lt;/tr&gt;
{% endfor %}
&lt;/table&gt;
&lt;/body&gt;
```
我重点说一下几个地方。首先是`&lt;title&gt;Positions for {{asset}}&lt;/title&gt;`,这里双大括号括住 asset 这个变量,这个变量对应的正是前面 context 字典中的 asset key。Django 的渲染引擎会将 asset ,替换成 context 中 asset 对应的内容,此处是替换成了 btc。
再来看`{% for position in positions %}`,这是个很关键的地方。我们需要处理一个列表的情况,用 for 对 positions 进行迭代就行了。这里的 positions ,同样对应的是 context 中的 positions。
末尾的`{% endfor %}`,自然就表示结束了。这样,我们就将数据封装到了一个列表之中。
### 设计链接 Urls
最后,我们需要为我们的操作提供 URL 接口,具体操作我放在了下面的代码中,内容比较简单,我就不详细展开讲解了。
```
# TradingMonitor/urls.py
from django.contrib import admin
from django.urls import path
from . import views
urlpatterns = [
path('admin/', admin.site.urls),
path('positions/&lt;str:asset&gt;', views.render_positions),
]
```
到这里,我们就可以通过 `http://127.0.0.1:8000/positions/btc` 来访问啦!
### 测试
当然,除了主要流程外,我还需要强调几个很简单但非常关键的细节,不然,我们这些改变就不能被真正地应用。
第一步,在 `TradingMonitor/TradingMonitor` 下,新建一个文件夹 migrations并在这个文件夹中新建一个空文件 `__init__.py`
```
mkdir TradingMonitor/migrations
touch TradingMonitor/migrations/__init__.py
```
此时,你的目录结构应该长成下面这样:
```
TradingMonitor/
├── TradingMonitor
│ ├── migrations
│ └── __init__.py
│ ├── templates
│ └── positions.html
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ ├── models.py
│ ├── views.py
│ └── wsgi.py
├── db.sqlite3
└── manage.py
```
第二步,修改 `TradingMonitor/settings.py`
```
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'TradingMonitor', # 这里把我们的 app 加上
]
```
```
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'TradingMonitor/templates')], # 这里把 templates 的目录加上
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
```
第三步,运行 `python manage.py makemigrations`
```
python manage.py makemigrations
########## 输出 ##########
Migrations for 'TradingMonitor':
TradingMonitor/migrations/0001_initial.py
- Create model Position
```
第四步,运行 `python manage.py migrate`
```
python manage.py migrate
########## 输出 ##########
Operations to perform:
Apply all migrations: TradingMonitor, admin, auth, contenttypes, sessions
Running migrations:
Applying TradingMonitor.0001_initial... OK
```
这几步的具体操作,我都用代码和注释表示了出来,你完全可以同步进行操作。操作完成后,现在,我们的数据结构就已经被成功同步到数据库中了。
最后,输入 `python manage.py runserver`,然后打开浏览器输入`http://127.0.0.1:8000/positions/btc`,你就能看到效果啦。<br>
<img src="https://static001.geekbang.org/resource/image/ab/4a/abb5a9aaf8016f485d38552f4291784a.png" alt="">
现在,我们再回过头来看一下 MVC 模式通过我画的这张图你可以看到M、V、C这三者以一种插件似的、松耦合的方式连接在一起
<img src="https://static001.geekbang.org/resource/image/c7/bc/c7ff058064e869d6da652805c29263bc.png" alt="">
当然,我带你写的只是一个简单的 Django 应用程序,对于真正的量化平台监控系统而言,这还只是一个简单的开始。
除此之外,对于监控系统来说,其实还有着非常多的开源插件可以使用。有一些界面非常酷炫,有一些可以做到很高的稳定性和易用性,它们很多都可以结合 Django 做出很好的效果来。比较典型的有:
- Graphite 是一款存储时间序列数据,并通过 Django Web 应用程序在图形中显示的插件;
- Vimeo 则是一个基于 Graphite 的仪表板,具有附加功能和平滑的设计;
- Scout 监控 Django和Flask应用程序的性能提供自动检测视图、SQL查询、模板等。
## 总结
这一节课的内容更靠近上游应用层,我们以 Django 这个 Python 后端为例,讲解了搭建一个服务端的过程。你应该发现了,使用 RESTful Framework 搭建服务器,是一个如此简单的过程,你可以去开一个自己的交易所了(笑)。相比起具体的技术,今天我所讲的 MVC 框架和 Django 的思想,更值得你去深入学习和领会。
## 思考题
今天我想给你留一个难度比较高的作业。RESTful API 在 Django 中是如何实现安全认证的?你能通过搜索和自学掌握这个知识点吗?希望可以在留言区看到你的认真学习记录和总结,我会一一给出建议。也欢迎你把这篇文章分享给你的朋友、同事,一起交流、一起进步。

View File

@@ -0,0 +1,93 @@
<audio id="audio" title="40 | 总结Python中的数据结构与算法全景" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/99/8e/998287b30aae55edbb216fed5db1228e.mp3"></audio>
你好,我是景霄。
不知不觉中,我们又一起完成了量化交易实战篇的学习。我非常高兴看到很多同学一直在坚持积极地学习,并且留下了很多高质量的留言,值得我们互相思考交流。也有一些同学反复推敲,指出了文章中一些表达不严谨或是不当的地方,我也表示十分感谢。
实战篇的主要用意,是通过一个完整的技术领域,讲明白 Python 在这个领域中如何发挥作用。所以,我们在每节课都会梳理一个小知识点;同时,也在第 36 讲中,我用大量篇幅讲解了策略和回测系统,作为量化交易中最重要内容的解释。
对于本章答疑因为不断有同学留言询问Python中数据结构和算法相关的问题我在这里也简单说一下。
<img src="https://static001.geekbang.org/resource/image/4b/80/4b72bbef1367976a203bd29a36b09d80.png" alt="">
首先希望你明白我们Python 专栏的定位是有一定计算机知识基础的进阶课程,重点在 Python 的核心知识点上,默认你对基础的算法和数据结构有一定的了解。因此,在语法和技术知识点的讲解过程中,我会综合性地穿插不少数据结构的基本知识,但并不会进行深入地讲解。涉及到数据结构中的关键名词和难点,自然都会有所提及,但还是希望你有一定的自学能力来掌握。
不过为了进一步方便你理解Python的数据结构和算法加深对 Python 基础内容的掌握,我在这里总结了一个综合性的提纲。如果你在这方面有所欠缺,可以参考性地借鉴学习一下。当然,有时间和精力的话,我最鼓励的是你可以通过 Python 把所有数据结构和算法实现一下。
## 基础数据结构:数组,堆,栈,队列,链表
数组自不必多说Python 中的基础数组,满足 O(1) 的随机查找,和 O(n) 的随机插入。
堆,严格来讲,是一种特殊的二叉树,满足 O(nlogn) 的随机插入和删除,以及 O(1) 时间复杂度拿到最大值或者最小值。堆可以用来实现优先队列,还可以在项目中实现多任务调度,有着非常广泛的应用。
栈,是一种先进后出的数据结构,入栈和出栈操作都是 O(1) 时间复杂度。
队列和栈对应,不过功能刚好相反,它是一种先进先出的数据结构,一如其名,先排队者先服务。入队和出队也是 O(1) 的时间复杂度。栈和队列都能用数组来实现,但是对空间的规划需要注意,特别是用数组实现的队列,我们通常用的是循环队列。
链表则是另一种线性表,和数组的不同是,它不支持随机访问,你不能通过下标来获取链表的元素。链表的元素通过指针相连,单链表中元素可以指向后者,双链表则是让相邻的元素互相连接。
这些基础数据结构,在 Python 中都有很好的库和包支持,从使用上来说都非常方便,但我仍然希望你对原理能有一定的了解,这样,处理起复杂问题也能得心应手不胆怯。
## 进阶数据结构无向图有向图DAG 图,字典树,哈希表
无向图,是由顶点和边组成的数据结构,一条边连接两个顶点(如果两个顶点是一个,这条边称为自环)。一如其名,“无向”,所以它的边没有指向性。
有向图,和无向图一样都是“图”这种数据结构,不同的是有向图的边有指向性,方向为一个顶点指向另一个顶点。
树这种数据结构,则可以分为有根树和无根树。前者中,最常见的就是我们的二叉树,从顶点开始一级级向下,每个父结点最多有两个子结点。至于无根树,则是一种特殊的无向图,无环连通的无向图被称为无根树,它有很多特别的性质和优点,在离散数学中应用广泛。
DAG 图,也叫做有向无环图,是一种特殊应用的数据结构,在图的动态规划问题中出现甚多。遍历 DAG 图的方式也就是我们常说的拓扑排序是一种图算法。DAG 可以认为是链表的图版本,如果说区块链是链表,那么区块链 3.0 时代可能就是 DAG 图。
字典树,又被称为 Trie 树,是一种边为字符的有向图,它在字符串处理中有着非常强大的应用。广为人知的 AC 自动机,就是用 Trie 树来解决多模式字符串匹配问题。Trie 树在工业界也常被拿来做搜索提示,例如你在百度中搜索 “极客时”,就会自动跳出 “极客时间”。
哈希表,这一定是程序员应用最广、自觉最简单的一个数据结构,比如 Python 的 dict() 就可以拿来即用,简单而自然。不过,哈希表其实有着非常深刻的内涵,冲突算法、哈希算法、扩容算法,都很值得我们去深究一下。
## 算法:排序
从排序开始入门算法有一定的难度,因为这需要你理解时间复杂度的概念,开始接触到基本的二分思想以及严谨的数学证明过程。不过,不管难度如何,我想强调的是,在学习的过程中一定不要跳过这些必需的科学训练。如果你忽略基础,只会调用 list.sort(),未来遇到稍复杂的问题基本懵圈,需要花费更多的时间来重走基础路,得不偿失。
我们可以从基础的冒泡排序开始理解排序,这是一个很好理解正确性和代码的算法;然后是选择排序和插入排序,它们和冒泡排序一样,都是 O(n^2) 时间复杂度的算法。
从归并排序开始,算法复杂度骤降到 O(nlogn) 的理论下界这里也开始涉及到算法中的一个经典思想——分治Divide and Conquer。然后就是快速排序、堆排序这些算法他们和快速排序一样都是 O(nlogn) 级别。
除此之外,还有一些针对性的优化排序,比如计数排序、桶排序、基数排序等,在特定条件下可以做到 O(n) 的时间复杂度。
关于各种算法我推荐你可以查看这个B站的视频[https://www.bilibili.com/video/av685670](https://www.bilibili.com/video/av685670)
## 算法:二分搜索
二分搜索也是一种思想,甚至在生活中都有很广泛的应用(笑),比如书本的翻页设计是一种二分,你不需要查找很多次,就能找到自己想要的那一页。再比如就是很有名的,就是女生通过图书馆的[笑话](https://twitter.com/xueshudi/status/911375561498357761)了。
>
图书馆自习的时候一女生背着一堆书进阅览室结果警报响了大妈让女生看是哪本书把警报弄响了女生把书倒出来一本一本地测。大妈见状急了把书分成两份第一份过了一下响了。又把这一份分成两份接着测三回就找到了大妈用鄙视的眼神看着女生仿佛在说O(n)和O(log2n)都分不清。
对于二分搜索算法,你千万不要只是套用 API 和简单的代码,一定要从本质上理解二分思想,做到活学活用。
## 算法深度优先搜索DFS和广度优先搜索BFS
DFS 和 BFS是图论算法中的基础。你需要先把这两个基础知识点掌握下来然后学习几个经典算法比如最短路算法、并查集、记忆化深度优先搜索、拓扑排序、DAG 图上的 DP 等等。
这里要注意我们的重点还是学习思想。对于业务逻辑而言图算法的重要性可能并没有那么大但是当你开始接触技术栈深层接触大数据Hadoop Spark接触神经网络和人工智能时你会发现图的基本思想早已渗透到了设计模式中而 DFS 和 BFS 正是操作图的最基础的两把钥匙。
## 算法:贪心和动态规划
这两个算法依然是两种重要的思维。虽然在绝大部分程序员的工作中,这两个算法可能一年都不会被用到过几次,但同样的,这些都是向更高技术能力升级必备的基本功。你不需要掌握到能够参加 ACM 世界总决赛的级别,但是,我们哪怕是对基本的方法论能有所了解,都将受益匪浅。
>
曾有参加过 ACM 竞赛的朋友和我讲过,说他学懂动态规划后,感觉整个人生观和方法论都有了变化。在那之后,他自己去思考一些现实生活中的决策时,就会明白哪些是短视的贪心,哪些才是长远考虑的动态规划(笑)。
## 总结
作为Python语言专栏我确实不可能给你把每一种数据结构和算法都详细讲解一遍但是还是那句话基础的数据结构和算法一定是每个程序员的基本功。
这里,我推荐你可以学习极客时间上王争老师的[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126)专栏,以及覃超老师的[《算法面试通关40讲》](https://time.geekbang.org/course/intro/130)视频课程。这两位在 Google和 Facebook 工作过的老师,同样底子扎实、实战经验丰富,将会给你带来不同角度的更翔实的算法精讲。
在数据爆炸的互联网的今天,学习资料触手可及,时间就显得更加宝贵。我在这里列出这些纲要的目的,也是希望能够帮你节省时间,为你整理出适合入门学习、掌握的基础知识点,让你可以带着全局观更有针对性地去学习。
当然,一切可以取得成果的学习,都离不开我们自己付出的努力。也只有这样,掌握了数据结构和算法的你,才能在数学基础上对 Python 的理解更进一步。同时,在未来的项目设计中,这些思维亦会在无形之中,帮你设计出更高质量的系统和架构,可以说是终生受益的学习投资了。
希望你可以学会并且切实有所收获,如果在哪个地方有所困惑,也欢迎在留言区和我交流讨论,我们一起精进和提高!