exchanges-wrapper 2.1.41__tar.gz → 2.1.43__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.41 → exchanges_wrapper-2.1.43}/PKG-INFO +4 -4
  2. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/__init__.py +4 -2
  3. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/client.py +67 -52
  4. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv.py +55 -56
  5. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/http_client.py +22 -3
  6. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/lib.py +1 -1
  7. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/okx.py +11 -11
  8. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/web_sockets.py +99 -72
  9. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/pyproject.toml +7 -4
  10. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/LICENSE.md +0 -0
  11. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/README.md +0 -0
  12. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/definitions.py +0 -0
  13. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/errors.py +0 -0
  14. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/events.py +0 -0
  15. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  16. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/martin/__init__.py +0 -0
  17. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  18. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bybit.py +0 -0
  19. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/huobi.py +0 -0
  20. {exchanges_wrapper-2.1.41 → exchanges_wrapper-2.1.43}/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.41
3
+ Version: 2.1.43
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.1.0
15
+ Requires-Dist: crypto-ws-api==2.1.4
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.2
19
19
  Requires-Dist: expiringdict~=1.2.2
20
20
  Requires-Dist: betterproto==2.0.0b7
21
21
  Requires-Dist: grpclib~=0.4.8
@@ -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.41"
15
+ __version__ = "2.1.43"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -33,9 +33,11 @@ __all__ = [
33
33
  'LOG_PATH',
34
34
  'WORK_PATH',
35
35
  'LOG_FILE',
36
- 'CONFIG_FILE'
36
+ 'CONFIG_FILE',
37
+ 'DEBUG_LOG'
37
38
  ]
38
39
 
40
+ DEBUG_LOG = 'debug' # The exchange for which log files, separated by trade_id with DEBUG level, will be generated
39
41
  WORK_PATH = Path(Path.home(), ".MartinBinance")
40
42
  CONFIG_PATH = Path(WORK_PATH, "config")
41
43
  CONFIG_FILE = Path(CONFIG_PATH, "exch_srv_cfg.toml")
@@ -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', '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']
@@ -83,7 +98,6 @@ class Client:
83
98
  else:
84
99
  self.user_session = None
85
100
  #
86
- self.loaded = False
87
101
  self.symbols = {}
88
102
  self.highest_precision = None
89
103
  self.rate_limits = None
@@ -98,15 +112,10 @@ class Client:
98
112
  self.main_account_uid = None
99
113
  self.ledgers_id = []
100
114
  self.ts_start = {}
101
- self.tasks = set()
115
+ self.tasks: dict[str, set] = {}
102
116
  self.request_event = asyncio.Event()
103
117
  self.request_event.set()
104
118
 
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
119
  async def fetch_object(self, key):
111
120
  res = None
112
121
  while res is None:
@@ -140,12 +149,11 @@ class Client:
140
149
  logger.info(f"Main ByBit account UID: {self.main_account_uid}, sub-UID: {self.account_uid}")
141
150
  # load rate limits
142
151
  self.rate_limits = infos["rateLimits"]
143
- self.loaded = True
144
152
  logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
145
153
 
146
154
  async def close(self):
147
- if self.http and self.http.session:
148
- await self.http.session.close()
155
+ if self.http:
156
+ await self.http.close_session()
149
157
 
150
158
  @property
151
159
  def events(self):
@@ -154,9 +162,8 @@ class Client:
154
162
  self._events = Events() # skipcq: PYL-W0201
155
163
  return self._events
156
164
 
157
- async def start_user_events_listener(self, _trade_id, symbol):
165
+ def start_user_events_listener(self, _trade_id, symbol):
158
166
  logger.info(f"Start '{self.exchange}' user events listener for {_trade_id}")
159
- user_data_stream = None
160
167
  if self.exchange == 'binance':
161
168
  user_data_stream = UserEventsDataStream(self, self.endpoint_ws_api, self.exchange, _trade_id)
162
169
  elif self.exchange == 'bitfinex':
@@ -173,24 +180,27 @@ class Client:
173
180
  )
174
181
  elif self.exchange == 'bybit':
175
182
  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)
