exchanges-wrapper 2.1.36__tar.gz → 2.1.40__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.36 → exchanges_wrapper-2.1.40}/PKG-INFO +3 -4
  2. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/README.md +0 -1
  3. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/__init__.py +2 -2
  4. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/client.py +3 -5
  5. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/events.py +2 -8
  6. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/exch_srv.py +4 -4
  7. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/http_client.py +10 -7
  8. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/bybit.py +39 -23
  9. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/pyproject.toml +2 -2
  10. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/LICENSE.md +0 -0
  11. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/definitions.py +0 -0
  12. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/errors.py +0 -0
  13. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  14. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/lib.py +0 -0
  15. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/martin/__init__.py +0 -0
  16. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  17. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/huobi.py +0 -0
  18. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/okx.py +0 -0
  19. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/proto/martin.proto +0 -0
  20. {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/web_sockets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.36
3
+ Version: 2.1.40
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
@@ -15,10 +15,10 @@ License-File: LICENSE.md
15
15
  Requires-Dist: crypto-ws-api==2.0.20
16
16
  Requires-Dist: pyotp==2.9.0
17
17
  Requires-Dist: simplejson==3.20.1
18
- Requires-Dist: aiohttp~=3.11.18
18
+ Requires-Dist: aiohttp~=3.12.13
19
19
  Requires-Dist: expiringdict~=1.2.2
20
20
  Requires-Dist: betterproto==2.0.0b7
21
- Requires-Dist: grpclib~=0.4.7
21
+ Requires-Dist: grpclib~=0.4.8
22
22
  Project-URL: Source, https://github.com/DogsTailFarmer/exchanges-wrapper
23
23
 
24
24
  <h1 align="center"><img align="center" src="https://raw.githubusercontent.com/gist/DogsTailFarmer/167eaf65cebfe95d954082c7f181a2cc/raw/a67270de8663ad3de4733330ff64c9ba3153f87d/Logo%202v3.svg" width="75">Crypto exchanges API/WSS wrapper with grpc powered server</h1>
@@ -29,7 +29,6 @@ Project-URL: Source, https://github.com/DogsTailFarmer/exchanges-wrapper
29
29
 
30
30
  ***
31
31
  <a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
32
- <a href="https://codeclimate.com/github/DogsTailFarmer/exchanges-wrapper/maintainability"><img src="https://api.codeclimate.com/v1/badges/f333ab9b1f3024699e09/maintainability" /></a>
33
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>
34
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>
35
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>
@@ -6,7 +6,6 @@
6
6
 
7
7
  ***
8
8
  <a href="https://pypi.org/project/exchanges-wrapper/"><img src="https://img.shields.io/pypi/v/exchanges-wrapper" alt="PyPI version"></a>
9
- <a href="https://codeclimate.com/github/DogsTailFarmer/exchanges-wrapper/maintainability"><img src="https://api.codeclimate.com/v1/badges/f333ab9b1f3024699e09/maintainability" /></a>
10
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>
11
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>
12
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,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.36"
15
+ __version__ = "2.1.40"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -23,7 +23,7 @@ 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
+ 'Server', 'GRPCError', 'Status', 'Channel', 'graceful_exit', 'exceptions'
27
27
  ]
28
28
 
29
29
  WORK_PATH = Path(Path.home(), ".MartinBinance")
@@ -128,9 +128,7 @@ class Client:
128
128
  precision = symbol_infos["baseAssetPrecision"]
129
129
  if precision > self.highest_precision:
130
130
  self.highest_precision = precision
131
- symbol_infos["filters"] = dict(
132
- map(lambda x: (x.pop("filterType"), x), symbol_infos["filters"])
133
- )
131
+ symbol_infos["filters"] = {x.pop("filterType"): x for x in symbol_infos["filters"]}
134
132
  self.symbols[symbol] = symbol_infos
135
133
  decimal.getcontext().prec = (self.highest_precision + 4) # for operations and rounding
136
134
  if self.exchange == 'bybit':
@@ -186,7 +184,7 @@ class Client:
186
184
  break
187
185
  await asyncio.sleep(0.1)
188
186
 
189
- async def start_market_events_listener(self, _trade_id):
187
+ def start_market_events_listener(self, _trade_id):
190
188
  _events = self.events.registered_streams.get(self.exchange, {}).get(_trade_id, set())
191
189
  if self.exchange == 'binance':
192
190
  market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
@@ -1262,7 +1260,7 @@ class Client:
1262
1260
  # print(f"fetch_open_orders.res: {res}")
1263
1261
  binance_res = okx.orders(res, response_type=response_type)
1264
1262
  elif self.exchange == 'bybit':
1265
- params = {'category': 'spot', 'symbol': symbol}
1263
+ params = {'category': 'spot', 'symbol': symbol, 'limit': 50}
1266
1264
  res, _ = await self.http.send_api_call("/v5/order/realtime", signed=True, **params)
