exchanges-wrapper 2.1.42__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.42 → exchanges_wrapper-2.1.43}/PKG-INFO +3 -3
  2. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/client.py +21 -24
  4. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv.py +38 -39
  5. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/web_sockets.py +4 -3
  6. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/pyproject.toml +2 -2
  7. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/LICENSE.md +0 -0
  8. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/README.md +0 -0
  9. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/definitions.py +0 -0
  10. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/errors.py +0 -0
  11. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/events.py +0 -0
  12. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  13. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/http_client.py +0 -0
  14. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/lib.py +0 -0
  15. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/martin/__init__.py +0 -0
  16. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  17. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bybit.py +0 -0
  18. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/huobi.py +0 -0
  19. {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/okx.py +0 -0
  20. {exchanges_wrapper-2.1.42 → 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.42
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.3
15
+ Requires-Dist: crypto-ws-api==2.1.4
16
16
  Requires-Dist: pyotp==2.9.0
17
17
  Requires-Dist: simplejson==3.20.2
18
- Requires-Dist: aiohttp~=3.13.0
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.42"
15
+ __version__ = "2.1.43"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -51,7 +51,7 @@ class Client:
51
51
  'passphrase', 'endpoint_api_public', 'endpoint_ws_public',
52
52
  'endpoint_api_auth', 'endpoint_ws_auth', 'endpoint_ws_api',
53
53
  'ws_add_on', 'master_email', 'master_name', 'two_fa', 'http',
54
- 'user_session', 'loaded', 'symbols', 'highest_precision',
54
+ 'user_session', 'symbols', 'highest_precision',
55
55
  'rate_limits', 'data_streams', 'active_orders', 'wss_buffer',
56
56
  'stream_queue', 'on_order_update_queues', 'account_id',
57
57
  'account_uid', 'main_account_id', 'main_account_uid',
@@ -98,7 +98,6 @@ class Client:
98
98
  else:
99
99
  self.user_session = None
100
100
  #
101
- self.loaded = False
102
101
  self.symbols = {}
103
102
  self.highest_precision = None
104
103
  self.rate_limits = None
@@ -150,7 +149,6 @@ class Client:
150
149
  logger.info(f"Main ByBit account UID: {self.main_account_uid}, sub-UID: {self.account_uid}")
151
150
  # load rate limits
152
151
  self.rate_limits = infos["rateLimits"]
153
- self.loaded = True
154
152
  logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
155
153
 
156
154
  async def close(self):
@@ -229,7 +227,7 @@ class Client:
229
227
  await self.user_session.stop(_trade_id)
230
228
 
231
229
  def assert_symbol_exists(self, symbol):
232
- if self.loaded and symbol not in self.symbols:
230
+ if symbol not in self.symbols:
233
231
  raise ExchangePyError(f"Symbol {symbol} is not valid according to the loaded exchange infos")
234
232
 
235
233
  def symbol_to_bfx(self, symbol) -> str:
@@ -279,32 +277,31 @@ class Client:
279
277
  def refine_amount(self, symbol, amount: Union[str, Decimal], _quote=False):
280
278
  if type(amount) is str: # to save time for developers
281
279
  amount = Decimal(amount)
282
- if self.loaded:
283
- precision = self.symbols[symbol]["baseAssetPrecision"]
284
- lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
285
- step_size = Decimal(lot_size_filter["stepSize"])
286
- # noinspection PyStringFormat
287
- amount = (
288
- (f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
289
- .rstrip("0")
290
- .rstrip(".")
291
- )
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
+ )
292
290
  return amount
293
291
 
294
292
  def refine_price(self, symbol, price: Union[str, Decimal]):
295
293
  if isinstance(price, str): # to save time for developers
296
294
  price = Decimal(price)
297
295
 
298
- if self.loaded:
299
- precision = self.symbols[symbol]["baseAssetPrecision"]
300
- price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
301
- price = price - (price % Decimal(price_filter["tickSize"]))
302
- # noinspection PyStringFormat
303
- price = (
304
- (f"%.{precision}f" % truncate(price, precision))
305
- .rstrip("0")
306
- .rstrip(".")
307
- )
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
+ )
308
305
  return price
309
306
 
310
307
  def assert_symbol(self, symbol):
@@ -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
@@ -41,24 +44,12 @@ HEARTBEAT = 1 # sec
41
44
  MAX_QUEUE_SIZE = 100
42
45
  WSS_TICKER_TIMEOUT = 600 # sec
43
46
  #
44
- logger = logging.getLogger(__name__)
45
- formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
46
- #
47
- fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1000000, backupCount=10)
48
- fh.setFormatter(formatter)
49
- fh.setLevel(logging.DEBUG)
50
- #
51
- sh = logging.StreamHandler()
52
- sh.setFormatter(formatter)
53
- sh.setLevel(logging.INFO)
54
- #
55
- root_logger = logging.getLogger()
56
- root_logger.setLevel(min([fh.level, sh.level]))
57
- root_logger.addHandler(fh)
58
- root_logger.addHandler(sh)
59
-
60
- logging.basicConfig()
47
+ logger = set_logger(__name__, LOG_FILE, file_level=logging.DEBUG, set_root_logger=True)
61
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)
62
53
 