183
+ else:
184
+ raise UserWarning(f"User Data Stream: exchange {self.exchange} not serviced")
185
+
186
+ self.data_streams[_trade_id] |= {user_data_stream}
187
+
188
+ trade_tasks = self.tasks.pop(_trade_id, set())
189
+ tasks_manage(trade_tasks, user_data_stream.start(), f"user_data_stream-{self.exchange}-{_trade_id}")
190
+ self.tasks[_trade_id] = trade_tasks
186
191
 
187
192
  def start_market_events_listener(self, _trade_id):
188
193
  _events = self.events.registered_streams.get(self.exchange, {}).get(_trade_id, set())
189
194
  if self.exchange == 'binance':
190
195
  market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
191
196
  self.data_streams[_trade_id] |= {market_data_stream}
192
- self.tasks_manage(market_data_stream.start())
197
+
198
+ trade_tasks = self.tasks.pop(_trade_id, set())
199
+ tasks_manage(trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{_trade_id}")
200
+ self.tasks[_trade_id] = trade_tasks
201
+
193
202
  else:
203
+ trade_tasks = self.tasks.pop(_trade_id, set())
194
204
  for channel in _events:
195
205
  # https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url
196
206
  if self.exchange == 'okx' and 'kline' in channel:
@@ -200,18 +210,24 @@ class Client:
200
210
  #
201
211
  market_data_stream = MarketEventsDataStream(self, _endpoint, self.exchange, _trade_id, channel)
202
212
  self.data_streams[_trade_id] |= {market_data_stream}
203
- self.tasks_manage(market_data_stream.start())
213
+ tasks_manage(
214
+ trade_tasks, market_data_stream.start(), f"market_data_stream-{self.exchange}-{channel}-{_trade_id}"
215
+ )
216
+ self.tasks[_trade_id] = trade_tasks
204
217
 
205
218
  async def stop_events_listener(self, _trade_id):
206
219
  logger.info(f"Stop events listener data streams for {_trade_id}")
207
220
  stopped_data_stream = self.data_streams.pop(_trade_id, set())
208
221
  for data_stream in stopped_data_stream:
209
222
  await data_stream.stop()
223
+ if trade_tasks := self.tasks.pop(_trade_id, set()):
224
+ await tasks_cancel(trade_tasks, _logger=logger)
225
+
210
226
  if self.user_session:
211
- await self.user_session.stop()
227
+ await self.user_session.stop(_trade_id)
212
228
 
213
229
  def assert_symbol_exists(self, symbol):
214
- if self.loaded and symbol not in self.symbols:
230
+ if symbol not in self.symbols:
215
231
  raise ExchangePyError(f"Symbol {symbol} is not valid according to the loaded exchange infos")
216
232
 
217
233
  def symbol_to_bfx(self, symbol) -> str:
@@ -261,32 +277,31 @@ class Client:
261
277
  def refine_amount(self, symbol, amount: Union[str, Decimal], _quote=False):
262
278
  if type(amount) is str: # to save time for developers
263
279
  amount = Decimal(amount)
264
- if self.loaded:
265
- precision = self.symbols[symbol]["baseAssetPrecision"]
266
- lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
267
- step_size = Decimal(lot_size_filter["stepSize"])
268
- # noinspection PyStringFormat
269
- amount = (
270
- (f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
271
- .rstrip("0")
272
- .rstrip(".")
273
- )
280
+
281
+ precision = self.symbols[symbol]["baseAssetPrecision"]
282
+ lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
283
+ step_size = Decimal(lot_size_filter["stepSize"])
284
+ # noinspection PyStringFormat
285
+ amount = (
286
+ (f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
287
+ .rstrip("0")
288
+ .rstrip(".")
289
+ )
274
290
  return amount
275
291
 
276
292
  def refine_price(self, symbol, price: Union[str, Decimal]):
277
293
  if isinstance(price, str): # to save time for developers
278
294
  price = Decimal(price)
279
295
 
280
- if self.loaded:
281
- precision = self.symbols[symbol]["baseAssetPrecision"]
282
- price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
283
- price = price - (price % Decimal(price_filter["tickSize"]))
284
- # noinspection PyStringFormat
285
- price = (
286
- (f"%.{precision}f" % truncate(price, precision))
287
- .rstrip("0")
288
- .rstrip(".")
289
- )
296
+ precision = self.symbols[symbol]["baseAssetPrecision"]
297
+ price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
298
+ price = price - (price % Decimal(price_filter["tickSize"]))
299
+ # noinspection PyStringFormat
300
+ price = (
301
+ (f"%.{precision}f" % truncate(price, precision))
302
+ .rstrip("0")
303
+ .rstrip(".")
304
+ )
290
305
  return price
291
306
 
292
307
  def assert_symbol(self, symbol):
@@ -835,7 +850,7 @@ class Client:
835
850
  **params,
836
851
  )
837
852
  if res:
838
- timeout = STATUS_TIMEOUT / 0.1
853
+ timeout = int(STATUS_TIMEOUT / 0.1)
839
854
  while not self.active_orders.get(int(res)) and timeout:
840
855
  timeout -= 1
841
856
  await asyncio.sleep(0.1)
@@ -908,7 +923,7 @@ class Client:
908
923
  )
909
924
  if b_res is None:
910
925
  fallback_warning(self.exchange, symbol)
911
- b_res = await self.http.send_api_call(
926
+ b_res = await self.http.send_api_call(
912
927
  ORDER_ENDPOINT,
913
928
  params=params,
914
929
  signed=True,
@@ -1030,7 +1045,7 @@ class Client:
1030
1045
  **params
1031
1046
  )
1032
1047
  if res and isinstance(res, list) and res[6] == 'SUCCESS':
1033
- timeout = STATUS_TIMEOUT / 0.1
1048
+ timeout = int(STATUS_TIMEOUT / 0.1)
1034
1049
  while timeout:
1035
1050
  timeout -= 1
1036
1051
  if self.active_orders.get(order_id, {}).get('cancelled', False):
@@ -1046,7 +1061,7 @@ class Client:
1046
1061
  signed=True
1047
1062
  )
1048
1063
  if res:
1049
- timeout = STATUS_TIMEOUT / 0.1
1064
+ timeout = int(STATUS_TIMEOUT / 0.1)
1050
1065
  while not self.active_orders.get(order_id, {}).get('cancelled', False) and timeout:
1051
1066
  timeout -= 1
1052
1067
  await asyncio.sleep(0.1)
@@ -1182,7 +1197,7 @@ class Client:
1182
1197
  signed=True,
1183
1198
  data=params,
1184
1199
  )
