├── .gitignore ├── README.md ├── config.json ├── docs ├── binance.md ├── okex.md └── okex_future.md └── src ├── main.py └── platforms ├── __init__.py ├── binance.py ├── deribit.py ├── okex.py └── okex_ftu.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.pyc 3 | test -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 行情服务 3 | 行情服务根据各个交易所当前提供的不同方式,通过REST API或Websocket方式实现了对各大交易所平台实时行情数据的获取及推送。 4 | 5 | 6 | #### 安装 7 | 需要安装 `thenextquant` 量化交易框架,使用 `pip` 可以简单方便安装: 8 | ```text 9 | pip install thenextquant 10 | ``` 11 | 12 | #### 运行 13 | ```text 14 | git clone https://github.com/TheNextQuant/Market.git # 下载项目 15 | cd Market # 进入项目目录 16 | vim config.json # 编辑配置文件 17 | 18 | python src/main.py config.json # 启动之前请修改配置文件 19 | ``` 20 | > 配置请参考 [配置文件说明](https://github.com/TheNextQuant/thenextquant/blob/master/docs/configure/README.md)。 21 | 22 | 23 | #### 各大交易所行情 24 | 25 | - [Binance](docs/binance.md) 26 | - [OKEx](docs/okex.md) 27 | - [OKEx Future](docs/okex_future.md) 28 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "LOG": { 3 | "console": true, 4 | "level": "DEBUG", 5 | "path": "/data/logs/servers/Market", 6 | "name": "market.log", 7 | "clear": true, 8 | "backup_count": 5 9 | }, 10 | "RABBITMQ": { 11 | "host": "127.0.0.1", 12 | "port": 5672, 13 | "username": "test", 14 | "password": "213456" 15 | }, 16 | "PROXY": "http://127.0.0.1:1087", 17 | 18 | "PLATFORMS": { 19 | "binance": { 20 | "symbols": [ 21 | "BTC/USD", 22 | "LTC/USD" 23 | ], 24 | "channels": [ 25 | "ticker", "kline", "orderbook", "trade" 26 | ] 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/binance.md: -------------------------------------------------------------------------------- 1 | 2 | ## Binance(币安)行情 3 | 4 | Binance(币安)的行情数据根据 [binance官方文档](https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md) 提供的方式, 5 | 通过websocket协议,订阅Binance官方实时推送的行情数据。然后程序将源数据经过适当打包处理,并通过行情事件的形式发布到事件中心。 6 | 7 | 当前行情服务器能够收集Binance的行情数据包括:Orderbook(订单薄)、Trade(成交)、Kline(K线)。 8 | 9 | ##### 1. 服务配置 10 | 11 | 配置文件需要指定一些基础参数来告诉行情服务器应该做什么,一个完整的配置文件大致如下: 12 | 13 | ```json 14 | { 15 | "LOG": { 16 | "console": true, 17 | "level": "DEBUG", 18 | "path": "/data/logs/servers/Market", 19 | "name": "market.log", 20 | "clear": true, 21 | "backup_count": 5 22 | }, 23 | "RABBITMQ": { 24 | "host": "127.0.0.1", 25 | "port": 5672, 26 | "username": "test", 27 | "password": "213456" 28 | }, 29 | "PROXY": "http://127.0.0.1:1087", 30 | 31 | "PLATFORMS": { 32 | "binance": { 33 | "symbols": [ 34 | "BTC/USDT", 35 | "LTC/USDT" 36 | ], 37 | "channels": [ 38 | "kline", "orderbook", "trade" 39 | ] 40 | } 41 | } 42 | } 43 | ``` 44 | 以上配置表示:订阅 `binance` 交易所里,交易对 `BTC/USDT` 和 `LTC/USDT` 的 `kline K线` 、 `orderbook 订单薄` 和 `trade 成交` 行情数据。 45 | 46 | > 配置文件可以参考 [配置文件说明](https://github.com/TheNextQuant/thenextquant/blob/master/docs/configure/README.md)。 47 | > 此处对 `PLATFORMS` 下的关键配置做一下说明: 48 | - PLATFORMS `dict` 需要配置的交易平台,key为交易平台名称,value为对应的行情配置 49 | - binance `dict` 交易平台行情配置 50 | - symbols `list` 需要订阅行情数据的交易对,注意此处配置的交易对都需要大写字母,交易对之间包含斜杠 51 | - channels `list` 需要订阅的行情类型,其中: kline K线 / orderbook 订单薄 / trade 成交 52 | 53 | 54 | > 其它: 55 | - [orderbook 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#21-%E8%AE%A2%E5%8D%95%E8%96%84orderbook) 56 | - [Kline 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#22-k%E7%BA%BFkline) 57 | - [Trade 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#23-%E6%88%90%E4%BA%A4trade) 58 | -------------------------------------------------------------------------------- /docs/okex.md: -------------------------------------------------------------------------------- 1 | 2 | ## OKEx行情 3 | 4 | OKEx的行情数据根据 [OKEx官方文档](https://www.okex.com/docs/zh) 提供的方式, 5 | 通过websocket协议,订阅OKEx官方实时推送的行情数据。然后程序将源数据经过适当打包处理,并通过行情事件的形式发布到事件中心。 6 | 7 | 当前行情服务器能够收集OKEx的行情数据包括:Orderbook(订单薄)、Trade(成交)、Kline(K线)。 8 | 9 | ##### 1. 服务配置 10 | 11 | 配置文件需要指定一些基础参数来告诉行情服务器应该做什么,一个完整的配置文件大致如下: 12 | 13 | ```json 14 | { 15 | "LOG": { 16 | "console": true, 17 | "level": "DEBUG", 18 | "path": "/data/logs/servers/Market", 19 | "name": "market.log", 20 | "clear": true, 21 | "backup_count": 5 22 | }, 23 | "RABBITMQ": { 24 | "host": "127.0.0.1", 25 | "port": 5672, 26 | "username": "test", 27 | "password": "213456" 28 | }, 29 | "PROXY": "http://127.0.0.1:1087", 30 | 31 | "PLATFORMS": { 32 | "okex": { 33 | "symbols": [ 34 | "BTC/USDT", 35 | "LTC/USDT" 36 | ], 37 | "channels": [ 38 | "kline", "orderbook", "trade" 39 | ] 40 | } 41 | } 42 | } 43 | ``` 44 | 以上配置表示:订阅 `okex` 交易所里,交易对 `BTC/USDT` 和 `LTC/USDT` 的 `kline K线` 、 `orderbook 订单薄` 和 `trade 成交` 行情数据。 45 | 46 | > 配置文件可以参考 [配置文件说明](https://github.com/TheNextQuant/thenextquant/blob/master/docs/configure/README.md)。 47 | > 此处对 `PLATFORMS` 下的关键配置做一下说明: 48 | - PLATFORMS `dict` 需要配置的交易平台,key为交易平台名称,value为对应的行情配置 49 | - okex `dict` 交易平台行情配置 50 | - symbols `list` 需要订阅行情数据的交易对,注意此处配置的交易对都需要大写字母,交易对之间包含斜杠 51 | - channels `list` 需要订阅的行情类型,其中: kline K线 / orderbook 订单薄 / trade 成交 52 | 53 | 54 | > 其它: 55 | - [orderbook 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#21-%E8%AE%A2%E5%8D%95%E8%96%84orderbook) 56 | - [Kline 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#22-k%E7%BA%BFkline) 57 | - [Trade 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#23-%E6%88%90%E4%BA%A4trade) 58 | -------------------------------------------------------------------------------- /docs/okex_future.md: -------------------------------------------------------------------------------- 1 | 2 | ## OKEx Future行情 3 | 4 | OKEx Future的行情数据根据 [OKEx Future官方文档](https://www.okex.com/docs/zh) 提供的方式, 5 | 通过websocket协议,订阅OKEx Future官方实时推送的行情数据。然后程序将源数据经过适当打包处理,并通过行情事件的形式发布到事件中心。 6 | 7 | 当前行情服务器能够收集OKEx Future的行情数据包括:Orderbook(订单薄)。 8 | 9 | ##### 1. 服务配置 10 | 11 | 配置文件需要指定一些基础参数来告诉行情服务器应该做什么,一个完整的配置文件大致如下: 12 | 13 | ```json 14 | { 15 | "LOG": { 16 | "console": true, 17 | "level": "DEBUG", 18 | "path": "/data/logs/servers/Market", 19 | "name": "market.log", 20 | "clear": true, 21 | "backup_count": 5 22 | }, 23 | "RABBITMQ": { 24 | "host": "127.0.0.1", 25 | "port": 5672, 26 | "username": "test", 27 | "password": "213456" 28 | }, 29 | "PROXY": "http://127.0.0.1:1087", 30 | 31 | "PLATFORMS": { 32 | "okex_future": { 33 | "symbols": [ 34 | "BTC-USD-190524" 35 | ], 36 | "channels": [ 37 | "orderbook" 38 | ] 39 | } 40 | } 41 | } 42 | ``` 43 | 以上配置表示:订阅 `okex_future` 交易所里,交易对 `BTC/USDT` 和 `LTC/USDT` 的 `orderbook 订单薄` 行情数据。 44 | 45 | > 配置文件可以参考 [配置文件说明](https://github.com/TheNextQuant/thenextquant/blob/master/docs/configure/README.md)。 46 | > 此处对 `PLATFORMS` 下的关键配置做一下说明: 47 | - PLATFORMS `dict` 需要配置的交易平台,key为交易平台名称,value为对应的行情配置 48 | - okex_future `dict` 交易平台行情配置 49 | - symbols `list` 需要订阅行情数据的交易对,注意此处配置的交易对都需要大写字母 50 | - channels `list` 需要订阅的行情类型,其中: orderbook 订单薄 51 | 52 | 53 | > 其它: 54 | - [orderbook 数据结构](https://github.com/TheNextQuant/thenextquant/blob/master/docs/market.md#21-%E8%AE%A2%E5%8D%95%E8%96%84orderbook) 55 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | 3 | """ 4 | 行情服务 5 | 6 | Author: HuangTao 7 | Date: 2018/05/04 8 | """ 9 | 10 | import sys 11 | 12 | from quant.quant import quant 13 | from quant.config import config 14 | from quant.const import OKEX, OKEX_FUTURE, BINANCE, DERIBIT 15 | 16 | 17 | def initialize(): 18 | """ 初始化 19 | """ 20 | 21 | for platform in config.platforms: 22 | if platform == OKEX: 23 | from platforms.okex import OKEx as Market 24 | elif platform == OKEX_FUTURE: 25 | from platforms.okex_ftu import OKExFuture as Market 26 | elif platform == BINANCE: 27 | from platforms.binance import Binance as Market 28 | elif platform == DERIBIT: 29 | from platforms.deribit import Deribit as Market 30 | else: 31 | from quant.utils import logger 32 | logger.error("platform error! platform:", platform) 33 | continue 34 | Market() 35 | 36 | 37 | def main(): 38 | config_file = sys.argv[1] # 配置文件 config.json 39 | quant.initialize(config_file) 40 | initialize() 41 | quant.start() 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /src/platforms/__init__.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | -------------------------------------------------------------------------------- /src/platforms/binance.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | Binance 行情数据 5 | https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md 6 | 7 | Author: HuangTao 8 | Date: 2018/07/04 9 | """ 10 | 11 | from quant import const 12 | from quant.utils import tools 13 | from quant.utils import logger 14 | from quant.config import config 15 | from quant.const import BINANCE 16 | from quant.utils.websocket import Websocket 17 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 18 | from quant.event import EventTrade, EventKline, EventOrderbook 19 | 20 | 21 | class Binance(Websocket): 22 | """ Binance 行情数据 23 | """ 24 | 25 | def __init__(self): 26 | self._platform = BINANCE 27 | self._url = config.platforms.get(self._platform).get("wss", "wss://stream.binance.com:9443") 28 | self._symbols = list(set(config.platforms.get(self._platform).get("symbols"))) 29 | self._channels = config.platforms.get(self._platform).get("channels") 30 | 31 | self._c_to_s = {} # {"channel": "symbol"} 32 | self._tickers = {} # 最新行情 {"symbol": price_info} 33 | 34 | self._make_url() 35 | super(Binance, self).__init__(self._url) 36 | self.initialize() 37 | 38 | def _make_url(self): 39 | """ 拼接请求url 40 | """ 41 | cc = [] 42 | for ch in self._channels: 43 | if ch == "kline": # 订阅K线 1分钟 44 | for symbol in self._symbols: 45 | c = self._symbol_to_channel(symbol, "kline_1m") 46 | cc.append(c) 47 | elif ch == "orderbook": # 订阅订单薄 深度为5 48 | for symbol in self._symbols: 49 | c = self._symbol_to_channel(symbol, "depth20") 50 | cc.append(c) 51 | elif ch == "trade": # 订阅实时交易 52 | for symbol in self._symbols: 53 | c = self._symbol_to_channel(symbol, "trade") 54 | cc.append(c) 55 | else: 56 | logger.error("channel error! channel:", ch, caller=self) 57 | self._url += "/stream?streams=" + "/".join(cc) 58 | 59 | async def process(self, msg): 60 | """ 处理websocket上接收到的消息 61 | """ 62 | # logger.debug("msg:", msg, caller=self) 63 | if not isinstance(msg, dict): 64 | return 65 | 66 | channel = msg.get("stream") 67 | if channel not in self._c_to_s: 68 | logger.warn("unkown channel, msg:", msg, caller=self) 69 | return 70 | 71 | symbol = self._c_to_s[channel] 72 | data = msg.get("data") 73 | e = data.get("e") # 事件名称 74 | 75 | # 保存数据到数据库 76 | if e == "kline": # K线 77 | kline = { 78 | "platform": self._platform, 79 | "symbol": symbol, 80 | "open": data.get("k").get("o"), # 开盘价 81 | "high": data.get("k").get("h"), # 最高价 82 | "low": data.get("k").get("l"), # 最低价 83 | "close": data.get("k").get("c"), # 收盘价 84 | "volume": data.get("k").get("q"), # 收盘价 85 | "timestamp": data.get("k").get("t"), # 时间戳 86 | "kline_type": const.MARKET_TYPE_KLINE 87 | } 88 | EventKline(**kline).publish() 89 | logger.info("symbol:", symbol, "kline:", kline, caller=self) 90 | elif channel.endswith("depth20"): # 订单薄 91 | bids = [] 92 | asks = [] 93 | for bid in data.get("bids"): 94 | bids.append(bid[:2]) 95 | for ask in data.get("asks"): 96 | asks.append(ask[:2]) 97 | orderbook = { 98 | "platform": BINANCE, 99 | "symbol": symbol, 100 | "asks": asks, 101 | "bids": bids, 102 | "timestamp": tools.get_cur_timestamp_ms() 103 | } 104 | EventOrderbook(**orderbook).publish() 105 | logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) 106 | elif e == "trade": # 实时成交信息 107 | trade = { 108 | "platform": self._platform, 109 | "symbol": symbol, 110 | "action": ORDER_ACTION_SELL if data["m"] else ORDER_ACTION_BUY, 111 | "price": data.get("p"), 112 | "quantity": data.get("q"), 113 | "timestamp": data.get("T") 114 | } 115 | EventTrade(**trade).publish() 116 | logger.info("symbol:", symbol, "trade:", trade, caller=self) 117 | else: 118 | logger.error("event error! msg:", msg, caller=self) 119 | 120 | def _symbol_to_channel(self, symbol, channel_type="ticker"): 121 | """ symbol转换到channel 122 | @param symbol symbol名字 123 | @param channel_type 频道类型 kline K线 / ticker 行情 124 | """ 125 | channel = "{x}@{y}".format(x=symbol.replace("/", "").lower(), y=channel_type) 126 | self._c_to_s[channel] = symbol 127 | return channel 128 | -------------------------------------------------------------------------------- /src/platforms/deribit.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | deribit外盘行情 5 | https://www.deribit.com/main#/pages/docs/api 6 | https://www.deribit.com/api/v1/public/getinstruments 7 | 8 | Author: HuangTao 9 | Date: 2018/10/08 10 | """ 11 | 12 | import base64 13 | import hashlib 14 | 15 | from quant.utils import tools 16 | from quant.utils import logger 17 | from quant.config import config 18 | from quant.const import DERIBIT 19 | from quant.event import EventOrderbook 20 | from quant.utils.websocket import Websocket 21 | 22 | 23 | class Deribit(Websocket): 24 | """ deribit外盘行情 25 | """ 26 | 27 | def __init__(self): 28 | self._platform = DERIBIT 29 | self._url = config.platforms.get(self._platform).get("wss") 30 | self._symbols = list(set(config.platforms.get(self._platform).get("symbols"))) 31 | self._access_key = config.platforms.get(self._platform).get("access_key") 32 | self._secret_key = config.platforms.get(self._platform).get("secret_key") 33 | self._last_msg_ts = tools.get_cur_timestamp() # 上次接收到消息的时间戳 34 | 35 | super(Deribit, self).__init__(self._url) 36 | self.heartbeat_msg = {"action": "/api/v1/public/ping"} 37 | self.initialize() 38 | 39 | async def connected_callback(self): 40 | """ 建立连接之后,订阅事件 41 | """ 42 | nonce = tools.get_cur_timestamp_ms() 43 | uri = "/api/v1/private/subscribe" 44 | params = { 45 | "instrument": self._symbols, 46 | "event": ["order_book"] 47 | } 48 | sign = self.deribit_signature(nonce, uri, params, self._access_key, self._secret_key) 49 | data = { 50 | "id": "huangtao", 51 | "action": uri, 52 | "arguments": params, 53 | "sig": sign 54 | } 55 | await self.ws.send_json(data) 56 | logger.info("subscribe orderbook success.", caller=self) 57 | 58 | async def process(self, msg): 59 | """ 处理websocket上接收到的消息 60 | """ 61 | # logger.debug("msg:", msg, caller=self) 62 | if tools.get_cur_timestamp() <= self._last_msg_ts: 63 | return 64 | if not isinstance(msg, dict): 65 | return 66 | notifications = msg.get("notifications") 67 | if not notifications: 68 | return 69 | message = notifications[0].get("message") 70 | if message != "order_book_event": 71 | return 72 | 73 | symbol = notifications[0].get("result").get("instrument") 74 | bids = [] 75 | for item in notifications[0].get("result").get("bids")[:10]: 76 | b = [item.get("price"), item.get("quantity")] 77 | bids.append(b) 78 | asks = [] 79 | for item in notifications[0].get("result").get("asks")[:10]: 80 | a = [item.get("price"), item.get("quantity")] 81 | asks.append(a) 82 | self._last_msg_ts = tools.get_cur_timestamp() 83 | orderbook = { 84 | "platform": self._platform, 85 | "symbol": symbol, 86 | "asks": asks, 87 | "bids": bids, 88 | "timestamp": self._last_msg_ts 89 | } 90 | EventOrderbook(**orderbook).publish() 91 | logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) 92 | 93 | def deribit_signature(self, nonce, uri, params, access_key, access_secret): 94 | """ 生成signature 95 | """ 96 | sign = "_=%s&_ackey=%s&_acsec=%s&_action=%s" % (nonce, access_key, access_secret, uri) 97 | for key in sorted(params.keys()): 98 | sign += "&" + key + "=" + "".join(params[key]) 99 | return "%s.%s.%s" % (access_key, nonce, base64.b64encode(hashlib.sha256(sign.encode()).digest()).decode()) 100 | -------------------------------------------------------------------------------- /src/platforms/okex.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | OKEx 现货行情 5 | https://www.okex.com/docs/zh 6 | 7 | Author: HuangTao 8 | Date: 2018/05/21 9 | """ 10 | 11 | import zlib 12 | import json 13 | import copy 14 | 15 | from quant import const 16 | from quant.utils import tools 17 | from quant.utils import logger 18 | from quant.config import config 19 | from quant.const import OKEX 20 | from quant.utils.websocket import Websocket 21 | from quant.event import EventOrderbook, EventTrade, EventKline 22 | from quant.order import ORDER_ACTION_BUY, ORDER_ACTION_SELL 23 | 24 | 25 | class OKEx(Websocket): 26 | """ OKEx 现货行情 27 | """ 28 | 29 | def __init__(self): 30 | self._platform = OKEX 31 | 32 | self._wss = config.platforms.get(self._platform).get("wss", "wss://real.okex.com:10442") 33 | self._symbols = list(set(config.platforms.get(self._platform).get("symbols"))) 34 | self._channels = config.platforms.get(self._platform).get("channels") 35 | 36 | self._orderbooks = {} # 订单薄数据 {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}}} 37 | self._length = 20 # 订单薄数据推送长度 38 | 39 | url = self._wss + "/ws/v3" 40 | super(OKEx, self).__init__(url) 41 | self.heartbeat_msg = "ping" 42 | self.initialize() 43 | 44 | async def connected_callback(self): 45 | """ 建立连接之后,订阅事件 ticker 46 | """ 47 | ches = [] 48 | for ch in self._channels: 49 | if ch == "orderbook": # 订阅orderbook行情 50 | for symbol in self._symbols: 51 | ch = "spot/depth:{s}".format(s=symbol.replace("/", '-')) 52 | ches.append(ch) 53 | elif ch == "trade": 54 | for symbol in self._symbols: 55 | ch = "spot/trade:{s}".format(s=symbol.replace("/", '-')) 56 | ches.append(ch) 57 | elif ch == "kline": 58 | for symbol in self._symbols: 59 | ch = "spot/candle60s:{s}".format(s=symbol.replace("/", '-')) 60 | ches.append(ch) 61 | else: 62 | logger.error("channel error! channel:", ch, caller=self) 63 | if ches: 64 | msg = { 65 | "op": "subscribe", 66 | "args": ches 67 | } 68 | await self.ws.send_json(msg) 69 | logger.info("subscribe orderbook success.", caller=self) 70 | 71 | async def process_binary(self, raw): 72 | """ 处理websocket上接收到的消息 73 | @param raw 原始的压缩数据 74 | """ 75 | decompress = zlib.decompressobj(-zlib.MAX_WBITS) 76 | msg = decompress.decompress(raw) 77 | msg += decompress.flush() 78 | msg = msg.decode() 79 | if msg == "pong": # 心跳返回 80 | return 81 | msg = json.loads(msg) 82 | # logger.debug("msg:", msg, caller=self) 83 | 84 | table = msg.get("table") 85 | if table == "spot/depth": # 订单薄 86 | if msg.get("action") == "partial": # 首次返回全量数据 87 | for d in msg["data"]: 88 | await self.deal_orderbook_partial(d) 89 | elif msg.get("action") == "update": # 返回增量数据 90 | for d in msg["data"]: 91 | await self.deal_orderbook_update(d) 92 | else: 93 | logger.warn("unhandle msg:", msg, caller=self) 94 | elif table == "spot/trade": 95 | for d in msg["data"]: 96 | await self.deal_trade_update(d) 97 | elif table == "spot/candle60s": 98 | for d in msg["data"]: 99 | await self.deal_kline_update(d) 100 | else: 101 | logger.warn("unhandle msg:", msg, caller=self) 102 | 103 | async def deal_orderbook_partial(self, data): 104 | """ 处理全量数据 105 | """ 106 | symbol = data.get("instrument_id").replace("-", "/") 107 | if symbol not in self._symbols: 108 | return 109 | asks = data.get("asks") 110 | bids = data.get("bids") 111 | self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} 112 | for ask in asks: 113 | price = float(ask[0]) 114 | quantity = float(ask[1]) 115 | self._orderbooks[symbol]["asks"][price] = quantity 116 | for bid in bids: 117 | price = float(bid[0]) 118 | quantity = float(bid[1]) 119 | self._orderbooks[symbol]["bids"][price] = quantity 120 | timestamp = tools.utctime_str_to_mts(data.get("timestamp")) 121 | self._orderbooks[symbol]["timestamp"] = timestamp 122 | 123 | async def deal_orderbook_update(self, data): 124 | """ 处理orderbook增量数据 125 | """ 126 | symbol = data.get("instrument_id").replace("-", "/") 127 | asks = data.get("asks") 128 | bids = data.get("bids") 129 | timestamp = tools.utctime_str_to_mts(data.get("timestamp")) 130 | 131 | if symbol not in self._orderbooks: 132 | return 133 | self._orderbooks[symbol]["timestamp"] = timestamp 134 | 135 | for ask in asks: 136 | price = float(ask[0]) 137 | quantity = float(ask[1]) 138 | if quantity == 0 and price in self._orderbooks[symbol]["asks"]: 139 | self._orderbooks[symbol]["asks"].pop(price) 140 | else: 141 | self._orderbooks[symbol]["asks"][price] = quantity 142 | 143 | for bid in bids: 144 | price = float(bid[0]) 145 | quantity = float(bid[1]) 146 | if quantity == 0 and price in self._orderbooks[symbol]["bids"]: 147 | self._orderbooks[symbol]["bids"].pop(price) 148 | else: 149 | self._orderbooks[symbol]["bids"][price] = quantity 150 | 151 | await self.publish_orderbook() 152 | 153 | async def publish_orderbook(self, *args, **kwargs): 154 | """ 推送orderbook数据 155 | """ 156 | for symbol, data in self._orderbooks.items(): 157 | ob = copy.copy(data) 158 | if not ob["asks"] or not ob["bids"]: 159 | logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) 160 | continue 161 | 162 | ask_keys = sorted(list(ob["asks"].keys())) 163 | bid_keys = sorted(list(ob["bids"].keys()), reverse=True) 164 | if ask_keys[0] <= bid_keys[0]: 165 | logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) 166 | continue 167 | 168 | # 卖 169 | asks = [] 170 | for k in ask_keys[:self._length]: 171 | price = "%.8f" % k 172 | quantity = "%.8f" % ob["asks"].get(k) 173 | asks.append([price, quantity]) 174 | 175 | # 买 176 | bids = [] 177 | for k in bid_keys[:self._length]: 178 | price = "%.8f" % k 179 | quantity = "%.8f" % ob["bids"].get(k) 180 | bids.append([price, quantity]) 181 | 182 | # 推送订单薄数据 183 | orderbook = { 184 | "platform": self._platform, 185 | "symbol": symbol, 186 | "asks": asks, 187 | "bids": bids, 188 | "timestamp": ob["timestamp"] 189 | } 190 | EventOrderbook(**orderbook).publish() 191 | logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) 192 | 193 | async def deal_trade_update(self, data): 194 | """ 处理trade数据 195 | """ 196 | symbol = data.get("instrument_id").replace("-", "/") 197 | if symbol not in self._symbols: 198 | return 199 | action = ORDER_ACTION_BUY if data["side"] == "buy" else ORDER_ACTION_SELL 200 | price = "%.8f" % float(data["price"]) 201 | quantity = "%.8f" % float(data["size"]) 202 | timestamp = tools.utctime_str_to_mts(data["timestamp"]) 203 | 204 | # 推送trade数据 205 | trade = { 206 | "platform": self._platform, 207 | "symbol": symbol, 208 | "action": action, 209 | "price": price, 210 | "quantity": quantity, 211 | "timestamp": timestamp 212 | } 213 | EventTrade(**trade).publish() 214 | logger.info("symbol:", symbol, "trade:", trade, caller=self) 215 | 216 | async def deal_kline_update(self, data): 217 | """ 处理K线数据 1分钟 218 | """ 219 | symbol = data["instrument_id"].replace("-", "/") 220 | if symbol not in self._symbols: 221 | return 222 | timestamp = tools.utctime_str_to_mts(data["candle"][0]) 223 | _open = "%.8f" % float(data["candle"][1]) 224 | high = "%.8f" % float(data["candle"][2]) 225 | low = "%.8f" % float(data["candle"][3]) 226 | close = "%.8f" % float(data["candle"][4]) 227 | volume = "%.8f" % float(data["candle"][5]) 228 | 229 | # 推送trade数据 230 | kline = { 231 | "platform": self._platform, 232 | "symbol": symbol, 233 | "open": _open, 234 | "high": high, 235 | "low": low, 236 | "close": close, 237 | "volume": volume, 238 | "timestamp": timestamp, 239 | "kline_type": const.MARKET_TYPE_KLINE 240 | } 241 | EventKline(**kline).publish() 242 | logger.info("symbol:", symbol, "kline:", kline, caller=self) 243 | -------------------------------------------------------------------------------- /src/platforms/okex_ftu.py: -------------------------------------------------------------------------------- 1 | # -*— coding:utf-8 -*- 2 | 3 | """ 4 | OKEx行情 分割合约 5 | https://www.okex.com/docs/zh/#futures_ws-all 6 | 7 | Author: HuangTao 8 | Date: 2018/12/20 9 | """ 10 | 11 | import zlib 12 | import json 13 | import copy 14 | 15 | from quant.utils import tools 16 | from quant.utils import logger 17 | from quant.config import config 18 | from quant.const import OKEX_FUTURE 19 | from quant.event import EventOrderbook 20 | from quant.utils.websocket import Websocket 21 | 22 | 23 | class OKExFuture(Websocket): 24 | """ OKEx行情 分割合约 25 | """ 26 | 27 | def __init__(self): 28 | self._platform = OKEX_FUTURE 29 | 30 | self._wss = config.platforms.get(self._platform).get("wss", "wss://real.okex.com:10442") 31 | self._symbols = list(set(config.platforms.get(self._platform).get("symbols"))) 32 | self._channels = config.platforms.get(self._platform).get("channels") 33 | 34 | self._orderbooks = {} # 订单薄数据 {"symbol": {"bids": {"price": quantity, ...}, "asks": {...}}} 35 | self._length = 20 # 订单薄数据推送长度 36 | 37 | url = self._wss + "/ws/v3" 38 | super(OKExFuture, self).__init__(url) 39 | self.heartbeat_msg = "ping" 40 | self.initialize() 41 | 42 | async def connected_callback(self): 43 | """ 建立连接之后,订阅事件 ticker 44 | """ 45 | for ch in self._channels: 46 | if ch == "orderbook": # 订阅orderbook行情 47 | chs = [] 48 | for symbol in self._symbols: 49 | ch = "futures/depth:{s}".format(s=symbol) 50 | chs.append(ch) 51 | msg = { 52 | "op": "subscribe", 53 | "args": chs 54 | } 55 | await self.ws.send_json(msg) 56 | logger.info("subscribe orderbook success.", caller=self) 57 | else: 58 | logger.error("channel error! channel:", ch, caller=self) 59 | 60 | async def process_binary(self, raw): 61 | """ 处理websocket上接收到的消息 62 | @param raw 原始的压缩数据 63 | """ 64 | decompress = zlib.decompressobj(-zlib.MAX_WBITS) 65 | msg = decompress.decompress(raw) 66 | msg += decompress.flush() 67 | msg = msg.decode() 68 | if msg == "pong": # 心跳返回 69 | return 70 | msg = json.loads(msg) 71 | # logger.debug("msg:", msg, caller=self) 72 | 73 | table = msg.get("table") 74 | if table == "futures/depth": # 订单薄 75 | if msg.get("action") == "partial": # 首次返回全量数据 76 | for d in msg["data"]: 77 | await self.deal_orderbook_partial(d) 78 | elif msg.get("action") == "update": # 返回增量数据 79 | for d in msg["data"]: 80 | await self.deal_orderbook_update(d) 81 | else: 82 | logger.warn("unhandle msg:", msg, caller=self) 83 | else: 84 | logger.warn("unhandle msg:", msg, caller=self) 85 | 86 | async def deal_orderbook_partial(self, data): 87 | """ 处理全量数据 88 | """ 89 | symbol = data.get("instrument_id") 90 | if symbol not in self._symbols: 91 | return 92 | asks = data.get("asks") 93 | bids = data.get("bids") 94 | self._orderbooks[symbol] = {"asks": {}, "bids": {}, "timestamp": 0} 95 | for ask in asks: 96 | price = float(ask[0]) 97 | quantity = int(ask[1]) 98 | self._orderbooks[symbol]["asks"][price] = quantity 99 | for bid in bids: 100 | price = float(bid[0]) 101 | quantity = int(bid[1]) 102 | self._orderbooks[symbol]["bids"][price] = quantity 103 | timestamp = tools.utctime_str_to_mts(data.get("timestamp")) 104 | self._orderbooks[symbol]["timestamp"] = timestamp 105 | 106 | async def deal_orderbook_update(self, data): 107 | """ 处理orderbook增量数据 108 | """ 109 | symbol = data.get("instrument_id") 110 | asks = data.get("asks") 111 | bids = data.get("bids") 112 | timestamp = tools.utctime_str_to_mts(data.get("timestamp")) 113 | 114 | if symbol not in self._orderbooks: 115 | return 116 | self._orderbooks[symbol]["timestamp"] = timestamp 117 | 118 | for ask in asks: 119 | price = float(ask[0]) 120 | quantity = int(ask[1]) 121 | if quantity == 0 and price in self._orderbooks[symbol]["asks"]: 122 | self._orderbooks[symbol]["asks"].pop(price) 123 | else: 124 | self._orderbooks[symbol]["asks"][price] = quantity 125 | 126 | for bid in bids: 127 | price = float(bid[0]) 128 | quantity = int(bid[1]) 129 | if quantity == 0 and price in self._orderbooks[symbol]["bids"]: 130 | self._orderbooks[symbol]["bids"].pop(price) 131 | else: 132 | self._orderbooks[symbol]["bids"][price] = quantity 133 | 134 | await self.publish_orderbook() 135 | 136 | async def publish_orderbook(self, *args, **kwargs): 137 | """ 推送orderbook数据 138 | """ 139 | for symbol, data in self._orderbooks.items(): 140 | ob = copy.copy(data) 141 | if not ob["asks"] or not ob["bids"]: 142 | logger.warn("symbol:", symbol, "asks:", ob["asks"], "bids:", ob["bids"], caller=self) 143 | continue 144 | 145 | ask_keys = sorted(list(ob["asks"].keys())) 146 | bid_keys = sorted(list(ob["bids"].keys()), reverse=True) 147 | if ask_keys[0] <= bid_keys[0]: 148 | logger.warn("symbol:", symbol, "ask1:", ask_keys[0], "bid1:", bid_keys[0], caller=self) 149 | continue 150 | 151 | # 卖 152 | asks = [] 153 | for k in ask_keys[:self._length]: 154 | price = "%.8f" % k 155 | quantity = str(ob["asks"].get(k)) 156 | asks.append([price, quantity]) 157 | 158 | # 买 159 | bids = [] 160 | for k in bid_keys[:self._length]: 161 | price = "%.8f" % k 162 | quantity = str(ob["bids"].get(k)) 163 | bids.append([price, quantity]) 164 | 165 | # 推送订单薄数据 166 | orderbook = { 167 | "platform": self._platform, 168 | "symbol": symbol, 169 | "asks": asks, 170 | "bids": bids, 171 | "timestamp": ob["timestamp"] 172 | } 173 | EventOrderbook(**orderbook).publish() 174 | logger.info("symbol:", symbol, "orderbook:", orderbook, caller=self) 175 | --------------------------------------------------------------------------------