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.
Files changed (81) hide show
  1. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/CHANGES.md +12 -0
  2. {cryptofeed-3.0.2/cryptofeed.egg-info → cryptofeed-3.0.3}/PKG-INFO +3 -3
  3. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/README.md +2 -2
  4. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/recorder.py +1 -0
  5. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/cryptodotcom.py +3 -6
  6. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/gate.py +1 -1
  7. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/okx.py +82 -74
  8. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/feed.py +13 -2
  9. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/nbbo.py +3 -3
  10. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/history.py +28 -2
  11. {cryptofeed-3.0.2 → cryptofeed-3.0.3/cryptofeed.egg-info}/PKG-INFO +3 -3
  12. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/pyproject.toml +1 -2
  13. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/LICENSE +0 -0
  14. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/MANIFEST.in +0 -0
  15. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/__init__.py +0 -0
  16. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/_json.py +0 -0
  17. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/__init__.py +0 -0
  18. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/_line_protocol.py +0 -0
  19. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/backend.py +0 -0
  20. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/http.py +0 -0
  21. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/influxdb.py +0 -0
  22. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/kafka.py +0 -0
  23. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/mongo.py +0 -0
  24. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/postgres.py +0 -0
  25. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/quest.py +0 -0
  26. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/redis.py +0 -0
  27. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/socket.py +0 -0
  28. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/backends/zmq.py +0 -0
  29. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/callback.py +0 -0
  30. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/__init__.py +0 -0
  31. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/_zstd.py +0 -0
  32. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/pcap.py +0 -0
  33. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/capture/playback.py +0 -0
  34. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/config.py +0 -0
  35. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/connection.py +0 -0
  36. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/connection_handler.py +0 -0
  37. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/defines.py +0 -0
  38. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exceptions.py +0 -0
  39. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchange.py +0 -0
  40. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/__init__.py +0 -0
  41. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance.py +0 -0
  42. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance_delivery.py +0 -0
  43. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance_futures.py +0 -0
  44. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/binance_us.py +0 -0
  45. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitfinex.py +0 -0
  46. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitflyer.py +0 -0
  47. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitget.py +0 -0
  48. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bithumb.py +0 -0
  49. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bitstamp.py +0 -0
  50. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/bybit.py +0 -0
  51. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/coinbase.py +0 -0
  52. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/deribit.py +0 -0
  53. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/dydx.py +0 -0
  54. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/gate_futures.py +0 -0
  55. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/gemini.py +0 -0
  56. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/htx.py +0 -0
  57. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/htx_swap.py +0 -0
  58. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/hyperliquid.py +0 -0
  59. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/independent_reserve.py +0 -0
  60. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kraken.py +0 -0
  61. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kraken_futures.py +0 -0
  62. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kucoin.py +0 -0
  63. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/kucoin_futures.py +0 -0
  64. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/mexc.py +0 -0
  65. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/phemex.py +0 -0
  66. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/poloniex.py +0 -0
  67. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/exchanges/upbit.py +0 -0
  68. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/feedhandler.py +0 -0
  69. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/log.py +0 -0
  70. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/py.typed +0 -0
  71. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/symbols.py +0 -0
  72. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/types.pyi +0 -0
  73. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/types.pyx +0 -0
  74. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/__init__.py +0 -0
  75. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/snapshots.py +0 -0
  76. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed/util/time.py +0 -0
  77. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/SOURCES.txt +0 -0
  78. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/dependency_links.txt +0 -0
  79. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/requires.txt +0 -0
  80. {cryptofeed-3.0.2 → cryptofeed-3.0.3}/cryptofeed.egg-info/top_level.txt +0 -0
  81. {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.2
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 `candle_history` control whether the batch of recent trades or candles some venues send on
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 `candle_history` control whether the batch of recent trades or candles some venues send on
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
- if wants_ticker:
208
- # an instrument with one side of the book empty omits that side's price
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']['v']),
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
- candle_interval_map = {'1M': 2630000, '1W': 604800, '1D': 86400, '12H': 43200, '6H': 21600, '4H': 14400, '2H': 7200, '1H': 3600, '30m': 1800, '15m': 900, '5m': 300, '3m': 180, '1m': 60}
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[TRADES], websocket_channels[TICKER], websocket_channels[FUNDING], websocket_channels[OPEN_INTEREST], websocket_channels[LIQUIDATIONS]), options={'compression': None}),
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={}', l2book='/api/v5/market/books?instId={}&sz={}'))]
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
- data = await self.http_conn.read(self.rest_endpoints[0].route('liquidations', sandbox=self.sandbox).format(instrument_type, status, uly))
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
- LOG.info('%s: no liquidation data received for %s @ %s', self.id, pair, self.rest_endpoints[0].route('liquidations', sandbox=self.sandbox).format(instrument_type, status, uly))
136
- continue
137
- if len(data['data'][0]['details']) == 0 or (len(data['data'][0]['details']) > 0 and last_update.get(pair) == data['data'][0]['details'][0]):
138
- continue
139
- for entry in data['data'][0]['details']:
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
- self.exchange_symbol_to_std_symbol(pair),
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
- @classmethod
169
- def instrument_type(cls, symbol: str):
170
- return cls.info()['instrument_type'][symbol]
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 + interval,
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
- timestamp,
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
- update_timestamp = self.timestamp_normalize(int(update['ts']))
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']) if update['bidPx'] else Decimal(0),
232
- Decimal(update['askPx']) if update['askPx'] else Decimal(0),
233
- update_timestamp,
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-USD-191227"
282
+ "channel": "trades-all",
283
+ "instId": "BTC-USDT"
273
284
  },
274
285
  "data": [
275
286
  {
276
- "instId": "BTC-USD-191227",
277
- "tradeId": "9",
278
- "px": "0.016",
279
- "sz": "50",
287
+ "instId": "BTC-USDT",
288
+ "tradeId": "1077229381",
289
+ "px": "77910.5",
290
+ "sz": "0.16",
280
291
  "side": "buy",
281
- "ts": "1597026383085"
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
- if self.websocket_channels[L2_BOOK] in msg['arg']['channel']:
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] in msg['arg']['channel']:
397
+ elif channel == self.websocket_channels[TICKER]:
401
398
  await self._ticker(msg, timestamp)
402
- elif self.websocket_channels[TRADES] in msg['arg']['channel']:
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] in msg['arg']['channel']:
402
+ elif channel.startswith(self.websocket_channels[CANDLES]):
405
403
  await self._candle(msg, timestamp)
406
- elif self.websocket_channels[FUNDING] in msg['arg']['channel']:
404
+ elif channel == self.websocket_channels[FUNDING]:
407
405
  await self._funding(msg, timestamp)
408
- elif self.websocket_channels[OPEN_INTEREST] in msg['arg']['channel']:
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, connection.subscription[chan])
415
+ self._spawn('liquidations', self._liquidations, pairs)
418
416
  continue
419
- for pair in connection.subscription[chan]:
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 in ['candle']:
427
- subscription_dict = {"channel": f"{channel}{self.candle_interval}",
428
- "instId": ticker}
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 = None
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, _symbol, symbol).items():
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.symbol, trade, key)
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.2
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 `candle_history` control whether the batch of recent trades or candles some venues send on
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.2"
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