1185
- ids_canceled = [int(ordr['ordId']) for ordr in res if ordr['sCode'] == '0']
1200
+ ids_canceled = [int(order['ordId']) for order in res if order['sCode'] == '0']
1186
1201
  orders_canceled[:] = [i for i in orders_canceled if i['orderId'] in ids_canceled]
1187
1202
  binance_res.extend(orders_canceled)
1188
1203
  elif self.exchange == 'bybit':
@@ -8,6 +8,8 @@ import grpclib.exceptions
8
8
  from exchanges_wrapper import __version__ as VER_EW
9
9
  # noinspection PyPep8Naming
10
10
  from crypto_ws_api import __version__ as VER_CW
11
+ from crypto_ws_api.ws_session import set_logger
12
+
11
13
  import time
12
14
  import weakref
13
15
  import gc
@@ -16,8 +18,9 @@ import asyncio
16
18
  import functools
17
19
  # noinspection PyPackageRequirements
18
20
  import ujson as json
19
- import logging.handlers
21
+ import logging
20
22
  from decimal import Decimal
23
+ import ctypes, ctypes.util
21
24
 
22
25
  import exchanges_wrapper.martin as mr
23
26
  from exchanges_wrapper import WORK_PATH, LOG_FILE, errors, Server, Status, GRPCError, graceful_exit
@@ -29,32 +32,24 @@ from exchanges_wrapper.lib import (
29
32
  REST_RATE_LIMIT_INTERVAL,
30
33
  FILTER_TYPE_MAP,
31
34
  )
32
- from exchanges_wrapper.martin import StreamResponse, SimpleResponse, OnKlinesUpdateResponse, OnTickerUpdateResponse, \
33
- FetchOrderBookResponse
34
-
35
+ from exchanges_wrapper.martin import (
36
+ StreamResponse,
37
+ SimpleResponse,
38
+ OnKlinesUpdateResponse,
39
+ OnTickerUpdateResponse,
40
+ FetchOrderBookResponse,
41
+ )
35
42
  #
