极客时间已完结课程限时免费阅读

35 | RESTful & Socket:行情数据对接和抓取

35 | RESTful & Socket:行情数据对接和抓取-极客时间

35 | RESTful & Socket:行情数据对接和抓取

讲述:冯永吉

时长13:39大小12.50M

你好,我是景霄。
上一节课,我们介绍了交易所的交易模式,数字货币交易所 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 爬虫又会发生什么?这一点应该如何避免呢?欢迎留言和我讨论,也欢迎你把这篇文章分享出去。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 16

提建议

上一篇
34 | RESTful & Socket:搭建交易执行层核心
下一篇
36 | Pandas & Numpy:策略与回测系统
unpreview
 写留言

精选留言(32)

  • Jingxiao
    置顶
    2019-08-01
    思考题答案: Websocket 可能丢包。TCP 协议保证了所有的包按顺序抵达(即使是乱序抵达,在前面的包收到之前,TCP 协议下的底层程序也会讲先到达的靠后的包缓存,直到前面的包抵达,才送给上层的应用程序),但是并不能保证不可恢复的错误发生的时候,包不会丢失。这种情况发生的时候,就会出现 Orderbook 中一个或多个(价格,数量)信息没得到及时更新。这种错误越积攒越多的情况下,就会导致本地的 Orderbook 充满垃圾信息,变得完全不可靠。因此一个很好的做法是,可以设置一个时间间隔,通过 RESTFul 或者其他方式重新抓一下 Orderbook 的 Snapshot,然后和本地的 Orderbook 进行比对,纠正错误。
    展开
    50
  • tt
    2019-07-29
    websocket包的安装使用如下命令: pip -m install websocket_client安装。 pip -m install websocket会安装另外一个完全不同的包
    共 1 条评论
    44
  • SCAR
    2019-07-29
    思考题: 1. websocket基于tcp的,虽然协议上有纠错,重传和等待的机制,但一些特殊的情况还是可能会有丢包的情况,比如同时有超过服务器负载的客户端在请求数据。 2.如果丢包的情况发生时,类似开大会会场人人都发微信图片,看着WiFi信号满格,却发不出去,差不多一样的道理爬虫也是收不到数据的。 3.查了下websocket的WebSocketApp的函数,有个参数on_error,是websocket发生错误的时候触发的,那么我们可以编写这个对应的回调函数来让服务器重发或者其他有效的处理。
    展开

    作者回复: 👍

    43
  • tux
    2019-07-29
    干布球和tt 的提示,解决了报错。 import websocket #pip install websocket-client import _thread as thread 在查找资料时,看到了: import websocket try: import thread except ImportError: import _thread as thread
    展开
    15
  • Claywoow
    2019-08-04
    老师,请教个问题,为什么我把这两个类分成两个模块来测试的时候,程序会进入无响应的状态,好像一直在运行,这会是什么原因呢?
    共 1 条评论
    7
  • 干布球
    2019-07-29
    第二段代码少了个import time,python3里面thread用import _thread,不知是不是这样?
    共 1 条评论
    8
  • SCAR
    2019-07-29
    尝试回答下Destroy的问题: 查看WebsocketApp函数: on_message: callable object which is called when received data. on_message has 2 arguments. The 1st argument is this class object. The 2nd argument is utf-8 string which we get from the server. 你如果直接on_message = self.on_message,那么会缺少第一个参数,因为class Crawler类里on_message(self,message)是缺少如最上面老师例子def on_message(ws, message)里的ws的。所以 on_message = lambda ws, message: self.on_message(message)是通过lambda补上第一个参数ws。
    展开
    共 3 条评论
    7
  • jxs1211
    2019-08-03
    实测on_message = self.on_message没有问题,源码中_callback中只是对回调函数做了类型判断,self.on_message是和method,所以直接将返回data给了形参message,执行接受数据的处理
    4
  • JustDoDT
    2019-07-29
    websocket资料 https://pypi.org/project/websocket_client/

    作者回复: 👍

    5
  • TKbook
    2019-07-29
    因此,应该你注意到了,它的第一个参数是 self,这里如果直接写成 on_message = self.on_message 将会出错。 为了避免这个问题,我们需要将函数再次包装一下。这里我使用了前面学过的匿名函数,来传递中间状态,注意我们只需要 message,因此传入 message 即可。 这段没看懂,老师或者哪位大神能解释一下?
    共 2 条评论
    3
  • Geek_688e7c
    2022-02-07
    按照老师的代码来都没有反应,是不是Url失效了? 我直接在浏览器里访问Url是说无法访问。 出现程序运行没有报错但是没有任何输出和结果的情况,是不是就是服务端出了问题呢
    共 2 条评论
    3
  • BinyGo
    2021-06-28
    理解:on_message = lambda ws, message: self.on_message(message) 我们先来看一个lambda的函数写法: g = lambda x, y=2, z=3: x + y + z g(1, z=4, y=5) # 输出结果是10 这个表达式也可以写成 def g(x, y=2, z=3): return x + y + z 那么再来理解老师的写法,也就好理解多了 websocket原本的on_message被我们修改为匿名函数, 也就是当websocket回调的时候,本来是带2个参数ws和message去找on_message函数的,变成了去找我们写的匿名函数 on_message(ws, message),改成匿名函数:anonymous(ws, message),也对应了lambda ws, message 因此这里参数必须一致 def anonymous(ws, message): # 这里函数名称随意,因为是匿名函数,只是来理解lambda的写法 self.on_message(message) #这里再让程序去执行我们需要的代码。绕了一个圈 为了验证这个逻辑流程,下面2行代码参数名改一下,做一个输出,执行老师的代码大概就20多秒就有打印采集的数据,验证成功 self.ws = websocket.WebSocketApp('wss://api.gemini.com/v1/marketdata/{}'.format(symbol), on_message=lambda gg, mmm: self.on_message(mmm)) def on_message(self, mmm): print(mmm) 最后不知道有没更优美的写法呢???
    展开
    2
  • shiziwen
    2020-05-20
    请问文章中,接口获取的数据中,bids和ask是什么意思呢?

    作者回复: orderbook 的买方和卖方:https://www.investopedia.com/terms/b/bid-and-ask.asp

    2
  • 建强
    2020-02-19
    网络编程没有经验,想问下老师,我用的是Python3,用ws = websocket.WebSocketApp("ws://echo.websocket.org/", on_message = on_message, on_open = on_open)是报错的,错误信息:AttributeError: module 'websocket' has no attribute 'WebSocketApp',这是什么原因?
    共 4 条评论
    2
  • Merlin
    2019-11-26
    对于web socket的编程,可以用asyncio,我觉得用asyncio来开发web socket更为方便

    作者回复: 嗯嗯

    2
  • Geek_adeba6
    2019-07-30
    想请问是否可以使用STOMP协议与Gemini这样的交易平台通信, 像消息队列rabbitmq 有 stomp的plugins

    作者回复: 理论上他可以这么做,但是 Gemini 目前官方只支持 RESTFul 和 Websocket,以及金融工程中经常用到的 FIX 协议,这些协议都有着广泛支持的开源库和经过充分时间验证的安全性,切换到新的协议没有太大必要,但是以后的事情谁也说不定呢

    3
  • hel793
    2019-09-05
    Latency is 2630.9838048 ms
    1
  • 小侠龙旋风
    2019-09-01
    最近在复习js的相关知识,js中socket.io库封装了websocket,同时也包含了其他连接方式,比如ajax。socket.on(event_name, callback)中的内置事件名有10种。https://socket.io/docs/client-api/
    1
  • 小侠龙旋风
    2019-08-03
    会丢包,在on_error(ws, error)回调函数中做处理
    1
  • 肖肖
    2022-05-13
    并发执行websocket连接,都能连接成功,但都立马断了,只会连接一个,是什么原因呢