cryptofeed 3.0.2__tar.gz → 3.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/CHANGES.md +12 -0
- {cryptofeed-3.0.2/cryptofeed.egg-info → cryptofeed-3.0.3}/PKG-INFO +3 -3
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/README.md +2 -2
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/recorder.py +1 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/cryptodotcom.py +3 -6
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/gate.py +1 -1
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/okx.py +82 -74
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/feed.py +13 -2
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/nbbo.py +3 -3
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/history.py +28 -2
- {cryptofeed-3.0.2 → cryptofeed-3.0.3/cryptofeed.egg-info}/PKG-INFO +3 -3
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/pyproject.toml +1 -2
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/LICENSE +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/MANIFEST.in +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/__init__.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/_json.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/__init__.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/_line_protocol.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/backend.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/http.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/influxdb.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/kafka.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/mongo.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/postgres.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/quest.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/redis.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/socket.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/zmq.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/callback.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/__init__.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/_zstd.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/pcap.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/playback.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/config.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/connection.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/connection_handler.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/defines.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exceptions.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchange.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/__init__.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance_delivery.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance_futures.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance_us.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitfinex.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitflyer.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitget.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bithumb.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitstamp.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bybit.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/coinbase.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/deribit.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/dydx.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/gate_futures.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/gemini.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/htx.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/htx_swap.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/hyperliquid.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/independent_reserve.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kraken.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kraken_futures.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kucoin.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kucoin_futures.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/mexc.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/phemex.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/poloniex.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/upbit.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/feedhandler.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/log.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/py.typed +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/symbols.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/types.pyi +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/types.pyx +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/__init__.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/snapshots.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/time.py +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/SOURCES.txt +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/dependency_links.txt +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/requires.txt +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/top_level.txt +0 -0
- {cryptofeed-3.0.2 → cryptofeed-3.0.3}/setup.cfg +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
+
### 3.0.3 (2026-11-02)
|
|
4
|
+
* Bugfix: Duplication issues in NBBO
|
|
5
|
+
* Update: `liquidation_history` feed option, like `trade_history` and `candle_history` - deliver the historical liquidations sent on initial subscribe
|
|
6
|
+
* Update: OKX `info()` has `contract_size` and `contract_currency` for swaps, futures and options; their sizes are in contracts, of that many units of that currency (the base coin on linear contracts and options, USD on inverse swaps and futures)
|
|
7
|
+
* Bugfix: OKX candles have `timestamp` None (exchange sends no timestamp), and monthly (`1M`) candles stop at the next calendar month (UTC+8) instead of a fixed 2,630,000 s after their start
|
|
8
|
+
* Update: OKX TRADES subscribes the `trades-all` channel (one Trade per fill with its own id).
|
|
9
|
+
* Bugfix: OKX no longer subscribes spot and option symbols to funding-rate, or spot symbols to open-interest, which the venue refused with error 60018
|
|
10
|
+
* Bugfix: OKX and Crypto.com skip a ticker update with an empty bid or ask side instead of reporting a price of 0
|
|
11
|
+
* Update: `OKX.instrument_type()` removed (unused)
|
|
12
|
+
* Bugfix: Gate candle volume is now the base currency amount (`a`), not the quote currency volume (`v`)
|
|
13
|
+
* Update: musllinux wheels support
|
|
14
|
+
|
|
3
15
|
### 3.0.2 (2026-10-29)
|
|
4
16
|
* Bugfix: Bybit liquidation side is now the side of the liquidation order (SELL when a long is liquidated), as for Binance and OKX
|
|
5
17
|
* Bugfix: Deribit liquidations flagged `M` (maker liquidated) reported the taker's side
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cryptofeed
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
4
4
|
Summary: Cryptocurrency Exchange Websocket Data Feed Handler
|
|
5
5
|
Author-email: Bryant Moscon <bmoscon@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -171,8 +171,8 @@ cryptofeed received the message that produced the callback. The objects are defi
|
|
|
171
171
|
set when the venue provides them.
|
|
172
172
|
* `Candle.closed` is `True` once the bar is final, by default only closed candles are delivered.
|
|
173
173
|
(`candle_closed_only=False` also delivers each update of the open bar).
|
|
174
|
-
* `trade_history` and `
|
|
175
|
-
subscribe is delivered, by default it is `False`.
|
|
174
|
+
* `trade_history`, `candle_history` and `liquidation_history` control whether the batch of recent trades, candles or
|
|
175
|
+
liquidations some venues send on subscribe is delivered, by default it is `False`.
|
|
176
176
|
|
|
177
177
|
## Backends
|
|
178
178
|
|
|
@@ -121,8 +121,8 @@ cryptofeed received the message that produced the callback. The objects are defi
|
|
|
121
121
|
set when the venue provides them.
|
|
122
122
|
* `Candle.closed` is `True` once the bar is final, by default only closed candles are delivered.
|
|
123
123
|
(`candle_closed_only=False` also delivers each update of the open bar).
|
|
124
|
-
* `trade_history` and `
|
|
125
|
-
subscribe is delivered, by default it is `False`.
|
|
124
|
+
* `trade_history`, `candle_history` and `liquidation_history` control whether the batch of recent trades, candles or
|
|
125
|
+
liquidations some venues send on subscribe is delivered, by default it is `False`.
|
|
126
126
|
|
|
127
127
|
## Backends
|
|
128
128
|
|
|
@@ -107,6 +107,7 @@ class PcapRecorder:
|
|
|
107
107
|
'candle_closed_only': feed.candle_closed_only,
|
|
108
108
|
'trade_history': feed.trade_history,
|
|
109
109
|
'candle_history': feed.candle_history,
|
|
110
|
+
'liquidation_history': feed.liquidation_history,
|
|
110
111
|
'max_depth': feed.max_depth,
|
|
111
112
|
'sandbox': feed.sandbox,
|
|
112
113
|
},
|
|
@@ -204,12 +204,9 @@ class CryptoDotCom(Feed):
|
|
|
204
204
|
|
|
205
205
|
for entry in msg['data']:
|
|
206
206
|
ts = self.timestamp_normalize(entry['t'])
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
t = Ticker(self.id, symbol,
|
|
210
|
-
Decimal(entry['b']) if entry.get('b') else Decimal(0),
|
|
211
|
-
Decimal(entry['k']) if entry.get('k') else Decimal(0),
|
|
212
|
-
ts, raw=entry)
|
|
207
|
+
# an instrument with one side of the book empty omits that side's price
|
|
208
|
+
if wants_ticker and entry.get('b') and entry.get('k'):
|
|
209
|
+
t = Ticker(self.id, symbol, Decimal(entry['b']), Decimal(entry['k']), ts, raw=entry)
|
|
213
210
|
await self.callback(TICKER, t, timestamp)
|
|
214
211
|
if wants_oi:
|
|
215
212
|
await self.callback(OPEN_INTEREST, OpenInterest(self.id, symbol, Decimal(entry['oi']), ts, raw=entry), timestamp)
|
|
@@ -292,7 +292,7 @@ class Gate(SnapshotSync, Feed):
|
|
|
292
292
|
Decimal(msg['result']['c']),
|
|
293
293
|
Decimal(msg['result']['h']),
|
|
294
294
|
Decimal(msg['result']['l']),
|
|
295
|
-
Decimal(msg['result']['
|
|
295
|
+
Decimal(msg['result']['a']),
|
|
296
296
|
bool(msg['result'].get('w')),
|
|
297
297
|
float(msg['time']),
|
|
298
298
|
raw=msg
|
|
@@ -5,6 +5,7 @@ Please see the LICENSE file for the terms and conditions
|
|
|
5
5
|
associated with this software.
|
|
6
6
|
'''
|
|
7
7
|
from collections import defaultdict
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
8
9
|
from decimal import Decimal
|
|
9
10
|
from typing import Dict, List, Tuple, Union
|
|
10
11
|
from cryptofeed import _json as json
|
|
@@ -16,7 +17,7 @@ from cryptofeed.connection import AsyncConnection, HTTPAsyncConn, RestEndpoint,
|
|
|
16
17
|
from cryptofeed.defines import CALL, FUTURES, OKX as OKX_str, LIQUIDATIONS, BUY, OPTION, PERPETUAL, PUT, SELL, FILLED, ASK, BID, FUNDING, L2_BOOK, OPEN_INTEREST, TICKER, TRADES, CANDLES, SPOT, UNFILLED
|
|
17
18
|
from cryptofeed.feed import Feed
|
|
18
19
|
from cryptofeed.exceptions import MissingSequenceNumber
|
|
19
|
-
from cryptofeed.symbols import Symbol
|
|
20
|
+
from cryptofeed.symbols import Symbol, str_to_symbol
|
|
20
21
|
from cryptofeed.types import OrderBook, Trade, Ticker, Funding, OpenInterest, Liquidation, Candle
|
|
21
22
|
|
|
22
23
|
|
|
@@ -33,21 +34,27 @@ class OKX(Feed):
|
|
|
33
34
|
provides_sequence_number = True
|
|
34
35
|
validates_sequence_number = True
|
|
35
36
|
valid_candle_intervals = {'1M', '1W', '1D', '12H', '6H', '4H', '2H', '1H', '30m', '15m', '5m', '3m', '1m'}
|
|
36
|
-
|
|
37
|
+
# bar lengths in seconds; a 1M bar is a calendar month, see _candle_stop
|
|
38
|
+
candle_interval_map = {'1W': 604800, '1D': 86400, '12H': 43200, '6H': 21600, '4H': 14400, '2H': 7200, '1H': 3600, '30m': 1800, '15m': 900, '5m': 300, '3m': 180, '1m': 60}
|
|
39
|
+
# bars without the utc suffix open at 00:00 UTC+8 ("UTC+8 opening price k-line: 6H/12H/1D/2D/3D/1W/1M/3M"),
|
|
40
|
+
# so a 1M bar opens at 16:00 UTC on the last day of the previous month
|
|
41
|
+
CANDLE_TZ = timezone(timedelta(hours=8))
|
|
37
42
|
websocket_channels = {
|
|
38
43
|
L2_BOOK: 'books',
|
|
39
|
-
TRADES: 'trades',
|
|
44
|
+
TRADES: 'trades-all',
|
|
40
45
|
TICKER: 'tickers',
|
|
41
46
|
FUNDING: 'funding-rate',
|
|
42
47
|
OPEN_INTEREST: 'open-interest',
|
|
43
48
|
LIQUIDATIONS: LIQUIDATIONS,
|
|
44
49
|
CANDLES: 'candle'
|
|
45
50
|
}
|
|
51
|
+
# instrument types a channel refuses
|
|
52
|
+
channel_excludes = {FUNDING: (SPOT, OPTION), OPEN_INTEREST: (SPOT,)}
|
|
46
53
|
websocket_endpoints = [
|
|
47
|
-
WebsocketEndpoint('wss://ws.okx.com/ws/v5/public', channel_filter=(websocket_channels[L2_BOOK], websocket_channels[
|
|
48
|
-
WebsocketEndpoint('wss://ws.okx.com/ws/v5/business', channel_filter=(websocket_channels[CANDLES],), options={'compression': None}),
|
|
54
|
+
WebsocketEndpoint('wss://ws.okx.com/ws/v5/public', channel_filter=(websocket_channels[L2_BOOK], websocket_channels[TICKER], websocket_channels[FUNDING], websocket_channels[OPEN_INTEREST], websocket_channels[LIQUIDATIONS]), options={'compression': None}),
|
|
55
|
+
WebsocketEndpoint('wss://ws.okx.com/ws/v5/business', channel_filter=(websocket_channels[CANDLES], websocket_channels[TRADES]), options={'compression': None}),
|
|
49
56
|
]
|
|
50
|
-
rest_endpoints = [RestEndpoint('https://www.okx.com', routes=Routes(['/api/v5/public/instruments?instType=SPOT', '/api/v5/public/instruments?instType=SWAP', '/api/v5/public/instruments?instType=FUTURES'], currencies='/api/v5/public/underlying?instType=OPTION', liquidations='/api/v5/public/liquidation-orders?instType={}&limit=100&state={}&uly={}'
|
|
57
|
+
rest_endpoints = [RestEndpoint('https://www.okx.com', routes=Routes(['/api/v5/public/instruments?instType=SPOT', '/api/v5/public/instruments?instType=SWAP', '/api/v5/public/instruments?instType=FUTURES'], currencies='/api/v5/public/underlying?instType=OPTION', liquidations='/api/v5/public/liquidation-orders?instType={}&limit=100&state={}&uly={}'))]
|
|
51
58
|
option_instruments = '/api/v5/public/instruments?instType=OPTION&uly={}'
|
|
52
59
|
default_option_underlyings = ('BTC-USD', 'ETH-USD')
|
|
53
60
|
request_limit = 20
|
|
@@ -108,17 +115,20 @@ class OKX(Feed):
|
|
|
108
115
|
ret[s.normalized] = e['instId']
|
|
109
116
|
info['tick_size'][s.normalized] = e['tickSz']
|
|
110
117
|
info['instrument_type'][s.normalized] = stype
|
|
118
|
+
if e.get('ctVal') and e.get('ctMult'):
|
|
119
|
+
# the size is in ctValCcy - the base coin on linear swaps, futures and options, USD on inverse
|
|
120
|
+
# swaps and futures (ctType says 'inverse' on BTC-settled options whose value is still in BTC)
|
|
121
|
+
info['contract_size'][s.normalized] = Decimal(e['ctVal']) * Decimal(e['ctMult'])
|
|
122
|
+
info['contract_currency'][s.normalized] = e['ctValCcy']
|
|
111
123
|
|
|
112
124
|
return ret, info
|
|
113
125
|
|
|
114
126
|
async def _liquidations(self, pairs: list):
|
|
115
|
-
last_update = defaultdict(dict)
|
|
116
127
|
"""
|
|
117
128
|
for PERP liquidations, the following arguments are required: uly, state
|
|
118
129
|
for FUTURES liquidations, the following arguments are required: uly, state, alias
|
|
119
130
|
FUTURES, MARGIN and OPTION liquidation request not currently supported by the below
|
|
120
131
|
"""
|
|
121
|
-
|
|
122
132
|
while True:
|
|
123
133
|
for pair in pairs:
|
|
124
134
|
if 'SWAP' in pair:
|
|
@@ -127,23 +137,20 @@ class OKX(Feed):
|
|
|
127
137
|
else:
|
|
128
138
|
continue
|
|
129
139
|
|
|
140
|
+
symbol = self.exchange_symbol_to_std_symbol(pair)
|
|
130
141
|
for status in (FILLED, UNFILLED):
|
|
131
|
-
|
|
142
|
+
address = self.rest_endpoints[0].route('liquidations', sandbox=self.sandbox).format(instrument_type, status, uly)
|
|
143
|
+
data = await self.http_conn.read(address)
|
|
132
144
|
data = json.loads(data, parse_float=Decimal)
|
|
133
145
|
timestamp = time.time()
|
|
134
146
|
if not data['data']:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if pair in last_update:
|
|
141
|
-
if entry == last_update[pair].get(status):
|
|
142
|
-
break
|
|
143
|
-
|
|
144
|
-
liq = Liquidation(
|
|
147
|
+
# the usual answer for unfilled liquidations, which fill within seconds
|
|
148
|
+
LOG.debug('%s: no liquidation data received for %s @ %s', self.id, pair, address)
|
|
149
|
+
|
|
150
|
+
liquidations = [
|
|
151
|
+
Liquidation(
|
|
145
152
|
self.id,
|
|
146
|
-
|
|
153
|
+
symbol,
|
|
147
154
|
BUY if entry['side'] == 'buy' else SELL,
|
|
148
155
|
Decimal(entry['sz']),
|
|
149
156
|
Decimal(entry['bkPx']),
|
|
@@ -152,22 +159,23 @@ class OKX(Feed):
|
|
|
152
159
|
self.timestamp_normalize(int(entry['ts'])),
|
|
153
160
|
raw=data
|
|
154
161
|
)
|
|
162
|
+
for entry in (data['data'][0]['details'] if data['data'] else [])
|
|
163
|
+
]
|
|
164
|
+
for liq in self.liquidation_watermark.batch(liquidations, symbol=symbol, status=status):
|
|
155
165
|
await self.callback(LIQUIDATIONS, liq, timestamp)
|
|
156
|
-
last_update[pair][status] = data['data'][0]['details'][0]
|
|
157
166
|
await asyncio.sleep(0.1)
|
|
158
167
|
await asyncio.sleep(60)
|
|
159
168
|
|
|
160
169
|
def __init__(self, **kwargs):
|
|
161
170
|
super().__init__(**kwargs)
|
|
162
|
-
self.__reset()
|
|
163
|
-
|
|
164
|
-
def __reset(self):
|
|
165
|
-
self._l2_book = {}
|
|
166
171
|
self.seq_no = {}
|
|
167
172
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
def _candle_stop(self, start: float) -> float:
|
|
174
|
+
if self.candle_interval == '1M':
|
|
175
|
+
opened = datetime.fromtimestamp(start, tz=self.CANDLE_TZ)
|
|
176
|
+
year, month = (opened.year + 1, 1) if opened.month == 12 else (opened.year, opened.month + 1)
|
|
177
|
+
return opened.replace(year=year, month=month).timestamp()
|
|
178
|
+
return start + self.candle_interval_map[self.candle_interval]
|
|
171
179
|
|
|
172
180
|
async def _candle(self, msg: dict, timestamp: float):
|
|
173
181
|
'''
|
|
@@ -190,9 +198,10 @@ class OKX(Feed):
|
|
|
190
198
|
]
|
|
191
199
|
]
|
|
192
200
|
}
|
|
201
|
+
|
|
202
|
+
the update carries no event time, so Candle.timestamp is None
|
|
193
203
|
'''
|
|
194
204
|
symbol = self.exchange_symbol_to_std_symbol(msg['arg']['instId'])
|
|
195
|
-
interval = self.candle_interval_map[self.candle_interval]
|
|
196
205
|
|
|
197
206
|
for entry in msg['data']:
|
|
198
207
|
closed = len(entry) > 8 and entry[8] == '1'
|
|
@@ -204,7 +213,7 @@ class OKX(Feed):
|
|
|
204
213
|
self.id,
|
|
205
214
|
symbol,
|
|
206
215
|
start,
|
|
207
|
-
start
|
|
216
|
+
self._candle_stop(start),
|
|
208
217
|
self.candle_interval,
|
|
209
218
|
None,
|
|
210
219
|
Decimal(entry[1]),
|
|
@@ -213,7 +222,7 @@ class OKX(Feed):
|
|
|
213
222
|
Decimal(entry[3]),
|
|
214
223
|
Decimal(entry[5]),
|
|
215
224
|
closed,
|
|
216
|
-
|
|
225
|
+
None,
|
|
217
226
|
raw=msg
|
|
218
227
|
)
|
|
219
228
|
await self.callback(CANDLES, candle, timestamp)
|
|
@@ -224,13 +233,15 @@ class OKX(Feed):
|
|
|
224
233
|
"""
|
|
225
234
|
pair = self.exchange_symbol_to_std_symbol(msg['arg']['instId'])
|
|
226
235
|
for update in msg['data']:
|
|
227
|
-
|
|
236
|
+
if not update['bidPx'] or not update['askPx']:
|
|
237
|
+
# a side of the book is empty
|
|
238
|
+
continue
|
|
228
239
|
t = Ticker(
|
|
229
240
|
self.id,
|
|
230
241
|
pair,
|
|
231
|
-
Decimal(update['bidPx'])
|
|
232
|
-
Decimal(update['askPx'])
|
|
233
|
-
|
|
242
|
+
Decimal(update['bidPx']),
|
|
243
|
+
Decimal(update['askPx']),
|
|
244
|
+
self.timestamp_normalize(int(update['ts'])),
|
|
234
245
|
raw=update
|
|
235
246
|
)
|
|
236
247
|
await self.callback(TICKER, t, timestamp)
|
|
@@ -268,17 +279,18 @@ class OKX(Feed):
|
|
|
268
279
|
"""
|
|
269
280
|
{
|
|
270
281
|
"arg": {
|
|
271
|
-
"channel": "trades",
|
|
272
|
-
"instId": "BTC-
|
|
282
|
+
"channel": "trades-all",
|
|
283
|
+
"instId": "BTC-USDT"
|
|
273
284
|
},
|
|
274
285
|
"data": [
|
|
275
286
|
{
|
|
276
|
-
"instId": "BTC-
|
|
277
|
-
"tradeId": "
|
|
278
|
-
"px": "
|
|
279
|
-
"sz": "
|
|
287
|
+
"instId": "BTC-USDT",
|
|
288
|
+
"tradeId": "1077229381",
|
|
289
|
+
"px": "77910.5",
|
|
290
|
+
"sz": "0.16",
|
|
280
291
|
"side": "buy",
|
|
281
|
-
"ts": "
|
|
292
|
+
"ts": "1793569925719",
|
|
293
|
+
"source": "0"
|
|
282
294
|
}
|
|
283
295
|
]
|
|
284
296
|
}
|
|
@@ -327,22 +339,6 @@ class OKX(Feed):
|
|
|
327
339
|
self.seq_no[pair] = sequence
|
|
328
340
|
return True
|
|
329
341
|
|
|
330
|
-
def _parse_snapshot(self, symbol: str, data) -> OrderBook:
|
|
331
|
-
response = json.loads(data, parse_float=Decimal)
|
|
332
|
-
if response.get('code') not in ('0', 0):
|
|
333
|
-
raise ValueError(f"{self.id}: book snapshot for {symbol} returned code {response.get('code')!r}: {response.get('msg')!r}")
|
|
334
|
-
if not response.get('data'):
|
|
335
|
-
raise ValueError(f'{self.id}: book snapshot for {symbol} carried no data - is the instrument listed?')
|
|
336
|
-
entry = response['data'][0]
|
|
337
|
-
|
|
338
|
-
bids = {Decimal(price): Decimal(size) for price, size, *_ in entry['bids']}
|
|
339
|
-
asks = {Decimal(price): Decimal(size) for price, size, *_ in entry['asks']}
|
|
340
|
-
book = OrderBook(self.id, symbol, max_depth=self.max_depth, bids=bids, asks=asks)
|
|
341
|
-
book.timestamp = self.timestamp_normalize(int(entry['ts'])) if entry.get('ts') else None
|
|
342
|
-
book.sequence_number = int(entry['seqId']) if entry.get('seqId') is not None else None
|
|
343
|
-
book.raw = entry
|
|
344
|
-
return book
|
|
345
|
-
|
|
346
342
|
async def _book(self, msg: dict, timestamp: float):
|
|
347
343
|
if msg['action'] == 'snapshot':
|
|
348
344
|
# snapshot
|
|
@@ -395,38 +391,50 @@ class OKX(Feed):
|
|
|
395
391
|
else:
|
|
396
392
|
LOG.warning("%s: Unhandled event %s", self.id, msg)
|
|
397
393
|
elif 'arg' in msg:
|
|
398
|
-
|
|
394
|
+
channel = msg['arg']['channel']
|
|
395
|
+
if channel == self.websocket_channels[L2_BOOK]:
|
|
399
396
|
await self._book(msg, timestamp)
|
|
400
|
-
elif self.websocket_channels[TICKER]
|
|
397
|
+
elif channel == self.websocket_channels[TICKER]:
|
|
401
398
|
await self._ticker(msg, timestamp)
|
|
402
|
-
elif self.websocket_channels[TRADES]
|
|
399
|
+
elif channel in (self.websocket_channels[TRADES], 'trades'):
|
|
400
|
+
# trades is the aggregated channel
|
|
403
401
|
await self._trade(msg, timestamp)
|
|
404
|
-
elif self.websocket_channels[CANDLES]
|
|
402
|
+
elif channel.startswith(self.websocket_channels[CANDLES]):
|
|
405
403
|
await self._candle(msg, timestamp)
|
|
406
|
-
elif self.websocket_channels[FUNDING]
|
|
404
|
+
elif channel == self.websocket_channels[FUNDING]:
|
|
407
405
|
await self._funding(msg, timestamp)
|
|
408
|
-
elif self.websocket_channels[OPEN_INTEREST]
|
|
406
|
+
elif channel == self.websocket_channels[OPEN_INTEREST]:
|
|
409
407
|
await self._open_interest(msg, timestamp)
|
|
410
408
|
else:
|
|
411
409
|
LOG.warning("%s: Unhandled message %s", self.id, msg)
|
|
412
410
|
|
|
413
411
|
async def subscribe(self, connection: AsyncConnection):
|
|
414
412
|
channels = []
|
|
415
|
-
for chan in connection.subscription:
|
|
413
|
+
for chan, pairs in connection.subscription.items():
|
|
416
414
|
if chan == LIQUIDATIONS:
|
|
417
|
-
self._spawn('liquidations', self._liquidations,
|
|
415
|
+
self._spawn('liquidations', self._liquidations, pairs)
|
|
418
416
|
continue
|
|
419
|
-
for pair in
|
|
417
|
+
for pair in self._supported(chan, pairs):
|
|
420
418
|
channels.append(self.build_subscription(chan, pair))
|
|
421
419
|
|
|
422
420
|
if channels:
|
|
423
421
|
await connection.write(json.dumps({"op": "subscribe", "args": channels}))
|
|
424
422
|
|
|
423
|
+
def _supported(self, channel: str, pairs: list) -> list:
|
|
424
|
+
'''
|
|
425
|
+
returns a list of the supported instruments for a channel
|
|
426
|
+
'''
|
|
427
|
+
excluded = self.channel_excludes.get(self.exchange_channel_to_std(channel), ())
|
|
428
|
+
if not excluded:
|
|
429
|
+
return pairs
|
|
430
|
+
kept, skipped = [], []
|
|
431
|
+
for pair in pairs:
|
|
432
|
+
(skipped if str_to_symbol(self.exchange_symbol_to_std_symbol(pair)).type in excluded else kept).append(pair)
|
|
433
|
+
if skipped:
|
|
434
|
+
LOG.warning('%s: %s does not exist for %s instruments, not subscribing %d symbols (%s%s)', self.id, channel, ', '.join(excluded), len(skipped), ', '.join(skipped[:5]), ', ...' if len(skipped) > 5 else '')
|
|
435
|
+
return kept
|
|
436
|
+
|
|
425
437
|
def build_subscription(self, channel: str, ticker: str) -> dict:
|
|
426
|
-
if channel
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
else:
|
|
430
|
-
subscription_dict = {"channel": channel,
|
|
431
|
-
"instId": ticker}
|
|
432
|
-
return subscription_dict
|
|
438
|
+
if channel == self.websocket_channels[CANDLES]:
|
|
439
|
+
channel = f'{channel}{self.candle_interval}'
|
|
440
|
+
return {'channel': channel, 'instId': ticker}
|
|
@@ -23,7 +23,7 @@ from cryptofeed.defines import CANDLES, FUNDING, INDEX, L1_BOOK, L2_BOOK, L3_BOO
|
|
|
23
23
|
from cryptofeed.exceptions import BidAskOverlapping
|
|
24
24
|
from cryptofeed.exchange import Exchange
|
|
25
25
|
from cryptofeed.types import OrderBook
|
|
26
|
-
from cryptofeed.util.history import CandleWatermark, TradeWatermark
|
|
26
|
+
from cryptofeed.util.history import CandleWatermark, LiquidationWatermark, TradeWatermark
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
LOG = logging.getLogger(__name__)
|
|
@@ -71,7 +71,7 @@ class Feed(Exchange):
|
|
|
71
71
|
async def keepalive(self, conn: AsyncConnection):
|
|
72
72
|
raise NotImplementedError(f'{self.id}: keepalive_interval is set but keepalive() is not implemented')
|
|
73
73
|
|
|
74
|
-
def __init__(self, candle_interval=None, candle_closed_only=True, trade_history=False, candle_history=False, timeout=120, timeout_interval=30, retries=15, symbols=None, channels=None, subscription=None, callbacks=None, max_depth=0, checksum_validation=None, cross_check=False, exceptions=None, log_message_on_error=False, delay_start=0, http_proxy: StrOrURL = None, shutdown_timeout=10.0, **kwargs):
|
|
74
|
+
def __init__(self, candle_interval=None, candle_closed_only=True, trade_history=False, candle_history=False, liquidation_history=False, timeout=120, timeout_interval=30, retries=15, symbols=None, channels=None, subscription=None, callbacks=None, max_depth=0, checksum_validation=None, cross_check=False, exceptions=None, log_message_on_error=False, delay_start=0, http_proxy: StrOrURL = None, shutdown_timeout=10.0, **kwargs):
|
|
75
75
|
"""
|
|
76
76
|
candle_interval: str
|
|
77
77
|
the candle interval. See the specific exchange to see what intervals they support.
|
|
@@ -83,6 +83,8 @@ class Feed(Exchange):
|
|
|
83
83
|
deliver historical trades sent on initial subscribe (not all exchange support this)
|
|
84
84
|
candle_history: bool
|
|
85
85
|
deliver historical candles sent on initial subscribe (not all exchange support this)
|
|
86
|
+
liquidation_history: bool
|
|
87
|
+
deliver historical liquidations sent on initial subscribe (not all exchanges support this)
|
|
86
88
|
timeout: int
|
|
87
89
|
wait time in seconds before feed is considered dead
|
|
88
90
|
Set to -1 for infinite. quiet_channels are ignored
|
|
@@ -132,6 +134,7 @@ class Feed(Exchange):
|
|
|
132
134
|
self.candle_closed_only = candle_closed_only
|
|
133
135
|
self.trade_watermark = TradeWatermark(trade_history)
|
|
134
136
|
self.candle_watermark = CandleWatermark(candle_history)
|
|
137
|
+
self.liquidation_watermark = LiquidationWatermark(liquidation_history)
|
|
135
138
|
self.shutdown_timeout = shutdown_timeout
|
|
136
139
|
self._conn_tg = None
|
|
137
140
|
self._spawned = set()
|
|
@@ -204,6 +207,14 @@ class Feed(Exchange):
|
|
|
204
207
|
def candle_history(self, value: bool):
|
|
205
208
|
self.candle_watermark.history = value
|
|
206
209
|
|
|
210
|
+
@property
|
|
211
|
+
def liquidation_history(self) -> bool:
|
|
212
|
+
return self.liquidation_watermark.history
|
|
213
|
+
|
|
214
|
+
@liquidation_history.setter
|
|
215
|
+
def liquidation_history(self, value: bool):
|
|
216
|
+
self.liquidation_watermark.history = value
|
|
217
|
+
|
|
207
218
|
def _build_subscription(self):
|
|
208
219
|
self._ensure_symbol_mapping()
|
|
209
220
|
|
|
@@ -12,7 +12,7 @@ class NBBO(Callback):
|
|
|
12
12
|
self.bids = {symbol: {} for symbol in symbols}
|
|
13
13
|
self.asks = {symbol: {} for symbol in symbols}
|
|
14
14
|
|
|
15
|
-
self.last_update =
|
|
15
|
+
self.last_update = {}
|
|
16
16
|
|
|
17
17
|
super(NBBO, self).__init__(callback)
|
|
18
18
|
|
|
@@ -36,9 +36,9 @@ class NBBO(Callback):
|
|
|
36
36
|
return
|
|
37
37
|
|
|
38
38
|
# only write updates when a best bid / best ask changes
|
|
39
|
-
if self.last_update == update:
|
|
39
|
+
if self.last_update.get(book.symbol) == update:
|
|
40
40
|
return
|
|
41
|
-
self.last_update = update
|
|
41
|
+
self.last_update[book.symbol] = update
|
|
42
42
|
|
|
43
43
|
bid, ask, bid_feed, ask_feed = update
|
|
44
44
|
await self.callback(book.symbol, bid['price'], bid['size'], ask['price'], ask['size'], bid_feed, ask_feed)
|
|
@@ -36,6 +36,9 @@ def _group(rows, stream, extra=None) -> dict:
|
|
|
36
36
|
|
|
37
37
|
class TradeWatermark:
|
|
38
38
|
# at a high level, this ensures that a feed delivers every trade once and only once, and in time ascending order
|
|
39
|
+
# one watermark per stream, the symbol for trades
|
|
40
|
+
_stream_of = staticmethod(_symbol)
|
|
41
|
+
|
|
39
42
|
def __init__(self, history: bool = False, key=None):
|
|
40
43
|
self.history = history
|
|
41
44
|
self.key = key
|
|
@@ -50,7 +53,7 @@ class TradeWatermark:
|
|
|
50
53
|
key = self.key or _trade_key
|
|
51
54
|
out = []
|
|
52
55
|
|
|
53
|
-
for sym, rows in _group(trades,
|
|
56
|
+
for sym, rows in _group(trades, self._stream_of, symbol).items():
|
|
54
57
|
rows.sort(key=lambda t: (t.timestamp, key(t)))
|
|
55
58
|
mark = self._marks.get(sym)
|
|
56
59
|
|
|
@@ -70,7 +73,7 @@ class TradeWatermark:
|
|
|
70
73
|
key = self.key or _trade_key
|
|
71
74
|
trades = sorted(trades, key=lambda t: (t.timestamp, key(t))) if sort else list(trades)
|
|
72
75
|
for trade in trades:
|
|
73
|
-
self._note(trade
|
|
76
|
+
self._note(self._stream_of(trade), trade, key)
|
|
74
77
|
return trades
|
|
75
78
|
|
|
76
79
|
def expect(self, symbols):
|
|
@@ -111,6 +114,29 @@ class TradeWatermark:
|
|
|
111
114
|
mark[1][key(trade)] += 1
|
|
112
115
|
|
|
113
116
|
|
|
117
|
+
def _liquidation_stream(liquidation):
|
|
118
|
+
return (liquidation.symbol, liquidation.status)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _liquidation_key(liquidation):
|
|
122
|
+
# liquidations carry no id
|
|
123
|
+
return (liquidation.side, liquidation.price, liquidation.quantity)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class LiquidationWatermark(TradeWatermark):
|
|
127
|
+
# the trade watermark per (symbol, status): a venue lists its filled and unfilled liquidations separately
|
|
128
|
+
_stream_of = staticmethod(_liquidation_stream)
|
|
129
|
+
|
|
130
|
+
def __init__(self, history: bool = False):
|
|
131
|
+
super().__init__(history, _liquidation_key)
|
|
132
|
+
|
|
133
|
+
def get(self, symbol, status):
|
|
134
|
+
return super().get((symbol, status))
|
|
135
|
+
|
|
136
|
+
def batch(self, liquidations, symbol=None, status=None) -> list:
|
|
137
|
+
return super().batch(liquidations, None if symbol is None else (symbol, status))
|
|
138
|
+
|
|
139
|
+
|
|
114
140
|
class CandleWatermark:
|
|
115
141
|
# at a high level, this ensures that a feed delivers every candle
|
|
116
142
|
# once and only once, and in time ascending order
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cryptofeed
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
4
4
|
Summary: Cryptocurrency Exchange Websocket Data Feed Handler
|
|
5
5
|
Author-email: Bryant Moscon <bmoscon@gmail.com>
|
|
6
6
|
License-Expression: AGPL-3.0-or-later
|
|
@@ -171,8 +171,8 @@ cryptofeed received the message that produced the callback. The objects are defi
|
|
|
171
171
|
set when the venue provides them.
|
|
172
172
|
* `Candle.closed` is `True` once the bar is final, by default only closed candles are delivered.
|
|
173
173
|
(`candle_closed_only=False` also delivers each update of the open bar).
|
|
174
|
-
* `trade_history` and `
|
|
175
|
-
subscribe is delivered, by default it is `False`.
|
|
174
|
+
* `trade_history`, `candle_history` and `liquidation_history` control whether the batch of recent trades, candles or
|
|
175
|
+
liquidations some venues send on subscribe is delivered, by default it is `False`.
|
|
176
176
|
|
|
177
177
|
## Backends
|
|
178
178
|
|
|
@@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"
|
|
|
9
9
|
|
|
10
10
|
[project]
|
|
11
11
|
name = "cryptofeed"
|
|
12
|
-
version = "3.0.
|
|
12
|
+
version = "3.0.3"
|
|
13
13
|
description = "Cryptocurrency Exchange Websocket Data Feed Handler"
|
|
14
14
|
readme = "README.md"
|
|
15
15
|
license = "AGPL-3.0-or-later"
|
|
@@ -97,7 +97,6 @@ define-macros = [["CYTHON_WITHOUT_ASSERTIONS", ""]]
|
|
|
97
97
|
|
|
98
98
|
[tool.cibuildwheel]
|
|
99
99
|
build = "cp313-* cp314-* cp314t-* cp315-* cp315t-*"
|
|
100
|
-
skip = "*musllinux*"
|
|
101
100
|
test-command = "python -c \"import sys, sysconfig; from cryptofeed import FeedHandler; from cryptofeed.types import COMPILED_WITH_ASSERTIONS; assert COMPILED_WITH_ASSERTIONS is False; assert not (sysconfig.get_config_var('Py_GIL_DISABLED') and sys._is_gil_enabled()), 'importing cryptofeed re-enabled the GIL'\""
|
|
102
101
|
|
|
103
102
|
[tool.cibuildwheel.linux]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|