mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-16 22:23:45 +08:00
mod
This commit is contained in:
183
极客时间专栏/Python核心技术与实战/量化交易实战篇/33 | 带你初探量化世界.md
Normal file
183
极客时间专栏/Python核心技术与实战/量化交易实战篇/33 | 带你初探量化世界.md
Normal 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))
|
||||
|
||||
########## 输出 ##########
|
||||
|
||||
{
|
||||
"bid": "8825.88",
|
||||
"ask": "8827.52",
|
||||
"volume": {
|
||||
"BTC": "910.0838782726",
|
||||
"USD": "7972904.560901317851",
|
||||
"timestamp": 1560643800000
|
||||
},
|
||||
"last": "8838.45"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
对算法交易系统来说,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?为什么?欢迎在留言区写下你的想法,也欢迎你把这篇文章分享给更多对量化交易感兴趣的人,我们一起交流和探讨。
|
||||
256
极客时间专栏/Python核心技术与实战/量化交易实战篇/34 | RESTful & Socket: 搭建交易执行层核心.md
Normal file
256
极客时间专栏/Python核心技术与实战/量化交易实战篇/34 | RESTful & Socket: 搭建交易执行层核心.md
Normal 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 = "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](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?为什么?欢迎留言写下你的思考,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
328
极客时间专栏/Python核心技术与实战/量化交易实战篇/35 | RESTful & Socket: 行情数据对接和抓取.md
Normal file
328
极客时间专栏/Python核心技术与实战/量化交易实战篇/35 | RESTful & Socket: 行情数据对接和抓取.md
Normal 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("https://api.gemini.com/v1/book/btcusd").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 "TCP handshake: %{time_connect}s, SSL handshake: %{time_appconnect}s\n" -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="{0}".format(i)
|
||||
ws.send(msg)
|
||||
print('Sent: ' + msg)
|
||||
# 休息1秒用于接收服务器回复的消息
|
||||
time.sleep(1)
|
||||
|
||||
# 关闭Websocket的连接
|
||||
ws.close()
|
||||
print("Websocket closed")
|
||||
|
||||
# 在另一个线程运行gao()函数
|
||||
thread.start_new_thread(gao, ())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ws = websocket.WebSocketApp("ws://echo.websocket.org/",
|
||||
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__ == "__main__":
|
||||
ws = websocket.WebSocketApp(
|
||||
"wss://api.gemini.com/v1/marketdata/btcusd?top_of_book=true&offers=true",
|
||||
on_message=on_message)
|
||||
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||
|
||||
###### 输出 #######
|
||||
{"type":"update","eventId":7275473603,"socket_sequence":0,"events":[{"type":"change","reason":"initial","price":"11386.12","delta":"1.307","remaining":"1.307","side":"ask"}]}
|
||||
{"type":"update","eventId":7275475120,"timestamp":1562380981,"timestampms":1562380981991,"socket_sequence":1,"events":[{"type":"change","side":"ask","price":"11386.62","remaining":"1","reason":"top-of-book"}]}
|
||||
{"type":"update","eventId":7275475271,"timestamp":1562380982,"timestampms":1562380982387,"socket_sequence":2,"events":[{"type":"change","side":"ask","price":"11386.12","remaining":"1.3148","reason":"top-of-book"}]}
|
||||
{"type":"update","eventId":7275475838,"timestamp":1562380986,"timestampms":1562380986270,"socket_sequence":3,"events":[{"type":"change","side":"ask","price":"11387.16","remaining":"0.072949","reason":"top-of-book"}]}
|
||||
{"type":"update","eventId":7275475935,"timestamp":1562380986,"timestampms":1562380986767,"socket_sequence":4,"events":[{"type":"change","side":"ask","price":"11389.22","remaining":"0.06204196","reason":"top-of-book"}]}
|
||||
|
||||
```
|
||||
|
||||
可以看到,在和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')
|
||||
|
||||
###### 输出 #######
|
||||
|
||||
{"bids": [[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]], "asks": [[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]], "ts": 1562558996535}
|
||||
{"bids": [[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]], "asks": [[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]], "ts": 1562558997377}
|
||||
{"bids": [[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]], "asks": [[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]], "ts": 1562558997765}
|
||||
{"bids": [[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]], "asks": [[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]], "ts": 1562558998638}
|
||||
{"bids": [[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]], "asks": [[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]], "ts": 1562558998645}
|
||||
{"bids": [[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]], "asks": [[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]], "ts": 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 爬虫又会发生什么?这一点应该如何避免呢?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。
|
||||
566
极客时间专栏/Python核心技术与实战/量化交易实战篇/36 | Pandas & Numpy: 策略与回测系统.md
Normal file
566
极客时间专栏/Python核心技术与实战/量化交易实战篇/36 | Pandas & Numpy: 策略与回测系统.md
Normal 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), "文件不存在")
|
||||
|
||||
# 读取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__() > 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:
|
||||
"""
|
||||
Backtest回测类,用于读取历史行情数据、执行策略、模拟交易并估计
|
||||
收益。
|
||||
|
||||
初始化的时候调用Backtest.run来时回测
|
||||
|
||||
instance, or `backtesting.backtesting.Backtest.optimize` to
|
||||
optimize it.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
data: pd.DataFrame,
|
||||
strategy_type: type(Strategy),
|
||||
broker_type: type(ExchangeAPI),
|
||||
cash: float = 10000,
|
||||
commission: float = .0):
|
||||
"""
|
||||
构造回测对象。需要的参数包括:历史数据,策略对象,初始资金数量,手续费率等。
|
||||
初始化过程包括检测输入类型,填充数据空值等。
|
||||
|
||||
参数:
|
||||
: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
|
||||
"""
|
||||
|
||||
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 & {'Open', 'High', 'Low', 'Close', 'Volume'}) == 5,
|
||||
("输入的`data`格式不正确,至少需要包含这些列:"
|
||||
"'Open', 'High', 'Low', 'Close'"))
|
||||
|
||||
# 检查缺失值
|
||||
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):
|
||||
"""
|
||||
运行回测,迭代历史数据,执行模拟交易并返回回测结果。
|
||||
Run the backtest. Returns `pd.Series` with results and statistics.
|
||||
|
||||
Keyword arguments are interpreted as strategy parameters.
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
返回简单滑动平均
|
||||
"""
|
||||
return pd.Series(values).rolling(n).mean()
|
||||
|
||||
def crossover(series1, series2) -> bool:
|
||||
"""
|
||||
检查两个序列是否在结尾交叉
|
||||
:param series1: 序列1
|
||||
:param series2: 序列2
|
||||
:return: 如果交叉返回True,反之False
|
||||
"""
|
||||
return series1[-2] < series2[-2] and series1[-1] > 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):
|
||||
"""
|
||||
抽象策略类,用于定义交易策略。
|
||||
|
||||
如果要定义自己的策略类,需要继承这个基类,并实现两个抽象方法:
|
||||
Strategy.init
|
||||
Strategy.next
|
||||
"""
|
||||
def __init__(self, broker, data):
|
||||
"""
|
||||
构造策略对象。
|
||||
|
||||
@params broker: ExchangeAPI 交易API接口,用于模拟交易
|
||||
@params data: list 行情数据数据
|
||||
"""
|
||||
self._indicators = []
|
||||
self._broker = broker # type: _Broker
|
||||
self._data = data # type: _Data
|
||||
self._tick = 0
|
||||
|
||||
def I(self, func: Callable, *args) -> np.ndarray:
|
||||
"""
|
||||
计算买卖指标向量。买卖指标向量是一个数组,长度和历史数据对应;
|
||||
用于判定这个时间点上需要进行"买"还是"卖"。
|
||||
|
||||
例如计算滑动平均:
|
||||
def init():
|
||||
self.sma = self.I(utils.SMA, self.data.Close, N)
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
初始化策略。在策略回测/执行过程中调用一次,用于初始化策略内部状态。
|
||||
这里也可以预计算策略的辅助参数。比如根据历史行情数据:
|
||||
计算买卖的指示器向量;
|
||||
训练模型/初始化模型参数
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def next(self, tick):
|
||||
"""
|
||||
步进函数,执行第tick步的策略。tick代表当前的"时间"。比如data[tick]用于访问当前的市场价格。
|
||||
"""
|
||||
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 < cash, "初始现金数量大于0,输入的现金数量:{}".format(cash))
|
||||
assert_msg(0 <= commission <= 0.05, "合理的手续费率一般不会超过5%,输入的费率:{}".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):
|
||||
"""
|
||||
:return: 返回当前账户现金数量
|
||||
"""
|
||||
return self._cash
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
"""
|
||||
:return: 返回当前账户仓位
|
||||
"""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def initial_cash(self):
|
||||
"""
|
||||
:return: 返回初始现金数量
|
||||
"""
|
||||
return self._inital_cash
|
||||
|
||||
@property
|
||||
def market_value(self):
|
||||
"""
|
||||
:return: 返回当前市值
|
||||
"""
|
||||
return self._cash + self._position * self.current_price
|
||||
|
||||
@property
|
||||
def current_price(self):
|
||||
"""
|
||||
:return: 返回当前市场价格
|
||||
"""
|
||||
return self._data.Close[self._i]
|
||||
|
||||
def buy(self):
|
||||
"""
|
||||
用当前账户剩余资金,按照市场价格全部买入
|
||||
"""
|
||||
self._position = float(self._cash / (self.current_price * (1 + self._commission)))
|
||||
self._cash = 0.0
|
||||
|
||||
def sell(self):
|
||||
"""
|
||||
卖出当前账户剩余持仓
|
||||
"""
|
||||
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数据吗?欢迎在留言区写下你的答案和问题,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
237
极客时间专栏/Python核心技术与实战/量化交易实战篇/37 | Kafka & ZMQ:自动化交易流水线.md
Normal file
237
极客时间专栏/Python核心技术与实战/量化交易实战篇/37 | Kafka & ZMQ:自动化交易流水线.md
Normal 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 发消息给 server,server 处理后返回给 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("msg: %s" % 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("msg: %s" % 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 应该怎么做呢?
|
||||
|
||||
欢迎留言写下你的思考和疑惑,也欢迎你把这篇文章分享给更多的人一起学习。
|
||||
|
||||
|
||||
239
极客时间专栏/Python核心技术与实战/量化交易实战篇/38 | MySQL:日志和数据存储系统.md
Normal file
239
极客时间专栏/Python核心技术与实战/量化交易实战篇/38 | MySQL:日志和数据存储系统.md
Normal 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(
|
||||
"2019-07-14 14:12:17",
|
||||
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 框架。
|
||||
|
||||
这里所说的ORM(Object 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 > 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 的使用方法讲起,再讲到后面的量化系统框架。数据库和数据在绝大部分互联网行业都是核心,对量化从业者来说也是重要的生产资料。而搭建一套负载合理、数据可靠的数据系统,也需要一个量化团队长期打磨,并根据需求进行迭代。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后给你留一道思考题。量化交易需要的数据量不是很大,但是有可能出现调用频率极高的情况,例如回测系统。那么,你能想到哪些优化手段,来降低调用代价吗?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。
|
||||
370
极客时间专栏/Python核心技术与实战/量化交易实战篇/39 | Django:搭建监控平台.md
Normal file
370
极客时间专栏/Python核心技术与实战/量化交易实战篇/39 | Django:搭建监控平台.md
Normal 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
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Positions for {{asset}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Positions for {{asset}}</h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
{% for position in positions %}
|
||||
<tr>
|
||||
<th>{{position.timestamp}}</th>
|
||||
<th>{{position.amount}}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</body>
|
||||
|
||||
```
|
||||
|
||||
我重点说一下几个地方。首先是`<title>Positions for {{asset}}</title>`,这里双大括号括住 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/<str:asset>', 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 中是如何实现安全认证的?你能通过搜索和自学掌握这个知识点吗?希望可以在留言区看到你的认真学习记录和总结,我会一一给出建议。也欢迎你把这篇文章分享给你的朋友、同事,一起交流、一起进步。
|
||||
|
||||
|
||||
93
极客时间专栏/Python核心技术与实战/量化交易实战篇/40 | 总结:Python中的数据结构与算法全景.md
Normal file
93
极客时间专栏/Python核心技术与实战/量化交易实战篇/40 | 总结:Python中的数据结构与算法全景.md
Normal 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 的理解更进一步。同时,在未来的项目设计中,这些思维亦会在无形之中,帮你设计出更高质量的系统和架构,可以说是终生受益的学习投资了。
|
||||
|
||||
希望你可以学会并且切实有所收获,如果在哪个地方有所困惑,也欢迎在留言区和我交流讨论,我们一起精进和提高!
|
||||
|
||||
|
||||
Reference in New Issue
Block a user