36
43
  HEARTBEAT = 1 # sec
37
44
  MAX_QUEUE_SIZE = 100
38
45
  WSS_TICKER_TIMEOUT = 600 # sec
39
46
  #
40
- logger = logging.getLogger(__name__)
41
- formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
42
- #
43
- fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1000000, backupCount=10)
44
- fh.setFormatter(formatter)
45
- fh.setLevel(logging.DEBUG)
46
- #
47
- sh = logging.StreamHandler()
48
- sh.setFormatter(formatter)
49
- sh.setLevel(logging.INFO)
50
- #
51
- root_logger = logging.getLogger()
52
- root_logger.setLevel(min([fh.level, sh.level]))
53
- root_logger.addHandler(fh)
54
- root_logger.addHandler(sh)
55
-
56
- logging.basicConfig()
47
+ logger = set_logger(__name__, LOG_FILE, file_level=logging.DEBUG, set_root_logger=True)
57
48
  logging.getLogger('hpack').setLevel(logging.INFO)
49
+ logging.getLogger('grpclib.protocol').setLevel(logging.INFO)
50
+
51
+ def malloc_trim(trim_type: int = 0):
52
+ ctypes.CDLL(ctypes.util.find_library('c')).malloc_trim(trim_type)
58
53
 
59
54
 
60
55
  class OpenClient:
@@ -90,9 +85,9 @@ class OpenClient:
90
85
  return res
91
86
 
92
87
  @classmethod
93
- def remove_client(cls, _account_name):
94
- cls.open_clients[:] = [i for i in cls.open_clients if i.name != _account_name]
95
-
88
+ def remove_client(cls, _id):
89
+ # noinspection PyTypeHints
90
+ cls.open_clients[:] = [i for i in cls.open_clients if id(i) != _id]
96
91
 
97
92
  # noinspection PyPep8Naming,PyMethodMayBeStatic
98
93
  class Martin(mr.MartinBase):
@@ -139,8 +134,8 @@ class Martin(mr.MartinBase):
139
134
  try:
140
135
  await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
141
136
  except asyncio.exceptions.TimeoutError:
142
- await OpenClient.get_client(client_id).client.http.session.close()
143
- OpenClient.remove_client(request.account_name)
137
+ await OpenClient.get_client(client_id).client.http.close_session()
138
+ OpenClient.remove_client(client_id)
144
139
  raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
145
140
  except Exception as ex:
146
141
  logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
@@ -227,7 +222,7 @@ class Martin(mr.MartinBase):
227
222
  server_time = res.get('serverTime')
228
223
  return mr.FetchServerTimeResponse(server_time=server_time)
229
224
 
230
- async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse():
225
+ async def one_click_arrival_deposit(self, request: mr.MarketRequest) -> mr.SimpleResponse:
231
226
  res, _, _ = await self.send_request('one_click_arrival_deposit', request, tx_id=request.symbol)
232
227
  return mr.SimpleResponse(success=True, result=json.dumps(str(res)))
233
228
 
@@ -254,7 +249,7 @@ class Martin(mr.MartinBase):
254
249
 
255
250
  async def fetch_order(self, request: mr.FetchOrderRequest) -> mr.FetchOrderResponse:
256
251
  response = mr.FetchOrderResponse()
