exchanges-wrapper 2.1.40__tar.gz → 2.1.42__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.40 → exchanges_wrapper-2.1.42}/PKG-INFO +8 -5
  2. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/README.md +4 -1
  3. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/__init__.py +14 -2
  4. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/client.py +49 -31
  5. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/exch_srv.py +24 -24
  6. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/http_client.py +24 -5
  7. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/lib.py +1 -1
  8. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/bitfinex.py +29 -11
  9. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/bybit.py +2 -2
  10. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/okx.py +20 -15
  11. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/web_sockets.py +96 -70
  12. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/pyproject.toml +7 -4
  13. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/LICENSE.md +0 -0
  14. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/definitions.py +0 -0
  15. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/errors.py +0 -0
  16. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/events.py +0 -0
  17. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  18. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/martin/__init__.py +0 -0
  19. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/parsers/huobi.py +0 -0
  20. {exchanges_wrapper-2.1.40 → exchanges_wrapper-2.1.42}/exchanges_wrapper/proto/martin.proto +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.40
3
+ Version: 2.1.42
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.10
@@ -12,10 +12,10 @@ Classifier: Operating System :: Unix
12
12
  Classifier: Operating System :: Microsoft :: Windows
13
13
  Classifier: Operating System :: MacOS
14
14
  License-File: LICENSE.md
15
- Requires-Dist: crypto-ws-api==2.0.20
15
+ Requires-Dist: crypto-ws-api==2.1.3
16
16
  Requires-Dist: pyotp==2.9.0
17
- Requires-Dist: simplejson==3.20.1
18
- Requires-Dist: aiohttp~=3.12.13
17
+ Requires-Dist: simplejson==3.20.2
18
+ Requires-Dist: aiohttp~=3.13.0
19
19
  Requires-Dist: expiringdict~=1.2.2
20
20
  Requires-Dist: betterproto==2.0.0b7
21
21
  Requires-Dist: grpclib~=0.4.8
@@ -28,12 +28,15 @@ Project-URL: Source, https://github.com/DogsTailFarmer/exchanges-wrapper
28
28
  <h3 align="center">For SPOT markets</h3>
29
29
 
30
30
  ***
31
- <a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
31
+ <h1 align="center"><a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
32
32
  <a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=resolved+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
33
33
  <a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=active+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
34
34
  <a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_exchanges-wrapper" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_exchanges-wrapper&metric=alert_status"/></a>
35
35
  <a href="https://pepy.tech/project/exchanges-wrapper" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/exchanges-wrapper/month"/></a>
36
+ </h1>
37
+
36
38
  ***
39
+
37
40
  On `Binance`: now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
38
41
 
39
42
  From `2.1.34` must be updated `exch_srv_cfg.toml` from `exchanges_wrapper/exch_srv_cfg.toml.template`
@@ -5,12 +5,15 @@
5
5
  <h3 align="center">For SPOT markets</h3>
6
6
 
7
7
  ***
8
- <a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
8
+ <h1 align="center"><a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
9
9
  <a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=resolved+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
10
10
  <a href="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper/?ref=repository-badge}" target="_blank"><img alt="DeepSource" title="DeepSource" src="https://deepsource.io/gh/DogsTailFarmer/exchanges-wrapper.svg/?label=active+issues&token=XuG5PmzMiKlDL921-qREIuX_"/></a>
11
11
  <a href="https://sonarcloud.io/summary/new_code?id=DogsTailFarmer_exchanges-wrapper" target="_blank"><img alt="sonarcloud" title="sonarcloud" src="https://sonarcloud.io/api/project_badges/measure?project=DogsTailFarmer_exchanges-wrapper&metric=alert_status"/></a>
12
12
  <a href="https://pepy.tech/project/exchanges-wrapper" target="_blank"><img alt="Downloads" title="Downloads" src="https://static.pepy.tech/badge/exchanges-wrapper/month"/></a>
13
+ </h1>
14
+
13
15
  ***
16
+
14
17
  On `Binance`: now API key type [Ed25519](https://www.binance.com/en/support/faq/detail/6b9a63f1e3384cf48a2eedb82767a69a) is used instead of `HMAC`
15
18
 
16
19
  From `2.1.34` must be updated `exch_srv_cfg.toml` from `exchanges_wrapper/exch_srv_cfg.toml.template`
@@ -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.40"
15
+ __version__ = "2.1.42"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -23,9 +23,21 @@ from grpclib.utils import graceful_exit
23
23
  from grpclib import exceptions
24
24
 
25
25
  __all__ = [
26
- 'Server', 'GRPCError', 'Status', 'Channel', 'graceful_exit', 'exceptions'
26
+ '__version__',
27
+ 'Server',
28
+ 'GRPCError',
29
+ 'Status',
30
+ 'Channel',
31
+ 'graceful_exit',
32
+ 'exceptions',
33
+ 'LOG_PATH',
34
+ 'WORK_PATH',
35
+ 'LOG_FILE',
36
+ 'CONFIG_FILE',
37
+ 'DEBUG_LOG'
27
38
  ]
28
39
 
40
+ DEBUG_LOG = 'debug' # The exchange for which log files, separated by trade_id with DEBUG level, will be generated
29
41
  WORK_PATH = Path(Path.home(), ".MartinBinance")
30
42
  CONFIG_PATH = Path(WORK_PATH, "config")
31
43
  CONFIG_FILE = Path(CONFIG_PATH, "exch_srv_cfg.toml")
@@ -1,5 +1,5 @@
1
1
  from enum import Enum
2
- from typing import Union
2
+ from typing import Union, Dict
3
3
  import decimal
4
4
  import math
5
5
  import asyncio
@@ -25,16 +25,18 @@ import exchanges_wrapper.parsers.bitfinex as bfx
25
25
  import exchanges_wrapper.parsers.huobi as hbp
26
26
  import exchanges_wrapper.parsers.okx as okx
27
27
  import exchanges_wrapper.parsers.bybit as bbt
28
- from crypto_ws_api.ws_session import UserWSSession
28
+ from crypto_ws_api.ws_session import UserWSSession, tasks_manage, tasks_cancel
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
32
32
  STATUS_TIMEOUT = 5 # sec, also use for lifetime limit for inactive order (Bitfinex) as 60 * STATUS_TIMEOUT
33
33
  ORDER_ENDPOINT = "/api/v3/order"
34
34
 
35
+
35
36
  def fallback_warning(exchange, symbol=None):
36
37
  logger.warning(f"Called by: {inspect.stack()[1][3]}: {exchange}:{symbol}: Fallback to HTTP API call")
37
38
 
39
+
38
40
  def truncate(f, n):
39
41
  return math.floor(f * 10 ** n) / 10 ** n
40
42
 
@@ -44,6 +46,19 @@ def any2str(_x) -> str:
44
46
 
45
47
 
46
48
  class Client:
49
+ __slots__ = (
50
+ 'exchange', 'sub_account', 'test_net', 'api_key', 'api_secret',
51
+ 'passphrase', 'endpoint_api_public', 'endpoint_ws_public',
52
+ 'endpoint_api_auth', 'endpoint_ws_auth', 'endpoint_ws_api',
53
+ 'ws_add_on', 'master_email', 'master_name', 'two_fa', 'http',
54
+ 'user_session', 'loaded', 'symbols', 'highest_precision',
55
+ 'rate_limits', 'data_streams', 'active_orders', 'wss_buffer',
56
+ 'stream_queue', 'on_order_update_queues', 'account_id',
57
+ 'account_uid', 'main_account_id', 'main_account_uid',
58
+ 'ledgers_id', 'ts_start', 'tasks', 'request_event',
59
+ '_events'
60
+ )
61
+
47
62
  def __init__(self, acc: dict):
48
63
  self.exchange = acc['exchange']
49
64
  self.sub_account = acc['sub_account']
@@ -98,15 +113,10 @@ class Client:
98
113
  self.main_account_uid = None
99
114
  self.ledgers_id = []
100
115
  self.ts_start = {}
101
- self.tasks = set()
116
+ self.tasks: dict[str, set] = {}
102
117
  self.request_event = asyncio.Event()
103
118
  self.request_event.set()
104
119
 
105
- def tasks_manage(self, coro):
106
- _t = asyncio.create_task(coro)
107
- self.tasks.add(_t)
108
- _t.add_done_callback(self.tasks.discard)
109
-
110
120
  async def fetch_object(self, key):
111
121
  res = None
112
122
  while res is None:
@@ -144,8 +154,8 @@ class Client:
144
154
  logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
145
155
 
146
156
  async def close(self):
147
- if self.http and self.http.session:
148
- await self.http.session.close()
157
+ if self.http:
158
+ await self.http.close_session()
149
159
 
150
160
  @property
151
161
  def events(self):
@@ -154,9 +164,8 @@ class Client:
154
164
  self._events = Events() # skipcq: PYL-W0201
155
165
  return self._events
156
166
 
157
- async def start_user_events_listener(self, _trade_id, symbol):
167
+ def start_user_events_listener(self, _trade_id, symbol):
158
168
  logger.info(f"Start '{self.exchange}' user events listener for {_trade_id}")
159
- user_data_stream = None
160
169
  if self.exchange == 'binance':
161
170
  user_data_stream = UserEventsDataStream(self, self.endpoint_ws_api, self.exchange, _trade_id)
162
171
  elif self.exchange == 'bitfinex':
@@ -173,24 +182,27 @@ class Client:
173
182
  )