63
54
 
64
55
  class OpenClient:
@@ -94,9 +85,9 @@ class OpenClient:
94
85
  return res
95
86
 
96
87
  @classmethod
97
- def remove_client(cls, _account_name):
98
- cls.open_clients[:] = [i for i in cls.open_clients if i.name != _account_name]
99
-
88
+ def remove_client(cls, _id):
89
+ # noinspection PyTypeHints
90
+ cls.open_clients[:] = [i for i in cls.open_clients if id(i) != _id]
100
91
 
101
92
  # noinspection PyPep8Naming,PyMethodMayBeStatic
102
93
  class Martin(mr.MartinBase):
@@ -144,7 +135,7 @@ class Martin(mr.MartinBase):
144
135
  await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
145
136
  except asyncio.exceptions.TimeoutError:
146
137
  await OpenClient.get_client(client_id).client.http.close_session()
147
- OpenClient.remove_client(request.account_name)
138
+ OpenClient.remove_client(client_id)
148
139
  raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
149
140
  except Exception as ex:
150
141
  logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
@@ -258,7 +249,7 @@ class Martin(mr.MartinBase):
258
249
 
259
250
  async def fetch_order(self, request: mr.FetchOrderRequest) -> mr.FetchOrderResponse:
260
251
  response = mr.FetchOrderResponse()
261
- res, _, msg_header = await self.send_request(
252
+ res, client, msg_header = await self.send_request(
262
253
  'fetch_order',
263
254
  request,
264
255
  rate_limit=True,
@@ -269,14 +260,15 @@ class Martin(mr.MartinBase):
269
260
  )
270
261
  logger.debug(f"{msg_header}: {res}")
271
262
 
272
- if res and request.filled_update_call and Decimal(res['executedQty']):
273
- request.order_id = res['orderId']
274
- 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)
275
267
  response.from_pydict(res)
276
268
  return response
277
269
 
278
- async def create_trade_stream_event(self, request, order):
279
- 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(
280
272
  'fetch_order_trade_list',
281
273
  request,
282
274
  trade_id=request.trade_id,
@@ -284,8 +276,6 @@ class Martin(mr.MartinBase):
284
276
  order_id=request.order_id
285
277
  )
286
278
 
287
- _queue = client.on_order_update_queues.get(request.trade_id)
288
-
289
279
  for trade in trades:
290
280
  trade['updateTime'] = trade.pop('time')