257
- res, _, msg_header = await self.send_request(
252
+ res, client, msg_header = await self.send_request(
258
253
  'fetch_order',
259
254
  request,
260
255
  rate_limit=True,
@@ -265,14 +260,15 @@ class Martin(mr.MartinBase):
265
260
  )
266
261
  logger.debug(f"{msg_header}: {res}")
267
262
 
268
- if res and request.filled_update_call and Decimal(res['executedQty']):
269
- request.order_id = res['orderId']
270
- await self.create_trade_stream_event(request, res)
263
+ if _queue := client.on_order_update_queues.get(request.trade_id):
264
+ if res and request.filled_update_call and Decimal(res['executedQty']):
265
+ request.order_id = res['orderId']
266
+ await self.create_trade_stream_event(request, res, _queue)
271
267
  response.from_pydict(res)
272
268
  return response
273
269
 
274
- async def create_trade_stream_event(self, request, order):
275
- trades, client, msg_header = await self.send_request(
270
+ async def create_trade_stream_event(self, request, order, _queue):
271
+ trades, _, msg_header = await self.send_request(
276
272
  'fetch_order_trade_list',
277
273
  request,
278
274
  trade_id=request.trade_id,
@@ -280,8 +276,6 @@ class Martin(mr.MartinBase):
280
276
  order_id=request.order_id
281
277
  )
282
278
 
283
- _queue = client.on_order_update_queues.get(request.trade_id)
284
-
285
279
  for trade in trades:
286
280
  trade['updateTime'] = trade.pop('time')
287
281
  trade |= {
@@ -297,7 +291,7 @@ class Martin(mr.MartinBase):
297
291
  await _queue.put(weakref.ref(event)())
298
292
  logger.debug(f"{msg_header}: {trades}")
299
293
 
300
- async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse():
294
+ async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse:
301
295
  response = mr.SimpleResponse()
302
296
 
303
297
  res, _, _ = await self.send_request(
@@ -525,10 +519,12 @@ class Martin(mr.MartinBase):
525
519
  _event_type = f"{_symbol}@miniTicker"
526
520
  client.events.register_event(functools.partial(event_handler, _queue, client, request.trade_id, _event_type),
527
521
  _event_type, client.exchange, request.trade_id)
522
+ Martin.ticker_update_time[request.trade_id] = time.time()
528
523
  while True:
529
524
  _event = await _queue.get()
530
525
  if isinstance(_event, str) and _event == request.trade_id:
531
526
  client.stream_queue.get(request.trade_id, set()).discard(_queue)
527
+ Martin.ticker_update_time.pop(request.trade_id, None)
532
528
  logger.info(f"OnTickerUpdate: Stop loop for {open_client.name}: {request.symbol}")
533
529
  return
534
530
  else:
@@ -754,7 +750,7 @@ class Martin(mr.MartinBase):
754
750
  )
755
751
  logger.info(f"Start WS streams for {open_client.name}")
756
752
  client.start_market_events_listener(request.trade_id)
757
- await client.start_user_events_listener(request.trade_id, request.symbol)
753
+ client.start_user_events_listener(request.trade_id, request.symbol)
758
754
  response.success = True
759
755
  return response
760
756
 
@@ -763,37 +759,43 @@ class Martin(mr.MartinBase):
763
759
  if open_client := OpenClient.get_client(request.client_id):
764
760
  client = open_client.client
765
761
  logger.info(f"StopStream request for {request.symbol} on {client.exchange}")
766
- await stop_stream(client, request.trade_id)
762
+ await stop_stream_ex(client, request.trade_id)
767
763
  response.success = True
768
764
  else:
769
765
  response.success = False
770
766
  return response
771
767
 
772
768
  async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
773
- response = mr.SimpleResponse()
774
- check_time = time.time() - Martin.ticker_update_time.get(request.trade_id, time.time() - WSS_TICKER_TIMEOUT - 1)
775
- if check_time < WSS_TICKER_TIMEOUT:
776
- response.success = True
777
- else:
778
- response.success = False
769
+ last_update = Martin.ticker_update_time.get(request.trade_id, 0)
770
+ check_time = time.time() - last_update
771
+ success = check_time < WSS_TICKER_TIMEOUT
772
+ response = mr.SimpleResponse(success=success)
773
+ if not success:
774
+ Martin.ticker_update_time.pop(request.trade_id, None)
779
775
  logger.warning(f"CheckStream request failed for {request.trade_id}")
780
776
  return response
781
777
 
782
778
  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()
785
- OpenClient.remove_client(request.account_name)
779
+ await self.stop_stream(request)
780
+ if client := OpenClient.get_client(request.client_id).client:
781
+ if user_session := client.user_session:
782
+ await user_session.stop(request.trade_id)
783
+ if session := client.http:
784
+ await session.close_session()
785
+ OpenClient.remove_client(request.client_id)
786
786
  return mr.SimpleResponse(success=True)
787
787
 
788
788
 
789
- async def stop_stream(client, trade_id):
789
+ async def stop_stream_ex(client, trade_id):
790
790
  await client.stop_events_listener(trade_id)
791
791
  client.events.unregister(client.exchange, trade_id)
792
792
  [await _queue.put(trade_id) for _queue in client.stream_queue.get(trade_id, [])]
793
793
  await asyncio.sleep(0)
794
794
  client.on_order_update_queues.pop(trade_id, None)
795
795
  client.stream_queue.pop(trade_id, None)
796
+ Martin.ticker_update_time.pop(trade_id, None)
796
797
  gc.collect(generation=2)
798
+ malloc_trim()
797
799
 
798
800
 
799
801
  async def event_handler(_queue, client, trade_id, _event_type, event):
@@ -802,10 +804,7 @@ async def event_handler(_queue, client, trade_id, _event_type, event):
802
804
  except asyncio.QueueFull:
803
805
  logger.warning(f"For {_event_type} asyncio queue full and wold be closed")
804
806
  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}")
807
+ await stop_stream_ex(client, trade_id)
809
808
 
810
809
 
811
810
  def is_port_in_use(port: int) -> bool:
@@ -825,8 +824,8 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
825
824
  await server.wait_closed()
826
825
 
827
826
  for oc in OpenClient.open_clients:
828
- if oc.client.http.session:
829
- await oc.client.http.session.close()
827
+ if oc.client.http:
828
+ await oc.client.http.close_session()
830
829
 
831
830
  [task.cancel() for task in asyncio.all_tasks() if not task.done() and task is not asyncio.current_task()]
832
831
 
@@ -836,8 +835,8 @@ def main():
836
835
  asyncio.run(amain())
837
836
  except grpclib.exceptions.StreamTerminatedError:
838
837
  pass # Task cancellation should not be logged as an error
839
- except Exception as ex:
840
- print(f"Exception: {ex}")
838
+ except Exception as expt:
839
+ print(f"Exception: {expt}")
841
840
  print(traceback.format_exc())
842
841
 
843
842
 
@@ -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,10 +69,15 @@ 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
74
  self.session = aiohttp.ClientSession(trust_env=True, timeout=TIMEOUT)
60
75
 
76
+ async def close_session(self):
77
+ if self.session:
78
+ await self.session.close()
79
+ self.session = None
80
+
61
81
  async def handle_errors(self, response, path=None):
62
82
  if response.status >= 500:
63
83
  raise ExchangeError(
@@ -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):
@@ -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"
@@ -15,7 +15,7 @@ def fetch_server_time(res: list) -> dict | None:
15
15
  return None
16
16
 
17
17
 
18
- 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:
19
19
  symbols = []
20
20
  symbols_price = {}
21
21
  for pair in tickers:
@@ -98,7 +98,7 @@ def orders(res: list, response_type=None) -> list:
98
98
  return binance_orders
99
99
 
100
100
 
101
- def order(res: {}, response_type=None) -> {}:
101
+ def order(res: dict, response_type=None) -> dict:
102
102
  symbol = res.get('instId').replace('-', '')
103
103
  order_id = int(res.get('ordId'))
104
104
  order_list_id = -1
@@ -186,7 +186,7 @@ def order(res: {}, response_type=None) -> {}:
186
186
  }
187
187
 
188
188
 
189
- def place_order_response(res: {}, req: {}) -> {}:
189
+ def place_order_response(res: dict, req: dict) -> dict:
190
190
  return {
191
191
  "symbol": req["instId"].replace('-', ''),
192
192
  "orderId": int(res["ordId"]),
@@ -230,7 +230,7 @@ def order_book(res: dict) -> Dict[str, Union[int, List[List[str]]]]:
230
230
  return binance_order_book
231
231
 
232
232
 
233
- def ticker_price_change_statistics(res: {}) -> {}:
233
+ def ticker_price_change_statistics(res: dict) -> dict:
234
234
  price_change = str(Decimal(res.get('last')) - Decimal(res.get('open24h')))
235
235
  price_change_percent = str(100 * (Decimal(res.get('last')) - Decimal(res.get('open24h'))) /
236
236
  Decimal(res.get('open24h')))
@@ -263,14 +263,14 @@ def ticker_price_change_statistics(res: {}) -> {}:
263
263
  }
264
264
 
265
265
 
266
- def fetch_symbol_price_ticker(res: {}, symbol) -> {}:
266
+ def fetch_symbol_price_ticker(res: dict, symbol) -> dict:
267
267
  return {
268
268
  "symbol": symbol,
269
269
  "price": res.get('last')
270
270
  }
271
271
 
272
272
 
273
- def ticker(res: {}) -> {}:
273
+ def ticker(res: dict) -> dict:
274
274
  symbol = res.get('instId').replace('-', '')
275
275
  return {
276
276
  'stream': f"{symbol.lower()}@miniTicker",
@@ -344,7 +344,7 @@ def interval2value(_interval: str) -> int:
344
344
  return resolution.get(_interval, 0)
345
345
 
346
346
 
347
- def candle(res: list, symbol: str = None, ch_type: str = None) -> {}:
347
+ def candle(res: list, symbol: str = None, ch_type: str = None) -> dict:
348
348
  symbol = symbol.replace('-', '').lower()
349
349
  start_time = int(res[0])
350
350
  _interval = ch_type.replace('kline_', '')
@@ -378,7 +378,7 @@ def candle(res: list, symbol: str = None, ch_type: str = None) -> {}:
378
378
  }
379
379
 
380
380
 
381
- def order_book_ws(res: {}, symbol: str) -> {}:
381
+ def order_book_ws(res: dict, symbol: str) -> dict:
382
382
  symbol = symbol.replace('-', '').lower()
383
383
  return {
384
384
  'stream': f"{symbol}@depth5",
@@ -386,7 +386,7 @@ def order_book_ws(res: {}, symbol: str) -> {}:
386
386
  }
387
387
 
388
388
 
389
- def on_funds_update(res: {}) -> {}:
389
+ def on_funds_update(res: dict) -> dict:
390
390
  event_time = int(time.time() * 1000)
391
391
  data = res.get('details')
392
392
  funds = []
@@ -408,7 +408,7 @@ def on_funds_update(res: {}) -> {}:
408
408
  }
