exchanges-wrapper 2.1.3__tar.gz → 2.1.6__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 (20) hide show
  1. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/PKG-INFO +2 -2
  2. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/bybit_parser.py +3 -1
  4. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/client.py +8 -10
  5. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/exch_srv.py +16 -14
  6. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/http_client.py +31 -8
  7. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/web_sockets.py +25 -13
  8. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/pyproject.toml +1 -1
  9. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/README.md +0 -0
  11. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/bitfinex_parser.py +0 -0
  12. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/definitions.py +0 -0
  13. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/errors.py +0 -0
  14. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/events.py +0 -0
  15. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  16. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/huobi_parser.py +0 -0
  17. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/lib.py +0 -0
  18. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/martin/__init__.py +0 -0
  19. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/okx_parser.py +0 -0
  20. {exchanges_wrapper-2.1.3 → exchanges_wrapper-2.1.6}/exchanges_wrapper/proto/martin.proto +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.3
3
+ Version: 2.1.6
4
4
  Summary: REST API and WebSocket asyncio wrapper with grpc powered multiplexer server
5
5
  Author-email: Thomas Marchand <thomas.marchand@tuta.io>, Jerry Fedorenko <jerry.fedorenko@yahoo.com>
6
6
  Requires-Python: >=3.9
@@ -11,7 +11,7 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Classifier: Operating System :: Unix
12
12
  Classifier: Operating System :: Microsoft :: Windows
13
13
  Classifier: Operating System :: MacOS
14
- Requires-Dist: crypto-ws-api==2.0.6
14
+ Requires-Dist: crypto-ws-api==2.0.8
15
15
  Requires-Dist: grpcio==1.62.0
16
16
  Requires-Dist: pyotp~=2.9.0
17
17
  Requires-Dist: simplejson==3.19.2
@@ -12,7 +12,7 @@ __maintainer__ = "Jerry Fedorenko"
12
12
  __contact__ = "https://github.com/DogsTailFarmer"
13
13
  __email__ = "jerry.fedorenko@yahoo.com"
14
14
  __credits__ = ["https://github.com/DanyaSWorlD"]
15
- __version__ = "2.1.3"
15
+ __version__ = "2.1.6"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -450,7 +450,9 @@ def on_balance_update(data_in: list, ts: str, symbol: str, mode: str, uid=None)
450
450
 
451
451
  elif mode == 'universal':
452
452
  for i in data_in:
453
- if i['coin'] in symbol and 'UNIFIED' in (i['fromAccountType'], i['toAccountType']):
453
+ if i['coin'] in symbol and \
454
+ ((i['fromAccountType'] == 'UNIFIED' and i['fromMemberId'] == str(uid))
455
+ or (i['toAccountType'] == 'UNIFIED' and i['toMemberId'] == str(uid))):
454
456
  data_out.append(
455
457
  {
456
458
  i['transferId']: {
@@ -1078,15 +1078,15 @@ class Client:
1078
1078
  "This query requires an order_id on Bitfinex. Deletion by user number is not implemented."
1079
1079
  )
1080
1080
  params = {'id': order_id}
1081
- res = (
1082
- await self.user_wss_session.handle_request(trade_id, "oc", _params=params)
1083
- or await self.http.send_api_call(
1081
+ res = await self.user_wss_session.handle_request(trade_id, "oc", _params=params)
1082
+ if res is None or (res and isinstance(res, list) and res[6] == 'ERROR'):
1083
+ logger.debug(f"cancel_order.bitfinex {order_id}: res1: {res}")
1084
+ res = await self.http.send_api_call(
1084
1085
  "v2/auth/w/order/cancel",
1085
1086
  method="POST",
1086
1087
  signed=True,
1087
1088
  **params
1088
- )
1089
- )
1089
+ )
1090
1090
  if res and isinstance(res, list) and res[6] == 'SUCCESS':
1091
1091
  timeout = STATUS_TIMEOUT / 0.1
1092
1092
  while timeout:
@@ -1095,9 +1095,8 @@ class Client:
1095
1095
  binance_res = bfx.order(res[4], response_type=True, cancelled=True)
1096
1096
  break
1097
1097
  await asyncio.sleep(0.1)
1098
- logger.debug(f"cancel_order.bitfinex {order_id}: timeout: {timeout}")
1099
1098
  else:
1100
- logger.warning(f"cancel_order.bitfinex {order_id}: res: {res}")
1099
+ logger.debug(f"cancel_order.bitfinex {order_id}: res2: {res}")
1101
1100
  elif self.exchange == 'huobi':
1102
1101
  res = await self.http.send_api_call(
1103
1102
  f"v1/order/orders/{order_id}/submitcancel",
@@ -1110,7 +1109,6 @@ class Client:
1110
1109
  timeout -= 1
1111
1110
  await asyncio.sleep(0.1)
1112
1111
  binance_res = await self.fetch_order(trade_id, symbol, order_id=res, response_type=True)
1113
-
1114
1112
  elif self.exchange == 'okx':
1115
1113
  _symbol = self.symbol_to_okx(symbol)
1116
1114
  _queue = asyncio.Queue()
@@ -1188,9 +1186,10 @@ class Client:
1188
1186
  **params,
1189
1187
  )
1190
1188
  )
1191
- logger.debug(f"cancel_all_orders.res: {res}")
1192
1189
  if res and res[6] == 'SUCCESS':
1193
1190
  return bfx.orders(res[4], response_type=True, cancelled=True)
1191
+ logger.debug(f"bitfinex: cancel_all_orders.res: {res}")
1192
+
1194
1193
  elif self.exchange == 'huobi':
1195
1194
  orders = await self.fetch_open_orders(trade_id, symbol, receive_window=receive_window, response_type=True)
1196
1195
  orders_id = [str(order.get('orderId')) for order in orders]
@@ -1296,7 +1295,6 @@ class Client:
1296
1295
  method="POST",
1297
1296
  signed=True
1298
1297
  )