1267
1265
  binance_res = bbt.orders(res.get('list', []), response_type=response_type)
1268
1266
  return binance_res
@@ -241,9 +241,7 @@ class OutboundAccountPositionWrapper(EventWrapper):
241
241
  super().__init__(event_data, handlers)
242
242
  self.event_time = event_data["E"]
243
243
  self.last_update = event_data["u"]
244
- self.balances = dict(
245
- map(lambda x: (x["a"], {"free": x["f"], "locked": x["l"]}), event_data["B"])
246
- )
244
+ self.balances = {x["a"]: {"free": x["f"], "locked": x["l"]} for x in event_data["B"]}
247
245
 
248
246
 
249
247
  # BALANCE UPDATE
@@ -308,8 +306,4 @@ class ListStatus(EventWrapper):
308
306
  self.list_order_status = event_data["L"]
309
307
  self.list_reject_reason = event_data["r"]
310
308
  self.list_client_order_id = event_data["C"]
311
- # noinspection PyArgumentList
312
- self.orders = dict(
313
- map(lambda x: (x["s"], {"orderId": x["i"], "clientOrderId": x["c"]})),
314
- event_data["O"],
315
- )
309
+ self.orders = {x["s"]: {"orderId": x["i"], "clientOrderId": x["c"]} for x in event_data["O"]}
@@ -94,7 +94,7 @@ class OpenClient:
94
94
 
95
95
  # noinspection PyPep8Naming,PyMethodMayBeStatic
96
96
  class Martin(mr.MartinBase):
97
- rate_limit_reached_time = None
97
+ rate_limit_reached_time = 0
98
98
  rate_limiter = None
99
99
  ticker_update_time = {}
100
100
 
@@ -162,9 +162,9 @@ class Martin(mr.MartinBase):
162
162
  open_client = OpenClient.get_client(request.client_id)
163
163
  client = open_client.client
164
164
  if Martin.rate_limit_reached_time:
165
- if time.time() - Martin.rate_limit_reached_time > 30:
165
+ if time.time() - Martin.rate_limit_reached_time > 600 if client.exchange == 'bybit' else 60:
166
166
  client.http.rate_limit_reached = False
167
- Martin.rate_limit_reached_time = None
167
+ Martin.rate_limit_reached_time = 0
168
168
  logger.info(f"RateLimit error clear for {open_client.name}, trying one else time")
169
169
  _success = True
170
170
  elif client.http.rate_limit_reached:
@@ -753,7 +753,7 @@ class Martin(mr.MartinBase):
753
753
  len(v[request.trade_id]) for v in client.events.registered_streams.values() if request.trade_id in v
754
754
  )
755
755
  logger.info(f"Start WS streams for {open_client.name}")
756
- await client.start_market_events_listener(request.trade_id)
756
+ client.start_market_events_listener(request.trade_id)
757
757
  await client.start_user_events_listener(request.trade_id, request.symbol)
758
758
  response.success = True
759
759
  return response
@@ -107,20 +107,23 @@ class HttpClient:
107
107
  elif payload.get('retCode') == 10002:
108
108
  raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
109
109
  elif payload.get('retCode') == 10006:
110
- self.rate_limit_handler.fire_exceeded_rate_limit(path)
111
110
  logger.warning(f"ByBit API: {payload.get('retMsg')}")
111
+ self.rate_limit_handler.fire_exceeded_rate_limit(path)
112
112
  return payload.get('result'), payload.get('time')
113
113
  else:
114
114
  raise ExchangeError(f"API request failed: {response.status}:{response.reason}:{payload}")
115
- elif self.exchange == 'huobi' and payload and (payload.get('status') == 'ok' or payload.get('ok')):
115
+
116
+ if self.exchange == 'huobi' and payload and (payload.get('status') == 'ok' or payload.get('ok')):
116
117
  return payload.get('data', payload.get('tick'))
117
- elif self.exchange == 'okx' and payload and payload.get('code') == '0':
118
+
119
+ if self.exchange == 'okx' and payload and payload.get('code') == '0':
118
120
  return payload.get('data', [])
119
- elif self.exchange not in ('binance', 'bitfinex') \
121
+
122
+ if self.exchange not in ('binance', 'bitfinex') \
120
123
  or (self.exchange == 'binance' and payload and "code" in payload):
121
124
  raise ExchangeError(f"API request failed: {response.status}:{response.reason}:{payload}")
122
- else:
123
- return payload
125
+
126
+ return payload
124
127
 