174
183
  elif self.exchange == 'bybit':
175
184
  user_data_stream = BBTPrivateEventsDataStream(self, self.endpoint_ws_auth, self.exchange, _trade_id)
176
- if user_data_stream:
177
- self.data_streams[_trade_id] |= {user_data_stream}
178
- self.tasks_manage(user_data_stream.start())
179
- timeout = STATUS_TIMEOUT / 0.1
180
- while not user_data_stream.wss_started:
181
- timeout -= 1
182
- if not timeout:
183
- logger.warning(f"{self.exchange} user WSS start timeout reached for {_trade_id}")
184
- break
185
- await asyncio.sleep(0.1)
185
+ else:
186
+ raise UserWarning(f"User Data Stream: exchange {self.exchange} not serviced")
187
+
188
+ self.data_streams[_trade_id] |= {user_data_stream}
189
+
190
+ trade_tasks = self.tasks.pop(_trade_id, set())
191
+ tasks_manage(trade_tasks, user_data_stream.start(), f"user_data_stream-{self.exchange}-{_trade_id}")
192
+ self.tasks[_trade_id] = trade_tasks
186
193
 
187
194
  def start_market_events_listener(self, _trade_id):
188
195
  _events = self.events.registered_streams.get(self.exchange, {}).get(_trade_id, set())
189
196
  if self.exchange == 'binance':
190
197
  market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
191
198
  self.data_streams[_trade_id] |= {market_data_stream}
192
- self.tasks_manage(market_data_stream.start())
199
+
200
+ trade_tasks = self.tasks.pop(_trade_id, set())
201
+ tasks_manage(trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{_trade_id}")
202
+ self.tasks[_trade_id] = trade_tasks
203
+
193
204
  else:
205
+ trade_tasks = self.tasks.pop(_trade_id, set())
194
206
  for channel in _events:
195
207
  # https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url
196
208
  if self.exchange == 'okx' and 'kline' in channel:
@@ -200,15 +212,21 @@ class Client:
200
212
  #
201
213
  market_data_stream = MarketEventsDataStream(self, _endpoint, self.exchange, _trade_id, channel)
202
214
  self.data_streams[_trade_id] |= {market_data_stream}
203
- self.tasks_manage(market_data_stream.start())
215
+ tasks_manage(
216
+ trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{channel}-{_trade_id}"
217
+ )
218
+ self.tasks[_trade_id] = trade_tasks
204
219
 
205
220
  async def stop_events_listener(self, _trade_id):
206
221
  logger.info(f"Stop events listener data streams for {_trade_id}")
207
222
  stopped_data_stream = self.data_streams.pop(_trade_id, set())
208
223
  for data_stream in stopped_data_stream:
209
224
  await data_stream.stop()
225
+ if trade_tasks := self.tasks.pop(_trade_id, set()):
226
+ await tasks_cancel(trade_tasks, _logger=logger)
227
+
210
228
  if self.user_session:
211
- await self.user_session.stop()
229
+ await self.user_session.stop(_trade_id)
212
230
 
213
231
  def assert_symbol_exists(self, symbol):
214
232
  if self.loaded and symbol not in self.symbols:
@@ -587,7 +605,7 @@ class Client:
587
605
  "/api/v3/klines", params=params, signed=False
588
606
  )
589
607
  elif self.exchange == 'bitfinex':
590
- params = {'limit': limit, 'sort': -1}
608
+ params: Dict[str, Union[str, int]] = {'limit': limit, 'sort': -1}
591
609
  if start_time:
592
610
  params["start"] = str(start_time)
593
611
  if end_time:
@@ -835,7 +853,7 @@ class Client:
835
853
  **params,
836
854
  )
837
855
  if res:
838
- timeout = STATUS_TIMEOUT / 0.1
856
+ timeout = int(STATUS_TIMEOUT / 0.1)
839
857
  while not self.active_orders.get(int(res)) and timeout:
840
858
  timeout -= 1
841
859
  await asyncio.sleep(0.1)
@@ -908,7 +926,7 @@ class Client:
908
926
  )
909
927
  if b_res is None:
910
928
  fallback_warning(self.exchange, symbol)