1299
- # logger.debug(f"fetch_open_orders.res: {res}")
1300
1298
  if res:
1301
1299
  binance_res = bfx.orders(res)
1302
1300
  elif self.exchange == 'huobi':
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ import grpclib.exceptions
3
4
 
4
- from exchanges_wrapper import __version__
5
+ from exchanges_wrapper import __version__ as ver_ew
6
+ from crypto_ws_api import __version__ as ver_cw
5
7
  import time
6
8
  import weakref
7
9
  import gc
@@ -129,9 +131,9 @@ class OpenClient:
129
131
  )
130
132
 
131
133
  @classmethod
132
- def get_client(cls, _id, raise_ex=True):
134
+ def get_client(cls, _id):
133
135
  res = next((client for client in cls.open_clients if id(client) == _id), None)
134
- if res is None and raise_ex:
136
+ if res is None:
135
137
  logger.warning(f"No client exist: {_id}")
136
138
  raise GRPCError(status=Status.UNAVAILABLE, message="No client exist")
137
139
  return res
@@ -172,8 +174,7 @@ class Martin(mr.MartinBase):
172
174
  if main_client.account_uid and main_client.account_id:
173
175
  open_client.client.main_account_uid = main_client.account_uid
174
176
  open_client.client.main_account_id = main_client.account_id
175
- logger.info(f"The values for main Huobi account were received and set:"
176
- f" UID: {main_client.account_uid} and account ID: {main_client.account_id}")
177
+ logger.info(f"Huobi UID: {main_client.account_uid} and account ID: {main_client.account_id}")
177
178
  else:
178
179
  logger.warning("No account IDs were received for the Huobi master account")
179
180
  await main_client.close()
@@ -196,7 +197,7 @@ class Martin(mr.MartinBase):
196
197
  Martin.rate_limiter = max(Martin.rate_limiter or 0, request.rate_limiter)
197
198
  return mr.OpenClientConnectionId(
198
199
  client_id=client_id,
199
- srv_version=__version__,
200
+ srv_version=f"{ver_cw}:{ver_ew}",
200
201
  exchange=open_client.client.exchange,
201
202
  real_market=open_client.real_market
202
203
  )
@@ -229,8 +230,9 @@ class Martin(mr.MartinBase):
229
230
  await self.rate_limit_control(open_client)
230
231
  try:
231
232
  res = await getattr(client, client_method_name)(**kwargs)
232
- except asyncio.CancelledError:
233
- pass # Task cancellation should not be logged as an error
233
+ except (asyncio.CancelledError, asyncio.exceptions.CancelledError):
234
+ msg = f"{msg_header} Server Shutdown"
235
+ raise GRPCError(status=Status.UNAVAILABLE, message=msg)
234
236
  except (errors.RateLimitReached, errors.QueryCanceled) as ex:
235
237
  Martin.rate_limit_reached_time = time.time()