409
409
 
410
410
 
411
- def on_order_update(res: {}) -> {}:
411
+ def on_order_update(res: dict) -> dict:
412
412
  # print(f"on_order_update.res: {res}")
413
413
  order_quantity = res.get('sz')
414
414
  order_price = res.get('px')
@@ -464,7 +464,7 @@ def on_order_update(res: {}) -> {}:
464
464
  }
465
465
 
466
466
 
467
- def on_balance_update(res: list, buffer: dict, transfer: bool) -> ():
467
+ def on_balance_update(res: list, buffer: dict, transfer: bool) -> tuple:
468
468
  res_diff = []
469
469
  for i in res:
470
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}:{self.trade_id} 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:
@@ -245,22 +262,25 @@ class EventsDataStream:
245
262
  await asyncio.sleep(interval)
246
263
  try:
247
264
  await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
248
- except (ConnectionClosed, asyncio.exceptions.TimeoutError):
249
- pass # handled elsewhere
265
+ except (ConnectionClosed, asyncio.exceptions.TimeoutError, AttributeError):
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")
260
- await self.websocket.close()
276
+ self.logger.warning("From HTX server PING timeout exceeded")
277
+ if self.websocket:
278
+ await self.websocket.close()
261
279
 
262
280
 
263
281
  class MarketEventsDataStream(EventsDataStream):
