exchanges-wrapper 2.1.35__tar.gz → 2.1.37__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.35 → exchanges_wrapper-2.1.37}/PKG-INFO +1 -1
  2. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/__init__.py +2 -2
  3. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/client.py +1 -1
  4. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/exch_srv.py +5 -7
  5. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/http_client.py +10 -7
  6. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/martin/__init__.py +0 -1
  7. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/bybit.py +39 -23
  8. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/proto/martin.proto +0 -1
  9. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/README.md +0 -0
  11. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/definitions.py +0 -0
  12. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/errors.py +0 -0
  13. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/events.py +0 -0
  14. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  15. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/lib.py +0 -0
  16. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  17. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/huobi.py +0 -0
  18. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/okx.py +0 -0
  19. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/web_sockets.py +0 -0
  20. {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.35
3
+ Version: 2.1.37
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,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.35"
15
+ __version__ = "2.1.37"
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")
@@ -1262,7 +1262,7 @@ class Client:
1262
1262
  # print(f"fetch_open_orders.res: {res}")
1263
1263
  binance_res = okx.orders(res, response_type=response_type)
1264
1264
  elif self.exchange == 'bybit':
1265
- params = {'category': 'spot', 'symbol': symbol}
1265
+ params = {'category': 'spot', 'symbol': symbol, 'limit': 50}
1266
1266
  res, _ = await self.http.send_api_call("/v5/order/realtime", signed=True, **params)
1267
1267
  binance_res = bbt.orders(res.get('list', []), response_type=response_type)
1268
1268
  return binance_res
@@ -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:
@@ -261,8 +261,7 @@ class Martin(mr.MartinBase):
261
261
  trade_id=request.trade_id,
262
262
  symbol=request.symbol,
263
263
  order_id=request.order_id,
264
- origin_client_order_id=request.client_order_id,
265
- receive_window=None
264
+ origin_client_order_id=request.client_order_id
266
265
  )
267
266
  logger.debug(f"{msg_header}: {res}")
268
267
 
@@ -378,8 +377,7 @@ class Martin(mr.MartinBase):
378
377
  request,
379
378
  rate_limit=True,
380
379
  asset=request.asset,
381
- need_btc_valuation=request.need_btc_valuation,
382
- receive_window=request.receive_window
380
+ need_btc_valuation=request.need_btc_valuation
383
381
  )
384
382
  response.items = list(map(json.dumps, res))
385
383
  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")
@@ -39,7 +39,6 @@ class FetchFundingWalletRequest(betterproto.Message):
39
39
  trade_id: str = betterproto.string_field(2)
40
40
  asset: str = betterproto.string_field(3)
41
41
  need_btc_valuation: bool = betterproto.bool_field(4)
42
- receive_window: int = betterproto.int64_field(5)
43
42
 
44
43
 
45
44
  @dataclass(eq=False, repr=False)
@@ -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:
@@ -56,7 +56,6 @@ message FetchFundingWalletRequest{
56
56
  string trade_id = 2;
57
57
  string asset = 3;
58
58
  bool need_btc_valuation = 4;
59
- int64 receive_window = 5;
60
59
  }
61
60
 
62
61
  message CancelOrderResponse{