291
281
  trade |= {
@@ -529,10 +519,12 @@ class Martin(mr.MartinBase):
529
519
  _event_type = f"{_symbol}@miniTicker"
530
520
  client.events.register_event(functools.partial(event_handler, _queue, client, request.trade_id, _event_type),
531
521
  _event_type, client.exchange, request.trade_id)
522
+ Martin.ticker_update_time[request.trade_id] = time.time()
532
523
  while True:
533
524
  _event = await _queue.get()
534
525
  if isinstance(_event, str) and _event == request.trade_id:
535
526
  client.stream_queue.get(request.trade_id, set()).discard(_queue)
527
+ Martin.ticker_update_time.pop(request.trade_id, None)
536
528
  logger.info(f"OnTickerUpdate: Stop loop for {open_client.name}: {request.symbol}")
537
529
  return
538
530
  else:
@@ -774,19 +766,23 @@ class Martin(mr.MartinBase):
774
766
  return response
775
767
 
776
768
  async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
777
- response = mr.SimpleResponse()
778
- check_time = time.time() - Martin.ticker_update_time.get(request.trade_id, time.time() - WSS_TICKER_TIMEOUT - 1)
779
- if check_time < WSS_TICKER_TIMEOUT:
780
- response.success = True
781
- else:
782
- 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)
783
775
  logger.warning(f"CheckStream request failed for {request.trade_id}")
784
776
  return response
785
777
 
786
778
  async def client_restart(self, request: mr.MarketRequest) -> mr.SimpleResponse:
787
- if session := OpenClient.get_client(request.client_id).client.http:
788
- await session.close_session()
789
- 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)
790
786
  return mr.SimpleResponse(success=True)
791
787
 
792
788
 
@@ -797,7 +793,9 @@ async def stop_stream_ex(client, trade_id):
797
793
  await asyncio.sleep(0)
798
794
  client.on_order_update_queues.pop(trade_id, None)
799
795
  client.stream_queue.pop(trade_id, None)
796
+ Martin.ticker_update_time.pop(trade_id, None)
800
797
  gc.collect(generation=2)
798
+ malloc_trim()
801
799
 
802
800
 
803
801
  async def event_handler(_queue, client, trade_id, _event_type, event):
@@ -841,5 +839,6 @@ def main():
841
839
  print(f"Exception: {expt}")
842
840
  print(traceback.format_exc())
843
841
 
842
+
844
843
  if __name__ == '__main__':
845
844
  main()
@@ -84,7 +84,7 @@ class EventsDataStream:
84
84
  except ConnectionClosed as ex:
85
85
  self.websocket = None
86
86
  ct = str(datetime.now(timezone.utc).replace(tzinfo=None) - start_time).rsplit('.')[0]
87
- self.logger.info(f"WSS life time for {self.exchange} is {ct}")
87
+ self.logger.info(f"WSS life time for {self.exchange}:{self.trade_id} is {ct}")
88
88
  if ex.rcvd and ex.rcvd.code == 4000:
89
89
  self.logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
90
90
  break
@@ -262,7 +262,7 @@ class EventsDataStream:
262
262
  await asyncio.sleep(interval)
263
263
  try:
264
264
  await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
265
- except (ConnectionClosed, asyncio.exceptions.TimeoutError):
265
+ except (ConnectionClosed, asyncio.exceptions.TimeoutError, AttributeError):
266
266
  break
267
267
 
268
268
  async def htx_keepalive(self, interval=60):
@@ -274,7 +274,8 @@ class EventsDataStream:
274
274
  else:
275
275
  self.ping = 1
276
276
  self.logger.warning("From HTX server PING timeout exceeded")
277
- await self.websocket.close()
277
+ if self.websocket:
278
+ await self.websocket.close()
278
279
 
279
280
 
280
281
  class MarketEventsDataStream(EventsDataStream):
@@ -20,10 +20,10 @@ dynamic = ["version", "description"]
20
20
  requires-python = ">=3.10"
21
21
 
22
22
  dependencies = [
23
- "crypto-ws-api==2.1.3",
23
+ "crypto-ws-api==2.1.4",
24
24
  "pyotp==2.9.0",
25
25
  "simplejson==3.20.2",
26
- "aiohttp~=3.13.0",
26
+ "aiohttp~=3.13.2",
27
27
  "expiringdict~=1.2.2",
28
28
  "betterproto==2.0.0b7",
29
29
  "grpclib~=0.4.8"