282
+ __slots__ = ('channel', 'candles_max_time', 'endpoint')
283
+
264
284
  def __init__(self, client, endpoint, exchange, trade_id, channel=None):
265
285
  super().__init__(client, endpoint, exchange, trade_id)
266
286
  self.channel = channel
@@ -271,7 +291,7 @@ class MarketEventsDataStream(EventsDataStream):
271
291
  self.endpoint = f"{endpoint}/stream?streams={combined_streams}"
272
292
 
273
293
  async def start_wss(self):
274
- logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
294
+ self.logger.info(f"Start market WSS {self.channel or ''} for {self.exchange}")
275
295
  symbol = None
276
296
  ch_type = str()
277
297
  request = {}
@@ -330,12 +350,12 @@ class MarketEventsDataStream(EventsDataStream):
330
350
  elif ch_type == 'depth5':
331
351
  request = {'sub': f"market.{symbol}.depth.step0"}
332
352
 
333
- self.tasks_manage(self.htx_keepalive(interval=30))
353
+ tasks_manage(self.tasks, self.htx_keepalive(interval=30), f"htx_keepalive-{symbol}-{ch_type}")
334
354
 
335
355
  await self.ws_listener(request, symbol, ch_type)
336
356
 
337
357
  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}")
358
+ # self.logger.info(f"MARKET_handle_event.content: symbol: {symbol}, ch_type: {ch_type}, content: {content}")
339
359
  self.try_count = 0