236
238
  msg = f"{msg_header} RateLimitReached: {ex}"
@@ -653,7 +655,7 @@ class Martin(mr.MartinBase):
653
655
  return
654
656
  _events.append(_event)
655
657
  _get_event_from_queue = True
656
- except asyncio.TimeoutError:
658
+ except asyncio.exceptions.TimeoutError:
657
659
  _get_event_from_queue = False
658
660
 
659
661
  if client.exchange in ('bitfinex', 'huobi', 'bybit'):
@@ -700,7 +702,7 @@ class Martin(mr.MartinBase):
700
702
  else:
701
703
  event = vars(_event)
702
704
  event.pop('handlers', None)
703
- # logger.debug(f"OnOrderUpdate: {open_client.name}: {event}")
705
+ # logger.info(f"OnOrderUpdate: {open_client.name}: {event}")
704
706
  response.success = True
705
707
  response.result = json.dumps(event)
706
708
  yield response
@@ -800,11 +802,11 @@ class Martin(mr.MartinBase):
800
802
 
801
803
  async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
802
804
  response = mr.SimpleResponse()
803
- if open_client := OpenClient.get_client(request.client_id, raise_ex=False):
805
+ response.success = False
806
+ if open_client := OpenClient.get_client(request.client_id):
804
807
  client = open_client.client
805
808
  response.success = bool(client.data_streams.get(request.trade_id))
806
809
  else:
807
- response.success = False
808
810
  logger.warning(f"CheckStream request failed for {request.symbol}")
809
811
  return response
810
812
 
@@ -841,7 +843,7 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
841
843
  server = Server([Martin()])
842
844
  with graceful_exit([server]):
843
845
  await server.start(host, port)
844
- logger.info(f"Starting server v:{__version__} on {host}:{port}")
846
+ logger.info(f"Starting server v:{ver_cw}:{ver_ew} on {host}:{port}")
845
847
  await server.wait_closed()
846
848
 
847
849
  for oc in OpenClient.open_clients:
@@ -853,7 +855,7 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
853
855
  def main():
854
856
  try:
855
857
  asyncio.run(amain())
856
- except asyncio.exceptions.CancelledError:
858
+ except (asyncio.exceptions.CancelledError, grpclib.exceptions.StreamTerminatedError):
857
859
  pass # # Task cancellation should not be logged as an error
858
860
  except Exception as ex:
859
861
  print(f"Exception: {ex}")