911
- b_res = await self.http.send_api_call(
929
+ b_res = await self.http.send_api_call(
912
930
  ORDER_ENDPOINT,
913
931
  params=params,
914
932
  signed=True,
@@ -1030,7 +1048,7 @@ class Client:
1030
1048
  **params
1031
1049
  )
1032
1050
  if res and isinstance(res, list) and res[6] == 'SUCCESS':
1033
- timeout = STATUS_TIMEOUT / 0.1
1051
+ timeout = int(STATUS_TIMEOUT / 0.1)
1034
1052
  while timeout:
1035
1053
  timeout -= 1
1036
1054
  if self.active_orders.get(order_id, {}).get('cancelled', False):
@@ -1046,7 +1064,7 @@ class Client:
1046
1064
  signed=True
1047
1065
  )
1048
1066
  if res:
1049
- timeout = STATUS_TIMEOUT / 0.1
1067
+ timeout = int(STATUS_TIMEOUT / 0.1)
1050
1068
  while not self.active_orders.get(order_id, {}).get('cancelled', False) and timeout:
1051
1069
  timeout -= 1
1052
1070
  await asyncio.sleep(0.1)
@@ -1182,7 +1200,7 @@ class Client:
1182
1200
  signed=True,
1183
1201
  data=params,
1184
1202
  )
1185
- ids_canceled = [int(ordr['ordId']) for ordr in res if ordr['sCode'] == '0']
1203
+ ids_canceled = [int(order['ordId']) for order in res if order['sCode'] == '0']
1186
1204
  orders_canceled[:] = [i for i in orders_canceled if i['orderId'] in ids_canceled]
1187
1205
  binance_res.extend(orders_canceled)
1188
1206
  elif self.exchange == 'bybit':
@@ -4,7 +4,9 @@ from typing import Any, AsyncGenerator
4
4
 
5
5
  import grpclib.exceptions
6
6
 
7
+ # noinspection PyPep8Naming
7
8
  from exchanges_wrapper import __version__ as VER_EW
9
+ # noinspection PyPep8Naming
8
10
  from crypto_ws_api import __version__ as VER_CW
9
11
  import time
10
12
  import weakref
@@ -27,9 +29,13 @@ from exchanges_wrapper.lib import (
27
29
  REST_RATE_LIMIT_INTERVAL,
28
30
  FILTER_TYPE_MAP,
29
31
  )
30
- from exchanges_wrapper.martin import StreamResponse, SimpleResponse, OnKlinesUpdateResponse, OnTickerUpdateResponse, \
31
- FetchOrderBookResponse
32
-
32
+ from exchanges_wrapper.martin import (
33
+ StreamResponse,
34
+ SimpleResponse,
35
+ OnKlinesUpdateResponse,
36
+ OnTickerUpdateResponse,
37
+ FetchOrderBookResponse,
38
+ )
33
39
  #
34
40
  HEARTBEAT = 1 # sec
35
41
  MAX_QUEUE_SIZE = 100
@@ -136,10 +142,8 @@ class Martin(mr.MartinBase):
136
142
 
137
143
  try:
138
144
  await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
139
- except asyncio.CancelledError:
140
- pass # Task cancellation should not be logged as an error
141
145
  except asyncio.exceptions.TimeoutError:
142
- await OpenClient.get_client(client_id).client.http.session.close()
146
+ await OpenClient.get_client(client_id).client.http.close_session()
143
147
  OpenClient.remove_client(request.account_name)
144
148
  raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
145
149
  except Exception as ex:
@@ -186,7 +190,7 @@ class Martin(mr.MartinBase):
186
190
 
187
191
  try:
188
192
  res = await asyncio.wait_for(getattr(client, client_method_name)(**kwargs), timeout=90)
189
- except asyncio.exceptions.CancelledError:
193
+ except KeyboardInterrupt:
190
194
  raise GRPCError(status=Status.UNAVAILABLE, message=f"{msg_header} Server Shutdown")
191
195
  except asyncio.exceptions.TimeoutError:
192
196
  self.log_and_raise_grpc_error(msg_header, Status.DEADLINE_EXCEEDED, "timeout error")
@@ -227,7 +231,7 @@ class Martin(mr.MartinBase):
227
231
  server_time = res.get('serverTime')
228
232
  return mr.FetchServerTimeResponse(server_time=server_time)
229
233
 
230
- async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse():
234
+ async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse:
231
235
  res, _, _ = await self.send_request('one_click_arrival_deposit', request, tx_id=request.symbol)
232
236
  return mr.SimpleResponse(success=True, result=json.dumps(str(res)))
233
237
 
@@ -297,7 +301,7 @@ class Martin(mr.MartinBase):
297
301
  await _queue.put(weakref.ref(event)())
298
302
  logger.debug(f"{msg_header}: {trades}")
299
303
 
300
- async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse():
304
+ async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse:
301
305
  response = mr.SimpleResponse()
302
306
 
303
307
  res, _, _ = await self.send_request(
@@ -754,7 +758,7 @@ class Martin(mr.MartinBase):
754
758
  )
755
759
  logger.info(f"Start WS streams for {open_client.name}")
756
760
  client.start_market_events_listener(request.trade_id)
757
- await client.start_user_events_listener(request.trade_id, request.symbol)
761
+ client.start_user_events_listener(request.trade_id, request.symbol)
758
762
  response.success = True
759
763
  return response
760
764
 
@@ -763,7 +767,7 @@ class Martin(mr.MartinBase):
763
767
  if open_client := OpenClient.get_client(request.client_id):
764
768
  client = open_client.client
765
769
  logger.info(f"StopStream request for {request.symbol} on {client.exchange}")
766
- await stop_stream(client, request.trade_id)
770
+ await stop_stream_ex(client, request.trade_id)
767
771
  response.success = True
768
772
  else:
769
773
  response.success = False
@@ -780,13 +784,13 @@ class Martin(mr.MartinBase):
780
784
  return response
781
785
 
782
786
  async def client_restart(self, request: mr.MarketRequest) -> mr.SimpleResponse:
783
- if session := OpenClient.get_client(request.client_id).client.http.session:
784
- await session.close()
787
+ if session := OpenClient.get_client(request.client_id).client.http:
788
+ await session.close_session()
785
789
  OpenClient.remove_client(request.account_name)
786
790
  return mr.SimpleResponse(success=True)
787
791
 
788
792
 
789
- async def stop_stream(client, trade_id):
793
+ async def stop_stream_ex(client, trade_id):
790
794
  await client.stop_events_listener(trade_id)
791
795
  client.events.unregister(client.exchange, trade_id)
792
796
  [await _queue.put(trade_id) for _queue in client.stream_queue.get(trade_id, [])]
@@ -802,10 +806,7 @@ async def event_handler(_queue, client, trade_id, _event_type, event):
802
806
  except asyncio.QueueFull:
803
807
  logger.warning(f"For {_event_type} asyncio queue full and wold be closed")
804
808
  client.stream_queue.get(trade_id, set()).discard(_queue)