340
360
  if self.exchange == 'bitfinex':
341
361
  if 'candles' in ch_type:
@@ -390,6 +410,8 @@ class MarketEventsDataStream(EventsDataStream):
390
410
 
391
411
 
392
412
  class HbpPrivateEventsDataStream(EventsDataStream):
413
+ __slots__ = ('symbol',)
414
+
393
415
  def __init__(self, client, endpoint, exchange, trade_id, symbol):
394
416
  super().__init__(client, endpoint, exchange, trade_id)
395
417
  self.symbol = symbol
@@ -423,7 +445,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
423
445
  "action": "sub",
424
446
  "ch": f"trade.clearing#{self.symbol.lower()}#0"
425
447
  }
426
- self.tasks_manage(self.htx_keepalive())
448
+ tasks_manage(self.tasks, self.htx_keepalive(), f"htx_keepalive-user-{self.symbol}")
427
449
  await self.ws_listener(request, symbol=self.symbol)
428
450
 
429
451
  async def _handle_event(self, msg_data, *args):
@@ -446,11 +468,12 @@ class HbpPrivateEventsDataStream(EventsDataStream):
446
468
  content = hbp.on_order_update(self.client.active_orders[order_id])
447
469
 
448
470
  if content:
449
- logger.debug(f"HTXPrivateEvents.content: {content}")
471
+ self.logger.debug(f"HTXPrivateEvents.content: {content}")
450
472
  await self.client.events.wrap_event(content).fire(self.trade_id)
451
473
 
452
474
 
453
475
  class BfxPrivateEventsDataStream(EventsDataStream):
476
+ __slots__ = ()
454
477
 
455
478
  async def start_wss(self):
456
479
  ts = int(time.time() * 1000)
@@ -505,6 +528,8 @@ class BfxPrivateEventsDataStream(EventsDataStream):
505
528
 
506
529
 
507
530
  class OkxPrivateEventsDataStream(EventsDataStream):
531
+ __slots__ = ('symbol',)
532
+
508
533
  def __init__(self, client, endpoint, exchange, trade_id, symbol):
509
534
  super().__init__(client, endpoint, exchange, trade_id)
510
535
  self.symbol = symbol
@@ -561,6 +586,7 @@ class OkxPrivateEventsDataStream(EventsDataStream):
561
586
 
562
587
 
563
588
  class BBTPrivateEventsDataStream(EventsDataStream):
589
+ __slots__ = ()
564
590
 
565
591
  async def start_wss(self):
566
592
  ts = int((time.time() + 1) * 1000)
@@ -591,7 +617,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
591
617
  await self.client.events.wrap_event(content).fire(self.trade_id)
592
618
  content = None
593
619
  elif ch_type == 'order.spot':
594
- # logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
620
+ # self.logger.info(f"_handle_event: ch_type: {ch_type}, msg_data: {msg_data}")
595
621
  event = msg_data[0]
596
622
  if event.get('orderStatus') in ("Cancelled", "PartiallyFilledCanceled"):
597
623
  self.client.wss_buffer[f"oc-{event.get('orderId')}"] = bbt.order(event, response_type=True)
@@ -603,6 +629,7 @@ class BBTPrivateEventsDataStream(EventsDataStream):
603
629
 
604
630
 
605
631
  class UserEventsDataStream(EventsDataStream):
632
+ __slots__ = ()
606
633
 
607
634
  async def start_wss(self):
608
635
  await self.websocket.send(
@@ -620,5 +647,5 @@ class UserEventsDataStream(EventsDataStream):
620
647
  await self.ws_listener(request)
621
648
 
622
649
  async def _handle_event(self, content):
623
- # logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
650
+ # self.logger.debug(f"UserEventsDataStream._handle_event.content: {content}")
624
651
  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.1.0",
23
+ "crypto-ws-api==2.1.4",
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.2",
24
27
  "expiringdict~=1.2.2",
25
28
  "betterproto==2.0.0b7",
26
29
  "grpclib~=0.4.8"