125
128
  async def send_api_call(self,
126
129
  path,
@@ -142,7 +145,7 @@ class HttpClient:
142
145
  if self.exchange == 'bybit':
143
146
  self.rate_limit_handler.update(path, response.headers)
144
147
 
145
- return await self.handle_errors(response)
148
+ return await self.handle_errors(response, path)
146
149
  except (aiohttp.ClientConnectionError, asyncio.exceptions.TimeoutError):
147
150
  await self.session.close()
148
151
  raise ExchangeError("HTTP ClientConnectionError, the connection will be restored")
@@ -2,13 +2,14 @@
2
2
  Parser for convert Bybit REST API/WSS V5 response to Binance like result
3
3
  """
4
4
  import asyncio
5
- import random
6
5
  import time
7
6
  from decimal import Decimal
8
7
  import logging
9
8
 
10
9
  logger = logging.getLogger(__name__)
11
10
 
11
+ TIMEOUT = 30
12
+
12
13
 
13
14
  class OrderBook:
14
15
  def __init__(self, snapshot) -> None:
@@ -54,8 +55,13 @@ class OrderBook:
54
55
  class RateLimitHandler:
55
56
  def __init__(self):
56
57
  self.stats = {} # {'path': [limit_status, start_time, reset_time, range_window, limit]}
58
+ self.path_fired = set()
59
+ self.path_count = {}
57
60
 
58
61
  def update(self, path, headers):
62
+ if self.path_count[path]:
63
+ self.path_count[path] -= 1
64
+
59
65
  if limit := int(headers.get('X-Bapi-Limit', '0')):
60
66
  limit_status = int(headers['X-Bapi-Limit-Status'])
61
67
  now = int(time.time() * 1000)
@@ -72,34 +78,44 @@ class RateLimitHandler:
72
78
  else:
73
79
  range_window = max(_range_window, now - start_time)
74
80
  n = (_limit_status - limit_status) if _limit_status > limit_status else 1
75
- reset_time += n * range_window / limit
81
+ reset_time += int(n * range_window / limit)
76
82
  self.stats[path] = [limit_status, start_time, reset_time, range_window, limit]
83
+ else:
84
+ self.path_fired.discard(path)
85
+
86
+ async def wait(self, path, count=0):
87
+ if path not in self.path_count:
88
+ self.path_count[path] = 0
89
+ self.path_count[path] += 1
90
+
91
+ _count = count or self.path_count[path]
77
92
 
78
- async def wait(self, path):
79
- if self.stats.get(path) is None:
80
- return
81
- limit_status, start_time, reset_time, range_window, limit = self.stats[path]
82
- min_delay = range_window / limit
83
- now = int(time.time() * 1000)
84
- if limit_status <= 1:
85
- delay = max(
86
- random.randint(1000, 2000), #NOSONAR python:S2245
87
- max(reset_time, start_time + range_window) - now
88
- ) / 1000
93
+ if stats := self.stats.get(path):
94
+ limit_status, start_time, reset_time, range_window, limit = stats
95
+ if limit_status <= 1 or count >= limit - 1:
96
+ delay = (
97
+ (
98
+ max(reset_time, start_time + range_window)
99
+ - time.time() * 1000
100
+ + _count * range_window / limit
101
+ ) / 1000
102
+ )
103
+ await asyncio.sleep(delay)
89
104
  else:
90
- delay = max(min_delay, reset_time - now) / 1000
91
- await asyncio.sleep(delay)
105
+ if path in self.path_fired:
106
+ start_time = time.time()
107
+ while not self.stats.get(path) and (time.time() - start_time < TIMEOUT):
108
+ await asyncio.sleep(0.1)
109
+ await self.wait(path, count=_count)
110
+ else:
111
+ self.path_fired.add(path)
92
112
 
93
113
  def fire_exceeded_rate_limit(self, path):
94
114
  if stats := self.stats.get(path):
95
- limit_status, start_time, _reset_time, range_window, limit = stats
96
- self.stats[path] = [
97
- limit_status,
98
- start_time,
99
- int(time.time() * 1000) + range_window,
100
- range_window,
101
- limit
102
- ]
115
+ _, start_time, _, range_window, limit = stats
116
+ new_reset_time = int(time.time() * 1000) + range_window
117
+ self.stats[path] = [0, start_time, new_reset_time, range_window, limit]
118
+ logger.warning(f"Bybit Rate limit exceeded for path: {path}")
103
119
 
104
120
 
105
121
  def fetch_server_time(res: dict) -> dict:
@@ -20,10 +20,10 @@ dependencies = [
20
20
  "crypto-ws-api==2.0.20",
21
21
  "pyotp==2.9.0",
22
22
  "simplejson==3.20.1",
23
- "aiohttp~=3.11.18",
23
+ "aiohttp~=3.12.13",
24
24
  "expiringdict~=1.2.2",
25
25
  "betterproto==2.0.0b7",
26
- "grpclib~=0.4.7"
26
+ "grpclib~=0.4.8"
27
27
  ]
28
28
 
29
29
  [tool.flit.module]