805
- await stop_stream(client, trade_id)
806
- global MAX_QUEUE_SIZE
807
- MAX_QUEUE_SIZE += int(MAX_QUEUE_SIZE / 10)
808
- logger.info(f"MAX_QUEUE_SIZE was updated: new value is {MAX_QUEUE_SIZE}")
809
+ await stop_stream_ex(client, trade_id)
809
810
 
810
811
 
811
812
  def is_port_in_use(port: int) -> bool:
@@ -825,8 +826,8 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
825
826
  await server.wait_closed()
826
827
 
827
828
  for oc in OpenClient.open_clients:
828
- if oc.client.http.session:
829
- await oc.client.http.session.close()
829
+ if oc.client.http:
830
+ await oc.client.http.close_session()
830
831
 
831
832
  [task.cancel() for task in asyncio.all_tasks() if not task.done() and task is not asyncio.current_task()]
832
833
 
@@ -834,12 +835,11 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
834
835
  def main():
835
836
  try:
836
837
  asyncio.run(amain())
837
- except (asyncio.exceptions.CancelledError, grpclib.exceptions.StreamTerminatedError):
838
+ except grpclib.exceptions.StreamTerminatedError:
838
839
  pass # Task cancellation should not be logged as an error
839
- except Exception as ex:
840
- print(f"Exception: {ex}")
840
+ except Exception as expt:
841
+ print(f"Exception: {expt}")
841
842
  print(traceback.format_exc())
842
843
 
843
-
844
844
  if __name__ == '__main__':
845
845
  main()
@@ -29,6 +29,21 @@ ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW = "Timestamp for this request is outside of th
29
29
  TIMEOUT = aiohttp.ClientTimeout(total=60)
30
30
 
31
31
  class HttpClient:
32
+ __slots__ = (
33
+ 'api_key',
34
+ 'api_secret',
35
+ 'passphrase',
36
+ 'endpoint',
37
+ 'exchange',
38
+ 'test_net',
39
+ 'rate_limit_reached',
40
+ 'rest_cycle_lock',
41
+ 'session',
42
+ '_session_mutex',
43
+ 'ex_imps',
44
+ 'rate_limit_handler'
45
+ )
46
+
32
47
  def __init__(self, params: dict):
33
48
  self.api_key = params['api_key']
34
49
  self.api_secret = params['api_secret']
@@ -54,9 +69,14 @@ class HttpClient:
54
69
  }
55
70
 
56
71
  async def _create_session_if_required(self):
57
- if self.session is None or self.session.closed:
72
+ if not self.session:
58
73
  async with self._session_mutex:
59
- self.session = aiohttp.ClientSession(timeout=TIMEOUT)
74
+ self.session = aiohttp.ClientSession(trust_env=True, timeout=TIMEOUT)
75
+
76
+ async def close_session(self):
77
+ if self.session:
78
+ await self.session.close()
79
+ self.session = None
60
80
 
61
81
  async def handle_errors(self, response, path=None):
62
82
  if response.status >= 500:
@@ -141,13 +161,12 @@ class HttpClient:
141
161
  await self._create_session_if_required()
142
162
  try:
143
163
  async with self.session.request(method, url, timeout=timeout, **query_kwargs) as response:
144
-
145
164
  if self.exchange == 'bybit':
146
165
  self.rate_limit_handler.update(path, response.headers)
147
166
 
148
167
  return await self.handle_errors(response, path)
149
168
  except (aiohttp.ClientConnectionError, asyncio.exceptions.TimeoutError):
150
- await self.session.close()
169
+ await self.close_session()
151
170
  raise ExchangeError("HTTP ClientConnectionError, the connection will be restored")
152
171
 
153
172
  async def _binance_request(self, path, method, signed, send_api_key, endpoint, timeout, **kwargs):
@@ -170,7 +189,7 @@ class HttpClient:
170
189
  async def _bitfinex_request(self, path, method, signed, send_api_key, endpoint, timeout, **kwargs):
171
190
  _endpoint = endpoint or self.endpoint
172
191
  bfx_post = (method == 'POST' and kwargs) or "params" in kwargs
173
- _params = json.dumps(kwargs) if bfx_post else None
192
+ _params = json.dumps(kwargs) if bfx_post else {}
174
193
  url = f'{_endpoint}/{path}'
175
194
  query_kwargs = {"headers": {"Accept": AJ}}
176
195
  if kwargs and not bfx_post:
@@ -25,7 +25,7 @@ FILTER_TYPE_MAP = {
25
25
 
26
26
 
27
27
  class OrderTradesEvent:
28
- def __init__(self, event_data: {}):
28
+ def __init__(self, event_data: dict):
29
29
  self.symbol = event_data["symbol"]
30
30
  self.client_order_id = event_data["clientOrderId"]
31
31
  self.side = "BUY" if event_data["isBuyer"] else "SELL"
@@ -4,6 +4,7 @@ Parser for convert Bitfinex REST API/WSS response to Binance like result
4
4
  import time
5
5
  from decimal import Decimal
6
6
  import logging
7
+ from typing import Dict, List, Union
7
8
 
8
9
  logger = logging.getLogger(__name__)
9
10
 
@@ -21,9 +22,9 @@ class OrderBook:
21
22
  self.asks[str(i[0])] = str(abs(i[2]))
22
23
 
23
24
  def get_book(self) -> dict:
24
- bids = list(map(list, self.bids.items()))
25
+ bids = [list(item) for item in self.bids.items()]
25
26
  bids.sort(key=lambda x: float(x[0]), reverse=True)
26
- asks = list(map(list, self.asks.items()))
27
+ asks = [list(item) for item in self.asks.items()]
27
28
  asks.sort(key=lambda x: float(x[0]), reverse=False)
28
29
  return {
29
30
  'stream': f"{self.symbol}@depth5",
@@ -270,15 +271,32 @@ def orders(res: list, response_type=None, cancelled=False) -> list:
270
271
  return binance_orders
271
272
 
272
273
 
273
- def order_book(res: list) -> dict:
274
- binance_order_book = {"lastUpdateId": int(time.time() * 1000)}
275
- bids = []
276
- asks = []
274
+ def order_book(res: List[List[float]]) -> Dict[str, Union[int, List[List[str]]]]:
275
+ """
276
+ Processes a list of raw order book entries into a structured dictionary.
277
+
278
+ Args:
279
+ res: A list of order entries, where each entry is a list like
280
+ [price (float), quantity (float), side_indicator (float)].
281
+ side_indicator > 0 for bids, < 0 for asks.
282
+
283
+ Returns:
284
+ A dictionary representing the order book with 'lastUpdateId', 'bids', and 'asks'.
285
+ """
286
+ binance_order_book: Dict[str, Union[int, List[List[str]]]] = {
287
+ "lastUpdateId": int(time.time() * 1000)
288
+ }
289
+
290
+ bids: List[List[str]] = []
291
+ asks: List[List[str]] = []
292
+
277
293
  for i in res:
278
- if i[2] > 0:
279
- bids.append([str(i[0]), str(i[2])])
280
- else:
281
- asks.append([str(i[0]), str(abs(i[2]))])
294
+ # Assuming i[2] determines bid/ask side and its absolute value is the quantity
295
+ if i[2] > 0: # This means it's a bid (positive quantity indicator)
296
+ bids.append([str(i[0]), str(i[2])]) # price, quantity
297
+ else: # This means it's an ask (negative quantity indicator or zero)
298
+ asks.append([str(i[0]), str(abs(i[2]))]) # price, absolute quantity
299
+
282
300
  binance_order_book['bids'] = bids
283
301
  binance_order_book['asks'] = asks
284
302
  return binance_order_book
@@ -435,7 +453,7 @@ def ticker(res: list, symbol: str = None) -> dict:
435
453
 
436
454
 
437
455
  def on_funds_update(res: list) -> dict:
438
- binance_funds = {
456
+ binance_funds: Dict[str, Union[str, int, List]] = {
439
457
  'e': 'outboundAccountPosition',
440
458
  'E': int(time.time() * 1000),
441
459
  'u': int(time.time() * 1000),
@@ -23,8 +23,8 @@ class OrderBook:
23
23
  'stream': f"{self.symbol}@depth5",
24
24
  'data': {
25
25
  'lastUpdateId': self.last_update_id,
26
- 'bids': list(map(list, self.bids.items()))[:5],
27
- 'asks': list(map(list, self.asks.items()))[:5],
26
+ 'bids': [list(item) for item in self.bids.items()][:5],
27
+ 'asks': [list(item) for item in self.asks.items()][:5],
28
28
  },
29
29
  }
30
30
 
@@ -3,6 +3,7 @@ Parser for convert OKX REST API/WSS V5 response to Binance like result
3
3
  """
4
4
  import time
5
5
  from decimal import Decimal
6
+ from typing import Dict, List, Union
6
7
  import logging
7
8
 
8
9
  logger = logging.getLogger(__name__)
@@ -14,7 +15,7 @@ def fetch_server_time(res: list) -> dict | None:
14
15
  return None
15
16
 
16
17
 
17
- def exchange_info(server_time: int, trading_symbol: list, tickers: list, symbol_t) -> {}:
18
+ def exchange_info(server_time: int, trading_symbol: list, tickers: list, symbol_t) -> dict:
18
19
  symbols = []
19
20
  symbols_price = {}
20
21
  for pair in tickers:
@@ -97,7 +98,7 @@ def orders(res: list, response_type=None) -> list:
97
98
  return binance_orders
98
99
 
99
100
 
100
- def order(res: {}, response_type=None) -> {}:
101
+ def order(res: dict, response_type=None) -> dict:
101
102
  symbol = res.get('instId').replace('-', '')
102
103
  order_id = int(res.get('ordId'))
103
104
  order_list_id = -1
@@ -185,7 +186,7 @@ def order(res: {}, response_type=None) -> {}:
185
186
  }
186
187
 
187
188
 
188
- def place_order_response(res: {}, req: {}) -> {}:
189
+ def place_order_response(res: dict, req: dict) -> dict:
189
190
  return {
190
191
  "symbol": req["instId"].replace('-', ''),
191
192
  "orderId": int(res["ordId"]),
@@ -214,10 +215,14 @@ def account_balances(res: list) -> dict:
214
215
  return {"balances": balances}
215
216
 
216
217
 
217
- def order_book(res: dict) -> dict:
218
- asks = []
219
- bids = []
220
- binance_order_book = {"lastUpdateId": int(res.get('ts'))}
218
+ def order_book(res: dict) -> Dict[str, Union[int, List[List[str]]]]:
219
+ binance_order_book: Dict[str, Union[int, List[List[str]]]] = {
220
+ "lastUpdateId": int(time.time() * 1000)
221
+ }
222
+
223
+ bids: List[List[str]] = []
224
+ asks: List[List[str]] = []
225
+
221
226
  [asks.append(ask[:2]) for ask in res.get('asks')]
222
227
  binance_order_book['asks'] = asks
223
228
  [bids.append(bid[:2]) for bid in res.get('bids')]
@@ -225,7 +230,7 @@ def order_book(res: dict) -> dict:
225
230
  return binance_order_book
226
231
 
227
232
 
228
- def ticker_price_change_statistics(res: {}) -> {}:
233
+ def ticker_price_change_statistics(res: dict) -> dict:
229
234
  price_change = str(Decimal(res.get('last')) - Decimal(res.get('open24h')))
230
235
  price_change_percent = str(100 * (Decimal(res.get('last')) - Decimal(res.get('open24h'))) /
231
236
  Decimal(res.get('open24h')))
@@ -258,14 +263,14 @@ def ticker_price_change_statistics(res: {}) -> {}:
258
263
  }
259
264
 
260
265
 
261
- def fetch_symbol_price_ticker(res: {}, symbol) -> {}:
266
+ def fetch_symbol_price_ticker(res: dict, symbol) -> dict:
262
267
  return {
263
268
  "symbol": symbol,
264
269
  "price": res.get('last')
265
270
  }
266
271
 
267
272
 
268
- def ticker(res: {}) -> {}:
273
+ def ticker(res: dict) -> dict:
269
274
  symbol = res.get('instId').replace('-', '')
270
275
  return {
271
276
  'stream': f"{symbol.lower()}@miniTicker",
@@ -339,7 +344,7 @@ def interval2value(_interval: str) -> int:
339
344
  return resolution.get(_interval, 0)
340
345
 
341
346
 
342
- def candle(res: list, symbol: str = None, ch_type: str = None) -> {}:
347
+ def candle(res: list, symbol: str = None, ch_type: str = None) -> dict:
343
348
  symbol = symbol.replace('-', '').lower()
344
349
  start_time = int(res[0])
345
350
  _interval = ch_type.replace('kline_', '')
@@ -373,7 +378,7 @@ def candle(res: list, symbol: str = None, ch_type: str = None) -> {}:
373
378
  }
374
379
 
375
380
 
376
- def order_book_ws(res: {}, symbol: str) -> {}:
381
+ def order_book_ws(res: dict, symbol: str) -> dict:
377
382
  symbol = symbol.replace('-', '').lower()
378
383
  return {
379
384
  'stream': f"{symbol}@depth5",
@@ -381,7 +386,7 @@ def order_book_ws(res: {}, symbol: str) -> {}:
381
386
  }
382
387
 
383
388
 
384
- def on_funds_update(res: {}) -> {}:
389
+ def on_funds_update(res: dict) -> dict:
385
390
  event_time = int(time.time() * 1000)
386
391
  data = res.get('details')
387
392
  funds = []
@@ -403,7 +408,7 @@ def on_funds_update(res: {}) -> {}:
403
408
  }
404
409
 
405
410
 
406
- def on_order_update(res: {}) -> {}:
411
+ def on_order_update(res: dict) -> dict:
407
412
  # print(f"on_order_update.res: {res}")
408
413
  order_quantity = res.get('sz')
409
414
  order_price = res.get('px')
@@ -459,7 +464,7 @@ def on_order_update(res: {}) -> {}:
459
464
  }
460
465
 
461
466
 
462
- def on_balance_update(res: list, buffer: dict, transfer: bool) -> ():
467
+ def on_balance_update(res: list, buffer: dict, transfer: bool) -> tuple:
463
468
  res_diff = []
464
469
  for i in res:
465
470
  asset = i.get('ccy')
@@ -1,8 +1,10 @@
1
1
  import sys
2
2
  import asyncio
3
+ import gc
4
+ import logging
5
+
3
6
  # noinspection PyPackageRequirements
4
7
  import ujson as json
5
- import logging.handlers
6
8
  from pathlib import Path
7
9
  import time
8
10
  from decimal import Decimal
@@ -18,28 +20,36 @@ import exchanges_wrapper.parsers.bitfinex as bfx
18
20
  import exchanges_wrapper.parsers.huobi as hbp
19
21
  import exchanges_wrapper.parsers.okx as okx
20
22
  import exchanges_wrapper.parsers.bybit as bbt
21
- from crypto_ws_api.ws_session import generate_signature, compose_htx_ws_auth, compose_binance_ws_auth
22
- from exchanges_wrapper import LOG_PATH
23
-
24
- logger = logging.getLogger(__name__)
25
- formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
26
- #
27
- fh = logging.handlers.RotatingFileHandler(Path(LOG_PATH, 'websockets.log'), maxBytes=1000000, backupCount=10)
28
- fh.setFormatter(formatter)
29
- fh.setLevel(logging.INFO)
30
- #
31
- sh = logging.StreamHandler()
32
- sh.setFormatter(formatter)
33
- sh.setLevel(logging.INFO)
34
-
35
- logger.addHandler(fh)
36
- logger.addHandler(sh)
37
- logger.propagate = False
38
-
23
+ from crypto_ws_api.ws_session import (
24
+ set_logger,
25
+ generate_signature,
26
+ compose_htx_ws_auth,
27
+ compose_binance_ws_auth,
28
+ tasks_manage,
29
+ tasks_cancel
30
+ )
31
+ from exchanges_wrapper import LOG_PATH, DEBUG_LOG
32
+
33
+ logger = set_logger(__name__, Path(LOG_PATH, 'websockets.log'))
39
34
  sys.tracebacklimit = 0
40
35
 
41
36
 
42
37
  class EventsDataStream:
38
+ __slots__ = (
39
+ 'client',
40
+ 'endpoint',
41
+ 'exchange',
42
+ 'trade_id',
43
+ 'websocket',
44
+ 'try_count',
45
+ 'wss_event_buffer',
46
+ '_order_book',
47
+ '_price',
48
+ 'tasks',
49
+ 'ping',
50
+ 'logger'
51
+ )
52
+
43
53
  def __init__(self, client, endpoint, exchange, trade_id):
44
54
  self.client = client
45
55
  self.endpoint = endpoint
@@ -51,51 +61,54 @@ class EventsDataStream:
51
61
  self._order_book = None
52
62
  self._price = None
53
63
  self.tasks = set()
54
- self.wss_started = False
55
64
  self.ping = 0
65
+ self.logger = logger
66
+
67
+ if exchange == DEBUG_LOG:
68
+ log_key = f"web_s_{trade_id}"
69
+ if log_key in logging.root.manager.loggerDict:
70
+ self.logger = logging.root.manager.loggerDict[log_key]
71
+ else:
72
+ self.logger = set_logger(log_key, Path(LOG_PATH, f"{log_key}.log"), logging.DEBUG)
56
73
 
57
74
  async def start(self):
58
75
  async for self.websocket in connect(
59
76
  self.endpoint,
60
- logger=logger,
77
+ logger=self.logger,
61
78
  ping_interval=None if self.exchange in ('binance', 'huobi') else 20
62
79
  ):
80
+ await self.cycle_init()
63
81
  start_time = datetime.now(timezone.utc).replace(tzinfo=None)
64
82
  try:
65
83
  await self.start_wss()
66
84
  except ConnectionClosed as ex:
85
+ self.websocket = None
67
86
  ct = str(datetime.now(timezone.utc).replace(tzinfo=None) - start_time).rsplit('.')[0]
68
- logger.info(f"WSS life time for {self.exchange} is {ct}")
69
- self.tasks_cancel()
87
+ self.logger.info(f"WSS life time for {self.exchange} is {ct}")
70
88
  if ex.rcvd and ex.rcvd.code == 4000:
71
- logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
89
+ self.logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
72
90
  break
73
91
  else:
74
- logger.warning(f"Restart WSS for {self.exchange}: {ex}")
92
+ self.logger.warning(f"Restart WSS for {self.exchange}: {ex}")
75
93
  continue
76
94
  except Exception as ex:
77
- self.tasks_cancel()
78
- logger.error(f"WSS start() other exception: {ex}")
95
+ self.logger.error(f"WSS start() other exception: {ex}")
79
96
 
80
97
  async def start_wss(self):
81
98
  pass # meant to be overridden in a subclass
82
99
 
100
+ async def cycle_init(self):
101
+ await tasks_cancel(self.tasks, _logger=logger)
102
+ self.wss_event_buffer.clear()
103
+ gc.collect()
104
+
83
105
  async def stop(self):
84
106
  """
85
107
  Stop data stream
86
108
  """
87
- self.tasks_cancel()
88
109
  if self.websocket:
89
110
  await self.websocket.close(code=4000)
90
-
91
- def tasks_cancel(self):
92
- [task.cancel() for task in self.tasks if not task.done()]
93
- self.tasks.clear()
94
-
95
- def tasks_manage(self, coro):
96
- _t = asyncio.create_task(coro)
97
- self.tasks.add(_t)
98
- _t.add_done_callback(self.tasks.discard)
111
+ await self.cycle_init()
99
112
 
100
113
  async def _handle_event(self, *args):
101
114
  pass # meant to be overridden in a subclass
@@ -109,8 +122,11 @@ class EventsDataStream:
109
122
  await self._handle_event(event)
110
123
  elif msg_data.get("status") == 200:
111
124
  result = msg_data.get("result")
112
- if isinstance(result, dict) and not result:
113
- self.wss_started = True
125
+ if isinstance(result, dict) and 'authorizedSince' in result:
126
+ self.logger.info(f"Binance User Data Stream started for {self.trade_id}")
127
+ else:
128
+ self.logger.warning(f"Reconnecting Binance User Data Stream for {self.trade_id}, msg_data: {msg_data}")
129
+ raise ConnectionClosed(None, None)
114
130
  elif self.exchange == 'bybit':
115
131
  if _data := msg_data.get('data'):
116
132
  if ch_type == 'depth5':
@@ -127,15 +143,17 @@ class EventsDataStream:
127
143
  return
128
144
  elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
129
145
  and msg_data.get("success")):
130
- self.tasks_manage(self.bybit_heartbeat(ch_type or "private"))
146
+ tasks_manage(
147
+ self.tasks, self.bybit_heartbeat(ch_type or "private"), f"bybit_heartbeat-{symbol}-{ch_type}"
148
+ )
131
149
  if msg_data["op"] == "subscribe" and msg_data["success"] and not msg_data["ret_msg"]:
132
- self.wss_started = True
150
+ self.logger.info(f"ByBit User Data Stream started for {self.trade_id}")
133
151
  elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
134
152
  and not msg_data.get("success")):
135
- logger.warning(f"Reconnecting ByBit WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
153
+ self.logger.warning(f"Reconnecting ByBit WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
136
154
  raise ConnectionClosed(None, None)
137
155
  else:
138
- logger.info(f"ByBit undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
156
+ self.logger.info(f"ByBit undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
139
157
  elif self.exchange == 'okx':
140
158
  if (not ch_type and
141
159
  msg_data.get('arg', {}).get('channel') in ('account', 'orders', 'balance_and_position')
@@ -146,46 +164,45 @@ class EventsDataStream:
146
164
  elif msg_data.get("event") == "login" and msg_data.get("code") == "0":
147
165
  return
148
166
  elif msg_data.get("event") == "subscribe" and msg_data.get('arg', {}).get('channel') == 'orders':
149
- self.wss_started = True
167
+ self.logger.info(f"OKX User Data Stream started for {self.trade_id}")
150
168
  elif msg_data.get("event") in ("login", "error") and msg_data.get("code") != "0":
151
- logger.warning(f"Reconnecting OKX WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
169
+ self.logger.warning(f"Reconnecting OKX WSS: {symbol}: {ch_type}, msg_data: {msg_data}")
152
170
  raise ConnectionClosed(None, None)
153
171
  else:
154
- logger.debug(f"OKX undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
172
+ self.logger.debug(f"OKX undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
155
173
  elif self.exchange == 'bitfinex':
156
174
  # info and error handling
157
175
  if isinstance(msg_data, dict):
158
176
  if msg_data.get('version') and msg_data.get('version') != 2:
159
- logger.critical('Change WSS version detected')
177
+ self.logger.critical('Change WSS version detected')
160
178
  if msg_data.get('platform') and msg_data.get('platform').get('status') != 1:
161
- logger.warning(f"Exchange in maintenance mode, trying reconnect. Exchange info: {msg}")
179
+ self.logger.warning(f"Exchange in maintenance mode, trying reconnect. Exchange info: {msg}")
162
180
  await asyncio.sleep(60)
163
181
  raise ConnectionClosed(None, None)
164
182
  elif 'code' in msg_data:
165
183
  code = msg_data.get('code')
166
184
  if code == 10300:
167
- logger.warning('WSS Subscription failed (generic)')
185
+ self.logger.warning('WSS Subscription failed (generic)')
168
186
  raise ConnectionClosed(None, None)
169
187
  elif code == 10301:
170
- logger.error('WSS Already subscribed')
188
+ self.logger.error('WSS Already subscribed')
171
189
  elif code == 10302:
172
190
  raise UserWarning(f"WSS Unknown channel {ch_type}")
173
191
  elif code == 10305:
174
192
  raise UserWarning('WSS Reached limit of open channels')
175
193
  elif code == 20051:
176
- logger.warning('WSS reconnection request received from exchange')
194
+ self.logger.warning('WSS reconnection request received from exchange')
177
195
  raise ConnectionClosed(None, None)
178
196
  elif code == 20060:
179
- logger.info('WSS entering in maintenance mode, trying reconnect after 120s')
197
+ self.logger.info('WSS entering in maintenance mode, trying reconnect after 120s')
180
198
  await asyncio.sleep(120)
181
199
  raise ConnectionClosed(None, None)
182
200
  elif msg_data.get('event') == 'subscribed':
183
201
  chan_id = msg_data.get('chanId')
184
- logger.info(f"bitfinex, ch_type: {ch_type}, chan_id: {chan_id}")
202
+ self.logger.info(f"bitfinex, ch_type: {ch_type}, chan_id: {chan_id}")
185
203
  elif msg_data.get('event') == 'auth' and msg_data.get('status') == 'OK':
186
204
  chan_id = msg_data.get('chanId')
187
- self.wss_started = True
188
- logger.info(f"bitfinex, user stream chan_id: {chan_id}")
205
+ self.logger.info(f"Bitfinex, user stream chan_id: {chan_id}")
189
206
 
190
207
  # data handling
191
208
  elif isinstance(msg_data, list) and len(msg_data) == 2 and msg_data[1] == 'hb':
@@ -196,7 +213,7 @@ class EventsDataStream:
196
213
  else:
197
214
  await self._handle_event(msg_data, symbol, ch_type)
198
215
  else:
199
- logger.debug(f"Bitfinex undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
216
+ self.logger.debug(f"Bitfinex undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
200
217
  elif self.exchange == 'huobi':
201
218
  if ping := msg_data.get('ping'):
202
219
  self.ping = 0
@@ -222,16 +239,16 @@ class EventsDataStream:
222
239
  await self._handle_event(msg_data, symbol, ch_type)
223
240
  elif msg_data.get('action') in ('req', 'sub') and msg_data.get('code') == 200:
224
241
  if msg_data.get('ch') == f"trade.clearing#{symbol.lower()}#0":
225
- self.wss_started = True
242
+ self.logger.info(f"HTX User Data Stream started for {self.trade_id}")
226
243
  elif 'subbed' in msg_data and msg_data.get('status') == 'ok':
227
- logger.info(f"Huobi WSS started: {msg_data['subbed']}")
244
+ self.logger.info(f"Huobi WSS started: {msg_data['subbed']}")
228
245
  elif (msg_data.get('action') == 'sub' and
229
246
  msg_data.get('code') == 500 and
230
247
  msg_data.get('message') == '系统异常:'):
231
- logger.warning(f"Reconnecting Huobi user {ch_type} channel")
248
+ self.logger.warning(f"Reconnecting Huobi user {ch_type} channel")
232
249
  raise ConnectionClosed(None, None)
233
250
  else:
234
- logger.debug(f"Huobi undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
251
+ self.logger.debug(f"Huobi undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
235
252
 
236
253
  async def ws_listener(self, request=None, symbol=None, ch_type=str()):
237
254
  if request:
@@ -246,21 +263,23 @@ class EventsDataStream:
246
263
  try:
247
264
  await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
248
265
  except (ConnectionClosed, asyncio.exceptions.TimeoutError):
249
- pass # handled elsewhere
266
+ break
250
267
 
251
268
  async def htx_keepalive(self, interval=60):
252
- await asyncio.sleep(interval * 10)
269
+ await asyncio.sleep(interval)
253
270
  while True:
254
271
  await asyncio.sleep(interval)
255
272
  if self.ping:
256
273
  break
257
274
  else:
258
275
  self.ping = 1
259
- logger.warning("From HTX server PING timeout exceeded")
276
+ self.logger.warning("From HTX server PING timeout exceeded")
260
277
  await self.websocket.close()
261
278
 
262
279
 
263
280
  class MarketEventsDataStream(EventsDataStream):
281
+ __slots__ = ('channel', 'candles_max_time', 'endpoint')
282
+
264
283
  def __init__(self, client, endpoint, exchange, trade_id, channel=None):
265
284
  super().__init__(client, endpoint, exchange, trade_id)
266
285
  self.channel = channel
@@ -271,7 +290,7 @@ class MarketEventsDataStream(EventsDataStream):
271
290
  self.endpoint = f"{endpoint}/stream?streams={combined_streams}"
272
291
 
273
292
  async def start_wss(self):
274
- logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
293
+ self.logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
275
294
  symbol = None
276
295
  ch_type = str()
277
296
  request = {}
@@ -330,12 +349,12 @@ class MarketEventsDataStream(EventsDataStream):
330
349
  elif ch_type == 'depth5':
331
350
  request = {'sub': f"market.{symbol}.depth.step0"}
332
351
 
333
- self.tasks_manage(self.htx_keepalive(interval=30))
352
+ tasks_manage(self.tasks, self.htx_keepalive(interval=30), f"htx_keepalive-{symbol}-{ch_type}")
334
353
 
335
354
  await self.ws_listener(request, symbol, ch_type)
336
355
 
337
356
  async def _handle_event(self, content, symbol=None, ch_type=str()):
338
- # logger.info(f"MARKET_handle_event.content: symbol: {symbol}, ch_type: {ch_type}, content: {content}")
357
+ # self.logger.info(f"MARKET_handle_event.content: symbol: {symbol}, ch_type: {ch_type}, content: {content}")
339
358
  self.try_count = 0
340
359
  if self.exchange == 'bitfinex':
341
360
  if 'candles' in ch_type:
@@ -390,6 +409,8 @@ class MarketEventsDataStream(EventsDataStream):
390
409
 
391
410
 
392
411
  class HbpPrivateEventsDataStream(EventsDataStream):
412
+ __slots__ = ('symbol',)
413
+
393
414
  def __init__(self, client, endpoint, exchange, trade_id, symbol):
394
415
  super().__init__(client, endpoint, exchange, trade_id)
395
416
  self.symbol = symbol
@@ -423,7 +444,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
423
444
  "action": "sub",
424
445
  "ch": f"trade.clearing#{self.symbol.lower()}#0"
425
446
  }
426
- self.tasks_manage(self.htx_keepalive())
447
+ tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-user-{self.symbol}")
427
448
  await self.ws_listener(request, symbol=self.symbol)
428
449
 
429
450
  async def _handle_event(self, msg_data, *args):
@@ -446,11 +467,12 @@ class HbpPrivateEventsDataStream(EventsDataStream):
446
467
  content = hbp.on_order_update(self.client.active_orders[order_id])
447
468
 
448
469
  if content:
449
- logger.debug(f"HTXPrivateEvents.content: {content}")
470
+ self.logger.debug(f"HTXPrivateEvents.content: {content}")
450
471
  await self.client.events.wrap_event(content).fire(self.trade_id)
451
472
 
452
473
 
453
474
  class BfxPrivateEventsDataStream(EventsDataStream):
475
+ __slots__ = ()
454
476
 
455
477
  async def start_wss(self):
456
478
  ts = int(time.time() * 1000)
@@ -505,6 +527,8 @@ class BfxPrivateEventsDataStream(EventsDataStream):
505
527
 
506
528
 
507
529
  class OkxPrivateEventsDataStream(EventsDataStream):
530
+ __slots__ = ('symbol',)
531
+
508
532
  def __init__(self, client, endpoint, exchange, trade_id, symbol):
509
533
  super().__init__(client, endpoint, exchange, trade_id)
510
534
  self.symbol = symbol
@@ -561,6 +585,7 @@ class OkxPrivateEventsDataStream(EventsDataStream):
561
585
 
562
586
 
563
587
  class BBTPrivateEventsDataStream(EventsDataStream):
588
+ __slots__ = ()
564
589
 
565
590
  async def start_wss(self):
566
591
  ts = int((time.time() + 1) * 1000)
@@ -591,7 +616,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
591
616
  await self.client.events.wrap_event(content).fire(self.trade_id)
592
617
  content = None
593
618
  elif ch_type == 'order.spot':
594
- # logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
619
+ # self.logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
595
620
  event = msg_data[0]
596
621
  if event.get('orderStatus') in ("Cancelled", "PartiallyFilledCanceled"):
597
622
  self.client.wss_buffer[f"oc-{event.get('orderId')}"] = bbt.order(event, response_type=True)
@@ -603,6 +628,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
603
628
 
604
629
 
605
630
  class UserEventsDataStream(EventsDataStream):
631
+ __slots__ = ()
606
632
 
607
633
  async def start_wss(self):
608
634
  await self.websocket.send(
@@ -620,5 +646,5 @@ class UserEventsDataStream(EventsDataStream):
620
646
  await self.ws_listener(request)
621
647
 
622
648
  async def _handle_event(self, content):
623
- # logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
649
+ # self.logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
624
650
  await self.client.events.wrap_event(content).fire(self.trade_id)
@@ -4,7 +4,10 @@ build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
6
  name = "exchanges-wrapper"
7
- authors = [{name = "Thomas Marchand", email = "thomas.marchand@tuta.io"}, {name = "Jerry Fedorenko", email = "jerry.fedorenko@yahoo.com"}]
7
+ authors = [
8
+ {name = "Thomas Marchand", email = "thomas.marchand@tuta.io"},
9
+ {name = "Jerry Fedorenko", email = "jerry.fedorenko@yahoo.com"}
10
+ ]
8
11
  readme = "README.md"
9
12
  license = {file = "LICENSE.md"}
10
13
  classifiers=["Programming Language :: Python :: 3",
@@ -17,10 +20,10 @@ dynamic = ["version", "description"]
17
20
  requires-python = ">=3.10"
18
21
 
19
22
  dependencies = [
20
- "crypto-ws-api==2.0.20",
23
+ "crypto-ws-api==2.1.3",
21
24
  "pyotp==2.9.0",
22
- "simplejson==3.20.1",
23
- "aiohttp~=3.12.13",
25
+ "simplejson==3.20.2",
26
+ "aiohttp~=3.13.0",
24
27
  "expiringdict~=1.2.2",
25
28
  "betterproto==2.0.0b7",
26
29
  "grpclib~=0.4.8"