first commit
This commit is contained in:
@ -0,0 +1,273 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
31 pdb & cProfile:调试和性能分析的法宝
|
||||
你好,我是景霄。
|
||||
|
||||
在实际生产环境中,对代码进行调试和性能分析,是一个永远都逃不开的话题。调试和性能分析的主要场景,通常有这么三个:
|
||||
|
||||
|
||||
一是代码本身有问题,需要我们找到root cause并修复;
|
||||
二是代码效率有问题,比如过度浪费资源,增加latency,因此需要我们debug;
|
||||
三是在开发新的feature时,一般都需要测试。
|
||||
|
||||
|
||||
在遇到这些场景时,究竟应该使用哪些工具,如何正确的使用这些工具,应该遵循什么样的步骤等等,就是这节课我们要讨论的话题。
|
||||
|
||||
用pdb进行代码调试
|
||||
|
||||
pdb的必要性
|
||||
|
||||
首先,我们来看代码的调试。也许不少人会有疑问:代码调试?说白了不就是在程序中使用print()语句吗?
|
||||
|
||||
没错,在程序中相应的地方打印,的确是调试程序的一个常用手段,但这只适用于小型程序。因为你每次都得重新运行整个程序,或是一个完整的功能模块,才能看到打印出来的变量值。如果程序不大,每次运行都非常快,那么使用print(),的确是很方便的。
|
||||
|
||||
但是,如果我们面对的是大型程序,运行一次的调试成本很高。特别是对于一些tricky的例子来说,它们通常需要反复运行调试、追溯上下文代码,才能找到错误根源。这种情况下,仅仅依赖打印的效率自然就很低了。
|
||||
|
||||
我们可以想象下面这个场景。比如你最常使用的极客时间App,最近出现了一个bug,部分用户无法登陆。于是,后端工程师们开始debug。
|
||||
|
||||
他们怀疑错误的代码逻辑在某几个函数中,如果使用print()语句debug,很可能出现的场景是,工程师们在他们认为的10个最可能出现bug的地方,都使用print()语句,然后运行整个功能块代码(从启动到运行花了5min),看打印出来的结果值,是不是和预期相符。
|
||||
|
||||
如果结果值和预期相符,并能直接找到错误根源,显然是最好的。但实际情况往往是,
|
||||
|
||||
|
||||
要么与预期并不相符,需要重复以上步骤,继续debug;
|
||||
要么虽说与预期相符,但前面的操作只是缩小了错误代码的范围,所以仍得继续添加print()语句,再一次运行相应的代码模块(又要5min),进行debug。
|
||||
|
||||
|
||||
你可以看到,这样的效率就很低下了。哪怕只是遇到稍微复杂一点的case,两、三个工程师一下午的时间可能就没了。
|
||||
|
||||
可能又有人会说,现在很多的IDE不都有内置的debug工具吗?
|
||||
|
||||
这话说的也没错。比如我们常用的Pycharm,可以很方便地在程序中设置断点。这样程序只要运行到断点处,便会自动停下,你就可以轻松查看环境中各个变量的值,并且可以执行相应的语句,大大提高了调试的效率。
|
||||
|
||||
看到这里,你不禁会问,既然问题都解决了,那为什么还要学习pdb呢?其实在很多大公司,产品的创造与迭代,往往需要很多编程语言的支持;并且,公司内部也会开发很多自己的接口,尝试把尽可能多的语言给结合起来。
|
||||
|
||||
这就使得,很多情况下,单一语言的IDE,对混合代码并不支持UI形式的断点调试功能,或是只对某些功能模块支持。另外,考虑到不少代码已经挪到了类似Jupyter的Notebook中,往往就要求开发者使用命令行的形式,来对代码进行调试。
|
||||
|
||||
而Python的pdb,正是其自带的一个调试库。它为Python程序提供了交互式的源代码调试功能,是命令行版本的IDE断点调试器,完美地解决了我们刚刚讨论的这个问题。
|
||||
|
||||
如何使用pdb
|
||||
|
||||
了解了pdb的重要性与必要性后,接下来,我们就一起来看看,pdb在Python中到底应该如何使用。
|
||||
|
||||
首先,要启动pdb调试,我们只需要在程序中,加入“import pdb”和“pdb.set_trace()”这两行代码就行了,比如下面这个简单的例子:
|
||||
|
||||
a = 1
|
||||
b = 2
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
c = 3
|
||||
print(a + b + c)
|
||||
|
||||
|
||||
当我们运行这个程序时时,它的输出界面是下面这样的,表示程序已经运行到了“pdb.set_trace()”这行,并且暂停了下来,等待用户输入。
|
||||
|
||||
> /Users/jingxiao/test.py(5)<module>()
|
||||
-> c = 3
|
||||
|
||||
|
||||
这时,我们就可以执行,在IDE断点调试器中可以执行的一切操作,比如打印,语法是"p <expression>":
|
||||
|
||||
(pdb) p a
|
||||
1
|
||||
(pdb) p b
|
||||
2
|
||||
|
||||
|
||||
你可以看到,我打印的是a和b的值,分别为1和2,与预期相符。为什么不打印c呢?显然,打印c会抛出异常,因为程序目前只运行了前面几行,此时的变量c还没有被定义:
|
||||
|
||||
(pdb) p c
|
||||
*** NameError: name 'c' is not defined
|
||||
|
||||
|
||||
除了打印,常见的操作还有“n”,表示继续执行代码到下一行,用法如下:
|
||||
|
||||
(pdb) n
|
||||
-> print(a + b + c)
|
||||
|
||||
|
||||
而命令”l“,则表示列举出当前代码行上下的11行源代码,方便开发者熟悉当前断点周围的代码状态:
|
||||
|
||||
(pdb) l
|
||||
1 a = 1
|
||||
2 b = 2
|
||||
3 import pdb
|
||||
4 pdb.set_trace()
|
||||
5 -> c = 3
|
||||
6 print(a + b + c)
|
||||
|
||||
|
||||
命令“s“,就是 step into 的意思,即进入相对应的代码内部。这时,命令行中会显示”--Call--“的字样,当你执行完内部的代码块后,命令行中则会出现”--Return--“的字样。
|
||||
|
||||
我们来看下面这个例子:
|
||||
|
||||
def func():
|
||||
print('enter func()')
|
||||
|
||||
a = 1
|
||||
b = 2
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
func()
|
||||
c = 3
|
||||
print(a + b + c)
|
||||
|
||||
# pdb
|
||||
> /Users/jingxiao/test.py(9)<module>()
|
||||
-> func()
|
||||
(pdb) s
|
||||
--Call--
|
||||
> /Users/jingxiao/test.py(1)func()
|
||||
-> def func():
|
||||
(Pdb) l
|
||||
1 -> def func():
|
||||
2 print('enter func()')
|
||||
3
|
||||
4
|
||||
5 a = 1
|
||||
6 b = 2
|
||||
7 import pdb
|
||||
8 pdb.set_trace()
|
||||
9 func()
|
||||
10 c = 3
|
||||
11 print(a + b + c)
|
||||
|
||||
(Pdb) n
|
||||
> /Users/jingxiao/test.py(2)func()
|
||||
-> print('enter func()')
|
||||
(Pdb) n
|
||||
enter func()
|
||||
--Return--
|
||||
> /Users/jingxiao/test.py(2)func()->None
|
||||
-> print('enter func()')
|
||||
|
||||
(Pdb) n
|
||||
> /Users/jingxiao/test.py(10)<module>()
|
||||
-> c = 3
|
||||
|
||||
|
||||
这里,我们使用命令”s“进入了函数func()的内部,显示”--Call--“;而当我们执行完函数func()内部语句并跳出后,显示”--Return--“。
|
||||
|
||||
另外,
|
||||
|
||||
|
||||
与之相对应的命令”r“,表示step out,即继续执行,直到当前的函数完成返回。
|
||||
命令”b [ ([filename:]lineno | function) [, condition] ]“可以用来设置断点。比方说,我想要在代码中的第10行,再加一个断点,那么在pdb模式下输入”b 11“即可。
|
||||
而”c“则表示一直执行程序,直到遇到下一个断点。
|
||||
|
||||
|
||||
当然,除了这些常用命令,还有许多其他的命令可以使用,这里我就不在一一赘述了。你可以参考对应的官方文档(https://docs.python.org/3/library/pdb.html#module-pdb),来熟悉这些用法。
|
||||
|
||||
用cProfile进行性能分析
|
||||
|
||||
关于调试的内容,我主要先讲这么多。事实上,除了要对程序进行调试,性能分析也是每个开发者的必备技能。
|
||||
|
||||
日常工作中,我们常常会遇到这样的问题:在线上,我发现产品的某个功能模块效率低下,延迟(latency)高,占用的资源多,但却不知道是哪里出了问题。
|
||||
|
||||
这时,对代码进行profile就显得异常重要了。
|
||||
|
||||
这里所谓的profile,是指对代码的每个部分进行动态的分析,比如准确计算出每个模块消耗的时间等。这样你就可以知道程序的瓶颈所在,从而对其进行修正或优化。当然,这并不需要你花费特别大的力气,在Python中,这些需求用cProfile就可以实现。
|
||||
|
||||
举个例子,比如我想计算斐波拉契数列,运用递归思想,我们很容易就能写出下面这样的代码:
|
||||
|
||||
def fib(n):
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1:
|
||||
return 1
|
||||
else:
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
def fib_seq(n):
|
||||
res = []
|
||||
if n > 0:
|
||||
res.extend(fib_seq(n-1))
|
||||
res.append(fib(n))
|
||||
return res
|
||||
|
||||
fib_seq(30)
|
||||
|
||||
|
||||
接下来,我想要测试一下这段代码总的效率以及各个部分的效率。那么,我就只需在开头导入cProfile这个模块,并且在最后运行cProfile.run()就可以了:
|
||||
|
||||
import cProfile
|
||||
# def fib(n)
|
||||
# def fib_seq(n):
|
||||
cProfile.run('fib_seq(30)')
|
||||
|
||||
|
||||
或者更简单一些,直接在运行脚本的命令中,加入选项“-m cProfile”也很方便:
|
||||
|
||||
python3 -m cProfile xxx.py
|
||||
|
||||
|
||||
运行完毕后,我们可以看到下面这个输出界面:
|
||||
|
||||
|
||||
|
||||
这里有一些参数你可能比较陌生,我来简单介绍一下:
|
||||
|
||||
|
||||
ncalls,是指相应代码/函数被调用的次数;
|
||||
tottime,是指对应代码/函数总共执行所需要的时间(注意,并不包括它调用的其他代码/函数的执行时间);
|
||||
tottime percall,就是上述两者相除的结果,也就是tottime / ncalls;
|
||||
cumtime,则是指对应代码/函数总共执行所需要的时间,这里包括了它调用的其他代码/函数的执行时间;
|
||||
cumtime percall,则是cumtime和ncalls相除的平均结果。
|
||||
|
||||
|
||||
了解这些参数后,再来看这张图。我们可以清晰地看到,这段程序执行效率的瓶颈,在于第二行的函数fib(),它被调用了700多万次。
|
||||
|
||||
有没有什么办法可以提高改进呢?答案是肯定的。通过观察,我们发现,程序中有很多对fib()的调用,其实是重复的,那我们就可以用字典来保存计算过的结果,防止重复。改进后的代码如下所示:
|
||||
|
||||
def memoize(f):
|
||||
memo = {}
|
||||
def helper(x):
|
||||
if x not in memo:
|
||||
memo[x] = f(x)
|
||||
return memo[x]
|
||||
return helper
|
||||
|
||||
@memoize
|
||||
def fib(n):
|
||||
if n == 0:
|
||||
return 0
|
||||
elif n == 1:
|
||||
return 1
|
||||
else:
|
||||
return fib(n-1) + fib(n-2)
|
||||
|
||||
|
||||
def fib_seq(n):
|
||||
res = []
|
||||
if n > 0:
|
||||
res.extend(fib_seq(n-1))
|
||||
res.append(fib(n))
|
||||
return res
|
||||
|
||||
fib_seq(30)
|
||||
|
||||
|
||||
这时,我们再对其进行profile,你就会得到新的输出结果,很明显,效率得到了极大的提高。
|
||||
|
||||
|
||||
|
||||
这个简单的例子,便是cProfile的基本用法,也是我今天想讲的重点。当然,cProfile还有很多其他功能,还可以结合stats类来使用,你可以阅读相应的 官方文档 来了解。
|
||||
|
||||
总结
|
||||
|
||||
这节课,我们一起学习了Python中常用的调试工具pdb,和经典的性能分析工具cProfile。pdb为Python程序提供了一种通用的、交互式的高效率调试方案;而cProfile则是为开发者提供了每个代码块执行效率的详细分析,有助于我们对程序的优化与提高。
|
||||
|
||||
关于它们的更多用法,你可以通过它们的官方文档进行实践,都不太难,熟能生巧。
|
||||
|
||||
思考题
|
||||
|
||||
最后,留一个开放性的交流问题。你在平时的工作中,常用的调试和性能分析工具是什么呢?有发现什么独到的使用技巧吗?你曾用到过pdb、cProfile或是其他相似的工具吗?
|
||||
|
||||
欢迎在下方留言与我讨论,也欢迎你把这篇文章分享出去。我们一起交流,一起进步。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,256 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
34 RESTful & Socket:搭建交易执行层核心
|
||||
你好,我是景霄。
|
||||
|
||||
上一节,我们简单介绍了量化交易的历史、严谨的定义和它的基本组成结构。有了这些高层次的基本知识,接下来我们就分模块,开始讲解量化交易系统中具体的部分。
|
||||
|
||||
从这节课开始,我们将实打实地从代码出发,一步步设计出一套清晰完整、易于理解的量化交易系统。
|
||||
|
||||
一个量化交易系统,可以说是一个黑箱。这个黑箱连接交易所获取到的数据,通过策略运算,然后再连接交易所进行下单操作。正如我们在输入输出那节课说的那样,黑箱的特性是输入和输出。每一个设计网络交互的同学,都需要在大脑中形成清晰的交互状态图:
|
||||
|
||||
|
||||
知道包是怎样在网络间传递的;
|
||||
知道每一个节点是如何处理不同的输入包,然后输出并分发给下一级的。
|
||||
|
||||
|
||||
在你搞不明白的时候,可以先在草稿纸上画出交互拓扑图,标注清楚每个节点的输入和输出格式,然后想清楚网络是怎么流动的。这一点,对网络编程至关重要。
|
||||
|
||||
现在,我假设你对网络编程只有很基本的了解。所以接下来,我将先从 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
|
||||
|
||||
|
||||
|
||||
|
||||
你会发现,这个接口不够“RESTful”的地方有:
|
||||
|
||||
|
||||
动词设计不准确,接口使用“POST”而不是重用HTTP动词“DELETE”;
|
||||
URI里包含动词cancel;
|
||||
ID代表的订单是资源,但订单ID是放在参数列表而不是URI里的,因此URI并没有指向资源。
|
||||
|
||||
|
||||
所以严格来说,这不是一个RESTful的接口。
|
||||
|
||||
此外,如果我们去检查Gemini的其他私有接口(Private,私有接口是指需要附加身份验证信息才能访问的接口),我们会发现,那些接口的设计都不是严格RESTful的。不仅如此,大部分的交易所,比如Bitmex、Bitfinex、OKCoin等等,它们提供的“REST接口”,也都不是严格RESTful的。这些接口之所以还能被称为“REST接口”,是因为他们大部分满足了REST接口的另一个重要要求:无状态。
|
||||
|
||||
无状态的意思是,每个REST请求都是独立的,不需要服务器在会话(Session)中缓存中间状态来完成这个请求。简单来说,如果服务器A接收到请求的时候宕机了,而此时把这个请求发送给交易所的服务器B,也能继续完成,那么这个接口就是无状态的。
|
||||
|
||||
这里,我再给你举一个简单的有状态的接口的例子。服务器要求,在客户端请求取消订单的时候,必须发送两次不一样的HTTP请求。并且,第一次发送让服务器“等待取消”;第二次发送“确认取消”。那么,就算这个接口满足了RESTful的动词、资源分离原则,也不是一个REST接口。
|
||||
|
||||
当然,对于交易所的REST接口,你并不需要过于纠结“RESTful”这个概念,否则很容易就被这些名词给绕晕了。你只需要把握住最核心的一点:一个HTTP请求完成一次完整操作。
|
||||
|
||||
交易所 API 简介
|
||||
|
||||
现在,你对 REST 和 Web Socket 应该有一个大致了解了吧。接下来,我们就开始做点有意思的事情。
|
||||
|
||||
首先,我来介绍一下交易所是什么。区块链交易所是个撮合交易平台: 它兼容了传统撮合规则撮合引擎,将资金托管和交割方式替换为区块链。数字资产交易所,则是一个中心化的平台,通过 Web 页面或 PC、手机客户端的形式,让用户将数字资产充值到指定钱包地址(交易所创建的钱包),然后在平台挂买单、卖单以实现数字资产之间的兑换。
|
||||
|
||||
通俗来说,交易所就是一个买和卖的菜市场。有人在摊位上大声喊着:“二斤羊肉啊,二斤羊肉,四斤牛肉来换!”这种人被称为 maker(挂单者)。有的人则游走于不同摊位,不动声色地掏出两斤牛肉,顺手拿走一斤羊肉。这种人被称为 taker(吃单者)。
|
||||
|
||||
交易所存在的意义,一方面是为 maker 和 taker 提供足够的空间活动;另一方面,让一个名叫撮合引擎的玩意儿,尽可能地把单子撮合在一起,然后收取一定比例的保护费…啊不对,是手续费,从而保障游戏继续进行下去。
|
||||
|
||||
市场显然是个很伟大的发明,这里我们就不进行更深入的哲学讨论了。
|
||||
|
||||
然后,我再来介绍一个叫作 Gemini 的交易所。Gemini,双子星交易所,全球首个获得合法经营许可的、首个推出期货合约的、专注于撮合大宗交易的数字货币交易所。Gemini 位于纽约,是一家数字货币交易所和托管机构,允许客户交易和存储数字资产,并直接受纽约州金融服务部门(NYDFS)的监管。
|
||||
|
||||
Gemini 的界面清晰,API 完整而易用,更重要的是,还提供了完整的测试网络,也就是说,功能和正常的 Gemini 完全一样。但是他家的交易采用虚拟币,非常方便从业者在平台上进行对接测试。
|
||||
|
||||
另一个做得很好的交易所,是 Bitmex,他家的 API UI 界面和测试网络也是币圈一流。不过,鉴于这家是期货交易所,对于量化初学者来说有一定的门槛,我们还是选择 Gemini 更方便一些。
|
||||
|
||||
在进入正题之前,我们最后再以比特币和美元之间的交易为例,介绍四个基本概念(orderbook 的概念这里就不介绍了,你也不用深究,你只需要知道比特币的价格是什么就行了)。
|
||||
|
||||
|
||||
买(buy):用美元买入比特币的行为。
|
||||
卖(sell):用比特币换取美元的行为。
|
||||
市价单(market order):给交易所一个方向(买或者卖)和一个数量,交易所把给定数量的美元(或者比特币)换成比特币(或者美元)的单子。
|
||||
限价单(limit order):给交易所一个价格、一个方向(买或者卖)和一个数量,交易所在价格达到给定价格的时候,把给定数量的美元(或者比特币)换成比特币(或者美元)的单子。
|
||||
|
||||
|
||||
这几个概念都不难懂。其中,市价单和限价单,最大的区别在于,限价单多了一个给定价格。如何理解这一点呢?我们可以来看下面这个例子。
|
||||
|
||||
小明在某一天中午12:00:00,告诉交易所,我要用1000美元买比特币。交易所收到消息,在 12:00:01 回复小明,现在你的账户多了 0.099 个比特币,少了 1000 美元,交易成功。这是一个市价买单。
|
||||
|
||||
而小强在某一天中午 11:59:00,告诉交易所,我要挂一个单子,数量为 0.1 比特币,1个比特币的价格为 10000 美元,低于这个价格不卖。交易所收到消息,在11:59:01 告诉小强,挂单成功,你的账户余额中 0.1 比特币的资金被冻结。又过了一分钟,交易所告诉小强,你的单子被完全执行了(fully executed),现在你的账户多了 1000 美元,少了 0.1 个比特币。这就是一个限价卖单。
|
||||
|
||||
(这里肯定有人发现不对了:貌似少了一部分比特币,到底去哪儿了呢?嘿嘿,你不妨自己猜猜看。)
|
||||
|
||||
显然,市价单,在交给交易所后,会立刻得到执行,当然执行价格也并不受你的控制。它很快,但是也非常不安全。而限价单,则限定了交易价格和数量,安全性相对高很多。缺点呢,自然就是如果市场朝相反方向走,你挂的单子可能没有任何人去接,也就变成了干吆喝却没人买。因为我没有讲解 orderbook,所以这里的说辞不完全严谨,但是对于初学者理解今天的内容,已经够用了。
|
||||
|
||||
储备了这么久的基础知识,想必你已经跃跃欲试了吧?下面,我们正式进入正题,手把手教你使用API下单。
|
||||
|
||||
手把手教你使用 API 下单
|
||||
|
||||
手动挂单显然太慢,也不符合量化交易的初衷。我们就来看看如何用代码实现自动化下单吧。
|
||||
|
||||
第一步,你需要做的是,注册一个 Gemini Sandbox 账号。请放心,这个测试账号不需要你充值任何金额,注册后即送大量虚拟现金。这口吻是不是听着特像网游宣传语,接下来就是“快来贪玩蓝月里找我吧”?哈哈,不过这个设定确实如此,所以赶紧来注册一个吧。
|
||||
|
||||
注册后,为了满足好奇,你可以先尝试着使用 Web 界面自行下单。不过,事实上,未解锁的情况下是无法正常下单的,因此这样尝试并没啥太大意义。
|
||||
|
||||
所以第二步,我们需要来配置 API Key。菜单栏User Settings->API Settings,然后点 GENERATE A NEW ACCOUNT API KEY,记下 Key 和 Secret 这两串字符。因为窗口一旦消失,这两个信息就再也找不到了,需要你重新生成。
|
||||
|
||||
配置到此结束。接下来,我们来看具体实现。
|
||||
|
||||
先强调一点,在量化系统开发的时候,你的心中一定要有清晰的数据流图。下单逻辑是一个很简单的 RESTful 的过程,和你在网页操作的一样,构造你的请求订单、加密请求,然后 POST 给 gemini 交易所即可。
|
||||
|
||||
不过,因为涉及到的知识点较多,带你一步一步从零来写代码显然不太现实。所以,我们采用“先读懂后记忆并使用”的方法来学,下面即为这段代码:
|
||||
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
import datetime
|
||||
import time
|
||||
|
||||
base_url = "https://api.sandbox.gemini.com"
|
||||
endpoint = "/v1/order/new"
|
||||
url = base_url + endpoint
|
||||
|
||||
gemini_api_key = "account-zmidXEwP72yLSSybXVvn"
|
||||
gemini_api_secret = "375b97HfE7E4tL8YaP3SJ239Pky9".encode()
|
||||
|
||||
t = datetime.datetime.now()
|
||||
payload_nonce = str(int(time.mktime(t.timetuple())*1000))
|
||||
|
||||
payload = {
|
||||
"request": "/v1/order/new",
|
||||
"nonce": payload_nonce,
|
||||
"symbol": "btcusd",
|
||||
"amount": "5",
|
||||
"price": "3633.00",
|
||||
"side": "buy",
|
||||
"type": "exchange limit",
|
||||
"options": ["maker-or-cancel"]
|
||||
}
|
||||
|
||||
encoded_payload = json.dumps(payload).encode()
|
||||
b64 = base64.b64encode(encoded_payload)
|
||||
signature = hmac.new(gemini_api_secret, b64, hashlib.sha384).hexdigest()
|
||||
|
||||
request_headers = {
|
||||
'Content-Type': "text/plain",
|
||||
'Content-Length': "0",
|
||||
'X-GEMINI-APIKEY': gemini_api_key,
|
||||
'X-GEMINI-PAYLOAD': b64,
|
||||
'X-GEMINI-SIGNATURE': signature,
|
||||
'Cache-Control': "no-cache"
|
||||
}
|
||||
|
||||
response = requests.post(url,
|
||||
data=None,
|
||||
headers=request_headers)
|
||||
|
||||
new_order = response.json()
|
||||
print(new_order)
|
||||
|
||||
|
||||
########## 输出 ##########
|
||||
|
||||
{'order_id': '239088767', 'id': '239088767', 'symbol': 'btcusd', 'exchange': 'gemini', 'avg_execution_price': '0.00', 'side': 'buy', 'type': 'exchange limit', 'timestamp': '1561956976', 'timestampms': 1561956976535, 'is_live': True, 'is_cancelled': False, 'is_hidden': False, 'was_forced': False, 'executed_amount': '0', 'remaining_amount': '5', 'options': ['maker-or-cancel'], 'price': '3633.00', 'original_amount': '5'}
|
||||
|
||||
|
||||
我们来深入看一下这段代码。
|
||||
|
||||
RESTful 的 POST 请求,通过 requests.post 来实现。post 接受三个参数,url、data 和 headers。
|
||||
|
||||
这里的 url 等价于 https://api.sandbox.gemini.com/v1/order/new,但是在代码中分两部分写。第一部分是交易所 API 地址;第二部分,以斜杠开头,用来表示统一的 API endpoint。我们也可以在其他交易所的 API 中看到类似的写法,两者连接在一起,就构成了最终的 url。
|
||||
|
||||
而接下来大段命令的目的,是为了构造 request_headers。
|
||||
|
||||
这里我简单说一下 HTTP request,这是互联网中基于 TCP 的基础协议。HTTP 协议是 Hyper Text Transfer Protocol(超文本传输协议)的缩写,用于从万维网(WWW:World Wide Web)服务器传输超文本到本地浏览器的传送协议。而 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?为什么?欢迎留言写下你的思考,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,334 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
35 RESTful & Socket:行情数据对接和抓取
|
||||
你好,我是景霄。
|
||||
|
||||
上一节课,我们介绍了交易所的交易模式,数字货币交易所RESTful接口的常见概念,以及如何调用RESTful接口进行订单操作。众所周知,买卖操作的前提,是你需要已知市场的最新情况。这节课里,我将介绍交易系统底层另一个最重要的部分,行情数据的对接和抓取。
|
||||
|
||||
行情数据,最重要的是实时性和有效性。市场的情况瞬息万变,合适的买卖时间窗口可能只有几秒。在高频交易里,合适的买卖机会甚至在毫秒级别。要知道,一次从北京发往美国的网络请求,即使是光速传播,都需要几百毫秒的延迟。更别提用Python这种解释型语言,建立HTTP连接导致的时间消耗。
|
||||
|
||||
经过上节课的学习,你对交易应该有了基本的了解,这也是我们今天学习的基础。接下来,我们先从交易所撮合模式讲起,然后介绍行情数据有哪些;之后,我将带你基于Websocket的行情数据来抓取模块。
|
||||
|
||||
行情数据
|
||||
|
||||
回顾上一节我们提到的,交易所是一个买方、卖方之间的公开撮合平台。买卖方把需要/可提供的商品数量和愿意出/接受的价格提交给交易所,交易所按照公平原则进行撮合交易。
|
||||
|
||||
那么撮合交易是怎么进行的呢?假设你是一个人肉比特币交易所,大量的交易订单往你这里汇总,你应该如何选择才能让交易公平呢?
|
||||
|
||||
显然,最直观的操作就是,把买卖订单分成两个表,按照价格由高到低排列。下面的图,就是买入和卖出的委托表。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
如果最高的买入价格小于最低的卖出价格,那就不会有任何交易发生。这通常是你看到的委托列表的常态。
|
||||
|
||||
如果最高的买入价格和最低的卖出价格相同,那么就尝试进行撮合。比如BTC在9002.01就会发生撮合,最后按照9002.01的价格,成交0.0330个BTC。当然,交易完成后,小林未完成部分的订单(余下0.1126 - 0.0330 = 0.0796 个 BTC 未卖出),还会继续在委托表里。
|
||||
|
||||
不过你可能会想,如果买入和卖出的价格有交叉,那么成交价格又是什么呢?事实上,这种情况并不会发生。我们来试想一下下面这样的场景。
|
||||
|
||||
如果你尝试给一个委托列表里加入一个新买入订单,它的价格比所有已有的最高买入价格高,也比所有的卖出价格高。那么此时,它会直接从最低的卖出价格撮合。等到最低价格的卖出订单吃完了,它便开始吃价格第二低的卖出订单,直到这个买入订单完全成交。反之亦然。所以,委托列表价格不会出现交叉。
|
||||
|
||||
当然,请注意,这里我说的只是限价订单的交易方式。而对于市价订单,交易规则会有一些轻微的区别,这里我就不详细解释了,主要是让你有个概念。
|
||||
|
||||
其实说到这里,所谓的“交易所行情”概念就呼之欲出了。交易所主要有两种行情数据:委托账本(Order Book)和活动行情(Tick data)。
|
||||
|
||||
我们把委托表里的具体用户隐去,相同价格的订单合并,就得到了下面这种委托账本。我们主要观察右边的数字部分,其中:
|
||||
|
||||
|
||||
上半部分里,第一列红色数字代表BTC的卖出价格,中间一列数字是这个价格区间的订单BTC总量,最右边一栏是从最低卖出价格到当前价格区间的积累订单量。
|
||||
中间的大字部分,9994.10 USD是当前的市场价格,也就是上一次成交交易的价格。
|
||||
下面绿色部分的含义与上半部分类似,不过指的是买入委托和对应的数量。
|
||||
|
||||
|
||||
|
||||
|
||||
Gemini的委托账本,来自https://cryptowat.ch
|
||||
|
||||
这张图中,最低的卖出价格比最高的买入价格要高 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一样,每发送一个请求,要等待服务器完成请求、完全回复之后,再进行下一个请求。换句话说,我们在请求的同时也在接受消息,这也就是前面所说的”全双工“。
|
||||
|
||||
|
||||
|
||||
REST(HTTP)单工请求响应的示意图
|
||||
|
||||
|
||||
|
||||
Websocket全双工请求响应的示意图
|
||||
|
||||
再来看第二段代码。为了解释”双向“,我们来看看获取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 爬虫又会发生什么?这一点应该如何避免呢?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,574 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
36 Pandas & Numpy:策略与回测系统
|
||||
大家好,我是景霄。
|
||||
|
||||
上节课,我们介绍了交易所的数据抓取,特别是orderbook和tick数据的抓取。今天这节课,我们考虑的是,怎么在这些历史数据上测试一个交易策略。
|
||||
|
||||
首先我们要明确,对于很多策略来说,我们上节课抓取的密集的orderbook和tick数据,并不能简单地直接使用。因为数据量太密集,包含了太多细节;而且长时间连接时,网络随机出现的不稳定,会导致丢失部分tick数据。因此,我们还需要进行合适的清洗、聚合等操作。
|
||||
|
||||
此外,为了进行回测,我们需要一个交易策略,还需要一个测试框架。目前已存在很多成熟的回测框架,但是为了Python学习,我决定带你搭建一个简单的回测框架,并且从中简单一窥Pandas的优势。
|
||||
|
||||
OHLCV数据
|
||||
|
||||
了解过一些股票交易的同学,可能知道K线这种东西。K线又称“蜡烛线”,是一种反映价格走势的图线。它的特色在于,一个线段内记录了多项讯息,相当易读易懂且实用有效,因此被广泛用于股票、期货、贵金属、数字货币等行情的技术分析。下面便是一个K线示意图。
|
||||
|
||||
|
||||
|
||||
K线示意图
|
||||
|
||||
其中,每一个小蜡烛,都代表着当天的开盘价(Open)、最高价(High)、最低价(Low)和收盘价(Close),也就是我画的第二张图表示的这样。
|
||||
|
||||
|
||||
|
||||
K线的“小蜡烛” – OHLC
|
||||
|
||||
类似的,除了日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数据,作为策略和回测的输入。你可以在这里下载数据。
|
||||
|
||||
数据下载完成后,我们可以利用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学习者来说,第一类也就是向量型回测框架,才是最适合我们练手的项目了。那么,我们就开始吧。
|
||||
|
||||
首先,我先为你梳理下回测流程,也就是下面五步:
|
||||
|
||||
|
||||
读取OHLC数据;
|
||||
对OHLC进行指标运算;
|
||||
策略根据指标向量决定买卖;
|
||||
发给模拟的”交易所“进行交易;
|
||||
最后,统计结果。
|
||||
|
||||
|
||||
对此,使用之前学到的面向对象思维方式,我们可以大致抽取三个类:
|
||||
|
||||
|
||||
交易所类( 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平滑之后,就会得到下面这张图的效果。
|
||||
|
||||
|
||||
|
||||
某个投资品价格的SMA,窗口大小为50
|
||||
|
||||
你可以看出,如果窗口大小越大,那么SMA应该越平滑,变化越慢;反之,如果SMA比较小,那么短期的变化也会越快地反映在SMA上。
|
||||
|
||||
于是,我们想到,能不能对投资品的价格设置两个指标呢?这俩指标,一个是小窗口的SMA,一个是大窗口的SMA。
|
||||
|
||||
|
||||
如果小窗口的SMA曲线从下面刺破或者穿过大窗口SMA,那么说明,这个投资品的价格在短期内快速上涨,同时这个趋势很强烈,可能是一个买入的信号;
|
||||
反之,如果大窗口的SMA从下方突破小窗口SMA,那么说明,投资品的价格在短期内快速下跌,我们应该考虑卖出。
|
||||
|
||||
|
||||
下面这幅图,就展示了这两种情况。
|
||||
|
||||
|
||||
|
||||
明白了这里的概念和原理后,接下来的操作就不难了。利用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数据吗?欢迎在留言区写下你的答案和问题,也欢迎你把这篇文章分享出去。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,243 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
37 Kafka & ZMQ:自动化交易流水线
|
||||
你好,我是景霄。
|
||||
|
||||
在进行这节课的学习前,我们先来回顾一下,前面三节课,我们学了些什么。
|
||||
|
||||
第 34 讲,我们介绍了如何通过 RESTful API 在交易所下单;第 35 讲,我们讲解了如何通过 Websocket ,来获取交易所的 orderbook 数据;第 36 讲,我们介绍了如何实现一个策略,以及如何对策略进行历史回测。
|
||||
|
||||
事实上,到这里,一个简单的、可以运作的量化交易系统已经成型了。你可以对策略进行反复修改,期待能得到不错的 PnL。但是,对于一个完善的量化交易系统来说,只有基本骨架还是不够的。
|
||||
|
||||
在大型量化交易公司,系统一般是分布式运行的,各个模块独立在不同的机器上,然后互相连接来实现。即使是个人的交易系统,在进行诸如高频套利等算法时,也需要将执行层布置在靠近交易所的机器节点上。
|
||||
|
||||
所以,从今天这节课开始,我们继续回到 Python 的技术栈,从量化交易系统这个角度切入,为你讲解如何实现分布式系统之间的复杂协作。
|
||||
|
||||
中间件
|
||||
|
||||
我们先来介绍一下中间件这个概念。中间件,是将技术底层工具和应用层进行连接的组件。它要实现的效果则是,让我们这些需要利用服务的工程师,不必去关心底层的具体实现。我们只需要拿着中间件的接口来用就好了。
|
||||
|
||||
这个概念听起来并不难理解,我们再举个例子让你彻底明白。比如拿数据库来说,底层数据库有很多很多种,从关系型数据库 MySQL 到非关系型数据库 NoSQL,从分布式数据库 Spanner 到内存数据库 Redis,不同的数据库有不同的使用场景,也有着不同的优缺点,更有着不同的调用方式。那么中间件起什么作用呢?
|
||||
|
||||
中间件,等于在这些不同的数据库上加了一层逻辑,这一层逻辑专门用来和数据库打交道,而对外只需要暴露同一个接口即可。这样一来,上层的程序员调用中间件接口时,只需要让中间件指定好数据库即可,其他参数完全一致,极大地方便了上层的开发;同时,下层技术栈在更新换代的时候,也可以做到和上层完全分离,不影响程序员的使用。
|
||||
|
||||
它们之间的逻辑关系,你可以参照下面我画的这张图。我习惯性把中间件的作用调侃为:没有什么事情是加一层解决不了的;如果有,那就加两层。
|
||||
|
||||
|
||||
|
||||
当然,这只是其中一个例子,也只是中间件的一种形式。事实上,比如在阿里,中间件主要有分布式关系型数据库 DRDS、消息队列和分布式服务这么三种形式。而我们今天,主要会用到消息队列,因为它非常符合量化交易系统的应用场景,即事件驱动模型。
|
||||
|
||||
消息队列
|
||||
|
||||
那么,什么是消息队列呢?一如其名,消息,即互联网信息传递的个体;而队列,学过算法和数据结构的你,应该很清楚这个 FIFO(先进先出)的数据结构吧。(如果算法基础不太牢,建议你可以学习极客时间平台上王争老师的“数据结构与算法之美”专栏,第 09讲即为队列知识)
|
||||
|
||||
简而言之,消息队列就是一个临时存放消息的容器,有人向消息队列中推送消息;有人则监听消息队列,发现新消息就会取走。根据我们刚刚对中间件的解释,清晰可见,消息队列也是一种中间件。
|
||||
|
||||
目前,市面上使用较多的消息队列有 RabbitMQ、Kafka、RocketMQ、ZMQ 等。不过今天,我只介绍最常用的 ZMQ 和 Kafka。
|
||||
|
||||
我们先来想想,消息队列作为中间件有什么特点呢?
|
||||
|
||||
首先是严格的时序性。刚刚说了,队列是一种先进先出的数据结构,你丢给它 1, 2, 3,然后另一个人从里面取数据,那么取出来的一定也是 1, 2, 3,严格保证了先进去的数据先出去,后进去的数据后出去。显然,这也是消息机制中必须要保证的一点,不然颠三倒四的结果一定不是我们想要的。
|
||||
|
||||
说到队列的特点,简单提一句,与“先进先出“相对的是栈这种数据结构,它是先进后出的,你丢给它 1, 2, 3,再从里面取出来的时候,拿到的就是3, 2, 1了,这一点一定要区分清楚。
|
||||
|
||||
其次,是分布式网络系统的老生常谈问题。如何保证消息不丢失?如何保证消息不重复?这一切,消息队列在设计的时候都已经考虑好了,你只需要拿来用就可以,不必过多深究。
|
||||
|
||||
不过,很重要的一点,消息队列是如何降低系统复杂度,起到中间件的解耦作用呢?我们来看下面这张图。
|
||||
|
||||
|
||||
|
||||
消息队列的模式是发布和订阅,一个或多个消息发布者可以发布消息,一个或多个消息接受者可以订阅消息。 从图中你可以看到,消息发布者和消息接受者之间没有直接耦合,其中,
|
||||
|
||||
|
||||
消息发布者将消息发送到分布式消息队列后,就结束了对消息的处理;
|
||||
消息接受者从分布式消息队列获取该消息后,即可进行后续处理,并不需要探寻这个消息从何而来。
|
||||
|
||||
|
||||
至于新增业务的问题,只要你对这类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,所以也就实现了业务的可扩展性设计。
|
||||
|
||||
讲了这么多概念层的东西,想必你迫不及待地想看具体代码了吧。接下来,我们来看一下 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核心技术与实战”,这个专栏里,胡夕老师用详实的篇幅,讲解了 Kafka 的实战和内核,你可以加以学习和使用。
|
||||
|
||||
|
||||
|
||||
来自极客时间专栏“Kafka核心技术与实战”
|
||||
|
||||
基于消息队列的 Orderbook 数据流
|
||||
|
||||
最后回到我们的量化交易系统上。
|
||||
|
||||
量化交易系统中,获取 orderbook 一般有两种用途:策略端获取实时数据,用来做决策;备份在文件或者数据库中,方便让策略和回测系统将来使用。
|
||||
|
||||
如果我们直接单机监听交易所的消息,风险将会变得很大,这在分布式系统中叫做 Single Point Failure。一旦这台机器出了故障,或者网络连接突然中断,我们的交易系统将立刻暴露于风险中。
|
||||
|
||||
于是,一个很自然的想法就是,我们可以在不同地区放置不同的机器,使用不同的网络同时连接到交易所,然后将这些机器收集到的信息汇总、去重,最后生成我们需要的准确数据。相应的拓扑图如下:
|
||||
|
||||
|
||||
|
||||
当然,这种做法也有很明显的缺点:因为要同时等待多个数据服务器的数据,再加上消息队列的潜在处理延迟和网络延迟,对策略服务器而言,可能要增加几十到数百毫秒的延迟。如果是一些高频或者滑点要求比较高的策略,这种做法需要谨慎考虑。
|
||||
|
||||
但是,对于低频策略、波段策略,这种延迟换来的整个系统的稳定性和架构的解耦性,还是非常值得的。不过,你仍然需要注意,这种情况下,消息队列服务器有可能成为瓶颈,也就是刚刚所说的Single Point Failure,一旦此处断开,依然会将系统置于风险之中。
|
||||
|
||||
事实上,我们可以使用一些很成熟的系统,例如阿里的消息队列,AWS 的 Simple Queue Service 等等,使用这些非常成熟的消息队列系统,风险也将会最小化。
|
||||
|
||||
总结
|
||||
|
||||
这节课,我们分析了现代化软件工程领域中的中间件系统,以及其中的主要应用——消息队列。我们讲解了最基础的消息队列的模式,包括点对点模型、发布者订阅者模型,和一些其他消息队列自己支持的模型。
|
||||
|
||||
在真实的项目设计中,我们要根据自己的产品需求,来选择使用不同的模型;同时也要在编程实践中,加深对不同技能点的了解,对系统复杂性进行解耦,这才是设计出高质量系统的必经之路。
|
||||
|
||||
思考题
|
||||
|
||||
今天的思考题,文中我也提到过,这里再专门列出强调一下。在ZMQ 那里,我提出了两个问题:
|
||||
|
||||
|
||||
如果你试着把发布者的 time.sleep(1) 放在 while 循环的最后,会发生什么?为什么?
|
||||
如果有多个发布者,ZMQ 应该怎么做呢?
|
||||
|
||||
|
||||
欢迎留言写下你的思考和疑惑,也欢迎你把这篇文章分享给更多的人一起学习。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,81 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
43 Q&A:聊一聊职业发展和选择
|
||||
你好,我是景霄。
|
||||
|
||||
在前面几节课中,我分享了在FB工作的一些经验和感想,不少同学都提出了自己的困惑,也希望我能给出一些职业发展方面的建议。综合这些问题,我主要选取了下面三个主题,来说说职业发展、职业选择方面我的看法。
|
||||
|
||||
Q:程序员的岗位主要有哪些类型?我该如何选择?
|
||||
|
||||
A:无论是在求职阶段,还是正式进入公司工作后,你都会发现,工程师普遍按技术的不同,分为下面几个岗位。
|
||||
|
||||
|
||||
前端:包括移动(Android、iOS)以及Web前端(JavaScript、CSS)开发。
|
||||
后端(服务器端):主要是服务器端的开发,简单来说,就是输入为请求,输出为响应,发送给客户端。
|
||||
算法:主要涉及到的是机器学习,比如推荐系统如何更好地实现个性化推荐,搜索引擎返回的结果如何才能更符合地用户的需求等等。
|
||||
架构:涉及系统架构,偏底层,语言以C++为主。
|
||||
|
||||
|
||||
从薪酬的角度来看,普遍来说:算法 > 架构 > 后端 > 前端。当然,这主要是由市场的供需关系决定的。
|
||||
|
||||
就拿算法岗来说,国内市场普遍缺少算法人才,也是因为这个岗位的培养难度更大,需要投入更大的精力。在顶尖互联网公司,参与核心产品研发的算法工程师们,工作三年,年收入100-200W人民币是很常见的。
|
||||
|
||||
不过,我这里所说的算法人才,绝不是指类似在校生那种,看过几篇论文,写过一些MATLAB,在学校做过几个科研项目的程度。算法工作岗位需要的算法能力,是你必须身体力行,有某些产品线的实践经历。还需要你真正了解市场,比如今日头条的推荐算法是怎样的,Google搜索引擎是怎么工作的,头条里的广告排序又是怎么做的等等。
|
||||
|
||||
再来说说架构,这也是目前一个热门的方向。我一直认为这是一个很偏工程、很硬核的领域,发展前景也相当不错,可以说是一个产品的基石。就拿刚刚提到的推荐系统来说,广告的定位和排序系统背后,都需要强有力的架构支撑。因此,这一行也可以称得上是人才紧缺,是企业舍得花高薪聘请的对象之一。
|
||||
|
||||
与算法不同的是,这个领域不会涉及很深的数学知识,工程师的主要关注点,在于如何提高系统性能,包括如何使系统高扩展、减小系统的延迟和所需CPU的容量等等。架构师需要很强的编程能力,常用的语言是C++;当然,最重要的还是不断积累大型项目中获得的第一手经验,对常见的问题有最principle的处理方式。
|
||||
|
||||
最后说说后端和前端,这是绝大多数程序员从事的岗位,也是我刚进公司时的选择。也许比起前两个岗位,不少人会认为,后端、前端工程师的薪酬较低,没有什么发展前景。这其实大错特错了!从一个产品的角度出发,你可以没有算法工程师、没有架构师,但是你能缺少后端和前端的开发人员吗?显然是不可能的。
|
||||
|
||||
后端和前端,相当于是一个产品的框架。框架搭好了,才会有机器学习、算法等的锦上添花。诚然,这两年来看,后端和前端没有前两者那么热门(还是市场供需关系的问题),但这并不代表,这些岗位没有发展前景,或者你就可以小看其技术含量。
|
||||
|
||||
比起算法和架构,后端、前端确实门槛更低些,但是其工作依然存在很高的技术含量。比如对一个产品或者其中的某些部件来说,如何设计搭建前后端的开发框架结构,使系统更加合理、可维护性更高,就是很多资深的开发工程师正在做的事。
|
||||
|
||||
前面聊了这么多,最后回到最根本的问题上:到底如何选择呢?
|
||||
|
||||
这里我给出的建议是:首先以自己的兴趣为出发点,因为只有自己感兴趣的东西,你才能做到最好。比如,一些人就是对前端感兴趣,那么为啥偏要去趟机器学习这趟浑水呢?当然不少人可能没有明确的偏好,那么这种情况下,我建议你尽可能多地去尝试,这是了解自己兴趣最好的方法。
|
||||
|
||||
另外,从广义的角度来看,计算机这门技术存在着study deep和study broad这两个方向,你得想清楚你属于哪类。所谓的study deep,就意味着数十年专攻一个领域,励志成为某个领域的专家;而study broad,便是类似于全栈工程师,对一个产品、系统的end to end都有一个了解,能够随时胜任任意角色的工作,这一点在初创公司身上体现得最为明显。
|
||||
|
||||
Q:如何成为一个全栈工程师?
|
||||
|
||||
A:相信屏幕前的不少同学是在创业公司工作的,刚刚也提到了,创业公司里全栈工程师的需求尤为突出。那么,如何成为一个优秀的全栈工程师呢?
|
||||
|
||||
简单来说,最好的方法就是“尽可能地多接触、多实践不同领域的项目”。身体力行永远是学习新知识、提高能力的最好办法。
|
||||
|
||||
当然,在每个领域的初始阶段,你可能会感觉到异常艰难,比如从未接触过前端的人被要求写一个页面,一时间内显然会不知从何下手。这个时候,我建议你可以先从“依葫芦画瓢”开始,通过阅读别人相似的代码,并在此基础上加以修改,完成你要实现的功能。时间久了,你看的多了,用的多了,理解自然就越来越深,动起手来也就越来越熟练了。
|
||||
|
||||
有条件的同学,比如工作在类似于FB这种文化的公司,可以通过在公司内部换组的方式,去接触不同的项目。这自然是最好不过了,因为和特定领域的人合作,永远比一个人单干强得多,你能够迅速学到更多的东西。
|
||||
|
||||
不过,没这种条件的同学也不必绝望,你还可以利用业余时间“充电“,自己做一些项目来培养和加强别的领域的能力。毕竟,对于成年人来说,自学才是精进自己的主要方式。
|
||||
|
||||
这样,到了最后,你应该达到的结果便是,自己一个人能够扛起整条产品线的开发,也对系统的整个工作流程有一个全面而深入的理解。
|
||||
|
||||
Q:学完本专栏后,在Python领域我该如何继续进阶呢?
|
||||
|
||||
A:在我看来,这个专栏的主要目的,是带你掌握Python这门语言的常见基本和高阶用法。接下来的进阶,便是Python本身在各种不同方向的运用,拿后端开发这个方向来说,比如,如何搭建大型系统的后台便是你需要掌握的。一个好的后端,自然离不开:
|
||||
|
||||
|
||||
合理的系统、框架设计;
|
||||
简约高效的代码质量;
|
||||
稳健齐全的单元测试;
|
||||
出色的性能表现。
|
||||
|
||||
|
||||
具体来说,你搭建的系统后端是不是易于拓展呢?比如过半年后,有了新的产品需求,需要增加新的功能。那么,在你的框架下,是否可以尽可能少地改动来实现新的功能,而不需要把某部分推倒重来呢?
|
||||
|
||||
再比如,你搭建的系统是不是符合可维护性高、可靠性高、单元测试齐全的要求,从而不容易在线上发生bug呢?
|
||||
|
||||
总之,在某一领域到了进阶的阶段,你需要关注的,绝不仅仅只是某些功能的实现,更需要你考虑所写代码的性能、质量,甚至于整个系统的设计等等。
|
||||
|
||||
虽然讲了这么多东西,但最后我想说的是,三百六十行,行行出状元。对于计算机行业,乃至整个职场来说,每一个领域都没有优劣之分,每个领域你都可以做得很牛逼,前提是你不懈地学习、实践和思考。
|
||||
|
||||
那么,对于职业选择和发展,你又是如何看待和理解的呢?欢迎留言和我一起交流探讨,也希望屏幕前的一直不懈学习的你,能找到属于自己的方向,不断前进和创新,实现自己的人生理想。
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user