@@ -18,9 +18,15 @@ from exchanges_wrapper.errors import (
18
18
  logger = logging.getLogger(__name__)
19
19
 
20
20
  AJ = 'application/json'
21
+ STATUS_BAD_REQUEST = 400
22
+ STATUS_UNAUTHORIZED = 401
23
+ STATUS_FORBIDDEN = 403
24
+ STATUS_I_AM_A_TEAPOT = 418 # HTTP status code for IPAddressBanned
25
+ ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW = "Timestamp for this request is outside of the recvWindow"
21
26
 
22
27
 
23
28
  class HttpClient:
29
+
24
30
  def __init__(self, **kwargs):
25
31
  self.api_key = kwargs.get('api_key')
26
32
  self.api_secret = kwargs.get('api_secret')
@@ -48,19 +54,36 @@ class HttpClient:
48
54
  payload = None
49
55
 
50
56
  if response.status >= 400:
51
- if response.status == 400 and payload and payload.get("error", str()) == "ERR_RATE_LIMIT":
52
- raise RateLimitReached(RateLimitReached.message)
53
- elif response.status == 400 and response.reason != "Bad Request":
54
- raise ExchangeError(f"ExchangeError: {payload}:{response.reason}:{response.text}")
55
- elif response.status == 403 and self.exchange != 'okx':
57
+ if response.status == STATUS_BAD_REQUEST:
58
+ if payload:
59
+ if payload.get("error", "") == "ERR_RATE_LIMIT":
60
+ raise RateLimitReached(RateLimitReached.message)
61
+ elif self.exchange == 'binance' and payload.get('code', 0) == -1021:
62
+ raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
63
+ else:
64
+ raise ExchangeError(f"ExchangeError: {payload}")
65
+ elif response.reason != "Bad Request":
66
+ raise ExchangeError(f"ExchangeError: {response.reason}:{response.text}")
67
+
68
+ elif (
69
+ response.status == STATUS_UNAUTHORIZED
70
+ and self.exchange == 'okx'
71
+ and payload
72
+ and payload.get('code', 0) == '50102'
73
+ ):
74
+ raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
75
+ elif response.status == STATUS_FORBIDDEN and self.exchange != 'okx':
56
76
  raise WAFLimitViolated(WAFLimitViolated.message)
57
- elif response.status == 418:
77
+ elif response.status == STATUS_I_AM_A_TEAPOT:
58
78
  raise IPAddressBanned(IPAddressBanned.message)
59
79
  else:
60
80
  raise HTTPError(f"Malformed request: {payload}:{response.reason}:{response.text}")
61
81
 
62
- if self.exchange == 'bybit' and payload and payload.get('retCode') == 0:
63
- return payload.get('result'), payload.get('time')
82
+ if self.exchange == 'bybit' and payload:
83
+ if payload.get('retCode') == 0:
84
+ return payload.get('result'), payload.get('time')
85
+ elif payload.get('retCode') == 10002:
86
+ raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
64
87
  elif self.exchange == 'huobi' and payload and (payload.get('status') == 'ok' or payload.get('ok')):
65
88
  return payload.get('data', payload.get('tick'))
66
89
  elif self.exchange == 'okx' and payload and payload.get('code') == '0':
@@ -8,8 +8,7 @@ from decimal import Decimal
8
8
  import gzip
9
9
  from datetime import datetime, timezone
10
10
  from urllib.parse import urlencode, urlparse
11
- import websockets.client
12
- from websockets import ConnectionClosed
11
+ import websockets
13
12
 
14
13
  import exchanges_wrapper.bitfinex_parser as bfx
15
14
  import exchanges_wrapper.huobi_parser as hbp
@@ -53,10 +52,15 @@ class EventsDataStream:
53
52
  self.wss_started = False
54
53
 
55
54
  async def start(self):
56
- async for self.websocket in websockets.client.connect(self.endpoint, logger=logger_ws):
55
+ ping_interval = None if self.exchange == 'huobi' else 20
56
+ async for self.websocket in websockets.connect(
57
+ self.endpoint,
58
+ logger=logger_ws,
59
+ ping_interval=ping_interval
60
+ ):
57
61
  try:
58
62
  await self.start_wss()
59
- except ConnectionClosed as ex:
63
+ except websockets.ConnectionClosed as ex:
60
64
  self.tasks_cancel()
61
65
  if ex.code == 4000:
62
66
  logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
@@ -113,7 +117,7 @@ class EventsDataStream:
113
117
  elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
114
118
  and not msg_data.get("success")):
