first commit

This commit is contained in:
张乾
2024-10-16 13:06:13 +08:00
parent 2393162ba9
commit c47809d1ff
41 changed files with 9189 additions and 0 deletions

View File

@ -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或是其他相似的工具吗
欢迎在下方留言与我讨论,也欢迎你把这篇文章分享出去。我们一起交流,一起进步。

View File

@ -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服务器传输超文本到本地浏览器的传送协议。而 TCPTransmission Control Protocol则是面向连接的、可靠的、基于字节流的传输层通信协议。
多提一句,如果你开发网络程序,建议利用闲暇时间认真读一读《计算机网络:自顶向下方法》这本书,它也是国内外计算机专业必修课中广泛采用的课本之一。一边学习,一边应用,对于初学者的能力提升是全面而充分的。
回到 HTTP它的主要特点是连接简单、灵活可以使用“简单请求收到回复然后断开连接”的方式也是一种无状态的协议因此充分符合 RESTful 的思想。
HTTP 发送需要一个请求头request header也就是代码中的 request_headers用 Python 的语言表示,就是一个 str 对 str 的字典。
这个字典里,有一些字段有特殊用途, 'Content-Type': "text/plain" 和 'Content-Length': "0" 描述 Content 的类型和长度,这里的 Content 对应于参数 data。但是 Gemini 这里的 request 的 data 没有任何用处,因此长度为 0。
还有一些其他字段,例如 'keep-alive' 来表示连接是否可持续化等,你也可以适当注意一下。要知道,网络编程很多 bug 都会出现在不起眼的细节之处。
继续往下走看代码。payload 是一个很重要的字典,它用来存储下单操作需要的所有的信息,也就是业务逻辑信息。这里我们可以下一个 limit buy限价买单价格为 3633 刀。
另外,请注意 nonce这是个很关键并且在网络通信中很常见的字段。
因为网络通信是不可靠的,一个信息包有可能会丢失,也有可能重复发送,在金融操作中,这两者都会造成很严重的后果。丢包的话,我们重新发送就行了;但是重复的包,我们需要去重。虽然 TCP 在某种程度上可以保证但为了在应用层面进一步减少错误发生的机会Gemini 交易所要求所有的通信 payload 必须带有 nonce。
nonce 是个单调递增的整数。当某个后来的请求的 nonce比上一个成功收到的请求的 nouce 小或者相等的时候Gemini 便会拒绝这次请求。这样一来,重复的包就不会被执行两次了。另一方面,这样也可以在一定程度上防止中间人攻击:
一则是因为 nonce 的加入,使得加密后的同样订单的加密文本完全混乱;
二则是因为,这会使得中间人无法通过“发送同样的包来构造重复订单”进行攻击。
这样的设计思路是不是很巧妙呢?这就相当于每个包都增加了一个身份识别,可以极大地提高安全性。希望你也可以多注意,多思考一下这些巧妙的用法。
接下来的代码就很清晰了。我们要对 payload 进行 base64 和 sha384 算法非对称加密,其中 gemini_api_secret 为私钥;而交易所存储着公钥,可以对你发送的请求进行解密。最后,代码再将加密后的请求封装到 request_headers 中,发送给交易所,并收到 response这个订单就完成了。
总结
这节课我们介绍了什么是 RESTful API带你了解了交易所的 RESTful API 是如何工作的,以及如何通过 RESTful API 来下单。同时,我简单讲述了网络编程中的一些技巧操作,希望你在网络编程中要注意思考每一个细节,尽可能在写代码之前,对业务逻辑和具体的技术细节有足够清晰的认识。
下一节,我们同样将从 Web Socket 的定义开始,讲解量化交易中数据模块的具体实现。
思考题
最后留一个思考题。今天的内容里,能不能使用 timestamp 代替 nonce为什么欢迎留言写下你的思考也欢迎你把这篇文章分享出去。

View File

@ -0,0 +1,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一样每发送一个请求要等待服务器完成请求、完全回复之后再进行下一个请求。换句话说我们在请求的同时也在接受消息这也就是前面所说的”全双工“。
RESTHTTP单工请求响应的示意图
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 爬虫又会发生什么?这一点应该如何避免呢?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。

View File

@ -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数据吗欢迎在留言区写下你的答案和问题也欢迎你把这篇文章分享出去

View File

@ -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 发消息给 serverserver 处理后返回给 client完成一次交互。这个场景你一定很熟悉吧没错和 HTTP 模式非常像,所以这里我就不重点介绍了。至于第三种模式,与今天内容无关,这里我也不做深入讲解。
我们需要详细来看的是第二种即“PubSub”模式。下面是它的具体实现代码很清晰你应该很容易理解
# 订阅者 1
import zmq
def run():
context = zmq.Context()
socket = context.socket(zmq.SUB)
socket.connect('tcp://127.0.0.1:6666')
socket.setsockopt_string(zmq.SUBSCRIBE, '')
print('client 1')
while True:
msg = socket.recv()
print("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 应该怎么做呢?
欢迎留言写下你的思考和疑惑,也欢迎你把这篇文章分享给更多的人一起学习。

View File

@ -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呢
总之,在某一领域到了进阶的阶段,你需要关注的,绝不仅仅只是某些功能的实现,更需要你考虑所写代码的性能、质量,甚至于整个系统的设计等等。
虽然讲了这么多东西,但最后我想说的是,三百六十行,行行出状元。对于计算机行业,乃至整个职场来说,每一个领域都没有优劣之分,每个领域你都可以做得很牛逼,前提是你不懈地学习、实践和思考。
那么,对于职业选择和发展,你又是如何看待和理解的呢?欢迎留言和我一起交流探讨,也希望屏幕前的一直不懈学习的你,能找到属于自己的方向,不断前进和创新,实现自己的人生理想。