115
119
  logger.warning(f"Reconnecting ByBit WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
116
- raise ConnectionClosed(None, None)
120
+ raise websockets.ConnectionClosed(None, None)
117
121
  else:
118
122
  logger.info(f"ByBit undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
119
123
  elif self.exchange == 'okx':
@@ -129,7 +133,7 @@ class EventsDataStream:
129
133
  self.wss_started = True
130
134
  elif msg_data.get("event") in ("login", "error") and msg_data.get("code") != "0":
131
135
  logger.warning(f"Reconnecting OKX WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
132
- raise ConnectionClosed(None, None)
136
+ raise websockets.ConnectionClosed(None, None)
133
137
  else:
134
138
  logger.debug(f"OKX undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
135
139
  elif self.exchange == 'bitfinex':
@@ -140,12 +144,12 @@ class EventsDataStream:
140
144
  if msg_data.get('platform') and msg_data.get('platform').get('status') != 1:
141
145
  logger.warning(f"Exchange in maintenance mode, trying reconnect. Exchange info: {msg}")
142
146
  await asyncio.sleep(60)
143
- raise ConnectionClosed(None, None)
147
+ raise websockets.ConnectionClosed(None, None)
144
148
  elif 'code' in msg_data:
145
149
  code = msg_data.get('code')
146
150
  if code == 10300:
147
151
  logger.warning('WSS Subscription failed (generic)')
148
- raise ConnectionClosed(None, None)
152
+ raise websockets.ConnectionClosed(None, None)
149
153
  elif code == 10301:
150
154
  logger.error('WSS Already subscribed')
151
155
  elif code == 10302:
@@ -154,11 +158,11 @@ class EventsDataStream:
154
158
  raise UserWarning('WSS Reached limit of open channels')
155
159
  elif code == 20051:
156
160
  logger.warning('WSS reconnection request received from exchange')
157
- raise ConnectionClosed(None, None)
161
+ raise websockets.ConnectionClosed(None, None)
158
162
  elif code == 20060:
159
163
  logger.info('WSS entering in maintenance mode, trying reconnect after 120s')
160
164
  await asyncio.sleep(120)
161
- raise ConnectionClosed(None, None)
165
+ raise websockets.ConnectionClosed(None, None)
162
166
  elif msg_data.get('event') == 'subscribed':
163
167
  chan_id = msg_data.get('chanId')
164
168
  logger.info(f"bitfinex, ch_type: {ch_type}, chan_id: {chan_id}")
@@ -178,8 +182,9 @@ class EventsDataStream:
178
182
  else:
179
183
  logger.debug(f"Bitfinex undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
180
184
  elif self.exchange == 'huobi':
181
- if msg_data.get('ping'):
182
- await self.websocket.send(json.dumps({"pong": msg_data.get('ping')}))
185
+ if ping := msg_data.get('ping'):
186
+ await self.websocket.send(json.dumps({"pong": ping}))
187
+ await asyncio.sleep(0)
183
188
  elif msg_data.get('action') == 'ping':
184
189
  pong = {
185
190
  "action": "pong",
@@ -188,6 +193,7 @@ class EventsDataStream:
188
193
  }
189
194
  }
190
195
  await self.websocket.send(json.dumps(pong))
196
+ await asyncio.sleep(0)
191
197
  elif msg_data.get('tick') or msg_data.get('data'):
192
198
  if ch_type == 'ticker':
193
199
  _price = msg_data.get('tick', {}).get('lastPrice', None)
@@ -203,13 +209,14 @@ class EventsDataStream:
203
209
  msg_data.get('code') == 500 and
204
210
  msg_data.get('message') == '系统异常:'):
205
211
  logger.warning(f"Reconnecting Huobi user {ch_type} channel")
206
- raise ConnectionClosed(None, None)
212
+ raise websockets.ConnectionClosed(None, None)
207
213
  else:
208
214
  logger.debug(f"Huobi undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
209
215
 
210
216
  async def ws_listener(self, request=None, symbol=None, ch_type=str()):
211
217
  if request:
212
218
  await self.websocket.send(json.dumps(request))
219
+ await asyncio.sleep(0)
213
220
  async for msg_data in self.websocket:
214
221
  await self._handle_messages(msg_data, symbol, ch_type)
215
222
 
@@ -370,6 +377,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
370
377
  "params": _params
371
378
  }
372
379
  await self.websocket.send(json.dumps(request))
380
+ await asyncio.sleep(0)
373
381
  await self._handle_messages(await self.websocket.recv(), symbol=self.symbol)
374
382
  #
375
383
  request = {
@@ -377,6 +385,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
377
385
  "ch": "accounts.update#2"
378
386
  }
379
387
  await self.websocket.send(json.dumps(request))
388
+ await asyncio.sleep(0)
380
389
  await self._handle_messages(await self.websocket.recv(), symbol=self.symbol)
381
390
  #
382
391
  request = {
@@ -384,6 +393,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
384
393
  "ch": f"orders#{self.symbol.lower()}"
385
394
  }
386
395
  await self.websocket.send(json.dumps(request))
396
+ await asyncio.sleep(0)
387
397
  await self._handle_messages(await self.websocket.recv(), symbol=self.symbol)
388
398
  #
389
399
  request = {
@@ -489,6 +499,7 @@ class OkxPrivateEventsDataStream(EventsDataStream):
489
499
  ]
490
500
  }
491
501
  await self.websocket.send(json.dumps(request))
502
+ await asyncio.sleep(0)
492
503
  await self._handle_messages(await self.websocket.recv())
493
504
  # Channel subscription
494
505
  request = {"op": 'subscribe',
@@ -539,6 +550,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
539
550
  "args": [self.client.api_key, ts, signature]
540
551
  }
541
552
  await self.websocket.send(json.dumps(request))
553
+ await asyncio.sleep(0)
542
554
  await self._handle_messages(await self.websocket.recv())
543
555
  # Channel subscription
544
556
  request = {
@@ -17,7 +17,7 @@ dynamic = ["version", "description"]
17
17
  requires-python = ">=3.9"
18
18
 
19
19
  dependencies = [
20
- "crypto-ws-api==2.0.6",
20
+ "crypto-ws-api==2.0.8",
21
21
  "grpcio==1.62.0",
22
22
  "pyotp~=2.9.0",
23
23
  "simplejson==3.19.2",