exchanges-wrapper 2.1.27__tar.gz → 2.1.29__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.27 → exchanges_wrapper-2.1.29}/PKG-INFO +1 -1
  2. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/client.py +25 -19
  4. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/exch_srv.py +17 -10
  5. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/exch_srv_cfg.toml.template +6 -6
  6. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/http_client.py +20 -5
  7. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/parsers/bybit.py +54 -0
  8. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/LICENSE.md +0 -0
  9. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/README.md +0 -0
  10. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/definitions.py +0 -0
  11. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/errors.py +0 -0
  12. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/events.py +0 -0
  13. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/lib.py +0 -0
  14. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/martin/__init__.py +0 -0
  15. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  16. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/parsers/huobi.py +0 -0
  17. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/parsers/okx.py +0 -0
  18. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/proto/martin.proto +0 -0
  19. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/exchanges_wrapper/web_sockets.py +0 -0
  20. {exchanges_wrapper-2.1.27 → exchanges_wrapper-2.1.29}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.27
3
+ Version: 2.1.29
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.9
@@ -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.27"
15
+ __version__ = "2.1.29"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -472,13 +472,15 @@ class Client:
472
472
  'status': 'SUCCESS',
473
473
  'startTime': max(self.ts_start[symbol], (int(time.time()) - 300) * 1000)
474
474
  }
475
+ _res = []
475
476
  # Internal transfer, ie from Funding to UTA account
476
477
  res, ts = await self.http.send_api_call(
477
478
  "/v5/asset/transfer/query-inter-transfer-list",
478
479
  signed=True,
479
480
  **params
480
481
  )
481
- _res = bbt.on_balance_update(res['list'], ts, symbol, 'internal')
482
+ if res:
483
+ _res = bbt.on_balance_update(res['list'], ts, symbol, 'internal')
482
484
 
483
485
  # Universal Transfer Records, ie from Sub account to Main account
484
486
  res, ts = await self.http.send_api_call(
@@ -486,13 +488,14 @@ class Client:
486
488
  signed=True,
487
489
  **params
488
490
  )
489
- _res += bbt.on_balance_update(
490
- res['list'],
491
- ts,
492
- symbol,
493
- 'universal',
494
- uid=self.account_uid
495
- )
491
+ if res:
492
+ _res += bbt.on_balance_update(
493
+ res['list'],
494
+ ts,
495
+ symbol,
496
+ 'universal',
497
+ uid=self.account_uid
498
+ )
496
499
 
497
500
  if not _res:
498
501
  # Get Transaction Log
@@ -506,12 +509,13 @@ class Client:
506
509
  signed=True,
507
510
  **params
508
511
  )
509
- _res += bbt.on_balance_update(
510
- res['list'],
511
- ts,
512
- symbol,
513
- 'log'
514
- )
512
+ if res:
513
+ _res += bbt.on_balance_update(
514
+ res['list'],
515
+ ts,
516
+ symbol,
517
+ 'log'
518
+ )
515
519
 
516
520
  for i in _res:
517
521
  _id = next(iter(i))
@@ -1005,11 +1009,13 @@ class Client:
1005
1009
  elif self.exchange == 'bybit':
1006
1010
  params = {
1007
1011
  'category': 'spot',
1008
- 'symbol': symbol,
1009
- 'orderId': str(order_id),
1010
- 'orderLinkId': str(origin_client_order_id),
1012
+ 'symbol': symbol
1011
1013
  }
1012
- res, _ = await self.http.send_api_call("/v5/order/history", signed=True, **params)
1014
+ if order_id:
1015
+ params['orderId'] = str(order_id)
1016
+ else:
1017
+ params['orderLinkId'] = str(origin_client_order_id)
1018
+ res, _ = await self.http.send_api_call("/v5/order/realtime", signed=True, **params)
1013
1019
  if res["list"]:
1014
1020
  b_res = bbt.order(res["list"][0], response_type=response_type)
1015
1021
  return b_res
@@ -1311,7 +1317,7 @@ class Client:
1311
1317
  elif self.exchange == 'bybit':
1312
1318
  params = {'category': 'spot', 'symbol': symbol}
1313
1319
  res, _ = await self.http.send_api_call("/v5/order/realtime", signed=True, **params)
1314
- binance_res = bbt.orders(res['list'], response_type=response_type)
1320
+ binance_res = bbt.orders(res.get('list', []), response_type=response_type)
1315
1321
  return binance_res
1316
1322
 
1317
1323
  # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#all-orders-user_data
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
+ from typing import Any, AsyncGenerator
3
4
 
4
5
  import grpclib.exceptions
5
6
 
@@ -25,6 +26,9 @@ from exchanges_wrapper.lib import (
25
26
  REST_RATE_LIMIT_INTERVAL,
26
27
  FILTER_TYPE_MAP,
27
28
  )
29
+ from exchanges_wrapper.martin import StreamResponse, SimpleResponse, OnKlinesUpdateResponse, OnTickerUpdateResponse, \
30
+ FetchOrderBookResponse
31
+
28
32
  #
29
33
  HEARTBEAT = 1 # sec
30
34
  MAX_QUEUE_SIZE = 100
@@ -154,12 +158,13 @@ class Martin(mr.MartinBase):
154
158
  async def reset_rate_limit(self, request: mr.OpenClientConnectionId) -> mr.SimpleResponse:
155
159
  Martin.rate_limiter = max(Martin.rate_limiter or 0, request.rate_limiter)
156
160
  _success = False
157
- client = OpenClient.get_client(request.client_id).client
161
+ open_client = OpenClient.get_client(request.client_id)
162
+ client = open_client.client
158
163
  if Martin.rate_limit_reached_time:
159
164
  if time.time() - Martin.rate_limit_reached_time > 30:
160
165
  client.http.rate_limit_reached = False
161
166
  Martin.rate_limit_reached_time = None
162
- logger.info("RateLimit error clear, trying one else time")
167
+ logger.info(f"RateLimit error clear for {open_client.name}, trying one else time")
163
168
  _success = True
164
169
  elif client.http.rate_limit_reached:
165
170
  Martin.rate_limit_reached_time = time.time()
@@ -434,7 +439,7 @@ class Martin(mr.MartinBase):
434
439
  response.items = list(map(json.dumps, res))
435
440
  return response
436
441
 
437
- async def on_klines_update(self, request: mr.FetchKlinesRequest) -> mr.OnKlinesUpdateResponse:
442
+ async def on_klines_update(self, request: mr.FetchKlinesRequest) -> AsyncGenerator[OnKlinesUpdateResponse, Any]:
438
443
  response = mr.OnKlinesUpdateResponse()
439
444
  open_client = OpenClient.get_client(request.client_id)
440
445
  client = open_client.client
@@ -508,7 +513,7 @@ class Martin(mr.MartinBase):
508
513
  response.items = list(map(json.dumps, res))
509
514
  return response
510
515
 
511
- async def on_ticker_update(self, request: mr.MarketRequest) -> mr.OnTickerUpdateResponse:
516
+ async def on_ticker_update(self, request: mr.MarketRequest) -> AsyncGenerator[OnTickerUpdateResponse, Any]:
512
517
  response = mr.OnTickerUpdateResponse()
513
518
  open_client = OpenClient.get_client(request.client_id)
514
519
  client = open_client.client
@@ -543,7 +548,7 @@ class Martin(mr.MartinBase):
543
548
  yield response
544
549
  _queue.task_done()
545
550
 
546
- async def on_order_book_update(self, request: mr.MarketRequest) -> mr.FetchOrderBookResponse:
551
+ async def on_order_book_update(self, request: mr.MarketRequest) -> AsyncGenerator[FetchOrderBookResponse, Any]:
547
552
  response = mr.FetchOrderBookResponse()
548
553
  open_client = OpenClient.get_client(request.client_id)
549
554
  client = open_client.client
@@ -577,7 +582,7 @@ class Martin(mr.MartinBase):
577
582
  yield response
578
583
  _queue.task_done()
579
584
 
580
- async def on_funds_update(self, request: mr.OnFundsUpdateRequest) -> mr.StreamResponse:
585
+ async def on_funds_update(self, request: mr.OnFundsUpdateRequest) -> AsyncGenerator[StreamResponse, Any]:
581
586
  response = mr.StreamResponse()
582
587
  open_client = OpenClient.get_client(request.client_id)
583
588
  client = open_client.client
@@ -597,7 +602,7 @@ class Martin(mr.MartinBase):
597
602
  yield response
598
603
  _queue.task_done()
599
604
 
600
- async def on_balance_update(self, request: mr.MarketRequest) -> mr.StreamResponse:
605
+ async def on_balance_update(self, request: mr.MarketRequest) -> AsyncGenerator[StreamResponse, Any]:
601
606
  response = mr.StreamResponse()
602
607
  open_client = OpenClient.get_client(request.client_id)
603
608
  client = open_client.client
@@ -640,10 +645,10 @@ class Martin(mr.MartinBase):
640
645
  response.event = json.dumps(balance)
641
646
  yield response
642
647
 
643
- if _get_event_from_queue:
644
- _queue.task_done()
648
+ if _get_event_from_queue:
649
+ _queue.task_done()
645
650
 
646
- async def on_order_update(self, request: mr.MarketRequest) -> mr.SimpleResponse:
651
+ async def on_order_update(self, request: mr.MarketRequest) -> AsyncGenerator[SimpleResponse, Any]:
647
652
  response = mr.SimpleResponse()
648
653
  open_client = OpenClient.get_client(request.client_id)
649
654
  client = open_client.client
@@ -787,6 +792,7 @@ class Martin(mr.MartinBase):
787
792
  OpenClient.remove_client(request.account_name)
788
793
  return mr.SimpleResponse(success=True)
789
794
 
795
+
790
796
  async def stop_stream(client, trade_id):
791
797
  await client.stop_events_listener(trade_id)
792
798
  client.events.unregister(client.exchange, trade_id)
@@ -808,6 +814,7 @@ async def event_handler(_queue, client, trade_id, _event_type, event):
808
814
  MAX_QUEUE_SIZE += int(MAX_QUEUE_SIZE / 10)
809
815
  logger.info(f"MAX_QUEUE_SIZE was updated: new value is {MAX_QUEUE_SIZE}")
810
816
 
817
+
811
818
  def is_port_in_use(port: int) -> bool:
812
819
  import socket
813
820
  with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -1,6 +1,6 @@
1
1
  # Parameters for exchanges-wrapper REST API Server exch_srv.py
2
2
  # Copyright © 2021-2025 Jerry Fedorenko aka VM
3
- # __version__ = "2.1.21"
3
+ # __version__ = "2.1.29"
4
4
  # region endpoint
5
5
  [endpoint]
6
6
  [endpoint.binance]
@@ -38,12 +38,12 @@
38
38
  ws_api = 'wss://api.huobi.pro/ws/trade'
39
39
 
40
40
  [endpoint.okx]
41
- api_public = 'https://aws.okx.com'
42
- api_auth = 'https://aws.okx.com'
43
- ws_public = 'wss://wsaws.okx.com:8443/ws/v5/public'
44
- ws_auth = 'wss://wsaws.okx.com:8443/ws/v5/private'
41
+ api_public = 'https://www.okx.com'
42
+ api_auth = 'https://www.okx.com'
43
+ ws_public = 'wss://ws.okx.com:8443/ws/v5/public'
44
+ ws_auth = 'wss://ws.okx.com:8443/ws/v5/private'
45
45
  ws_business = 'wss://ws.okx.com:8443/ws/v5/business'
46
- api_test = 'https://aws.okx.com'
46
+ api_test = 'https://www.okx.com'
47
47
  ws_test = 'wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999'
48
48
 
49
49
  [endpoint.bybit]
@@ -15,6 +15,9 @@ from exchanges_wrapper.errors import (
15
15
  HTTPError,
16
16
  QueryCanceled,
17
17
  )
18
+ from exchanges_wrapper.parsers.bybit import RateLimitHandler
19
+
20
+
18
21
  logger = logging.getLogger(__name__)
19
22
 
20
23
  AJ = 'application/json'
@@ -39,6 +42,7 @@ class HttpClient:
39
42
  self._session_mutex = asyncio.Lock()
40
43
  self.ex_imps = {} # exchanges implementation
41
44
  self.declare_exchanges_implementation()
45
+ self.rate_limit_handler = RateLimitHandler() if self.exchange == 'bybit' else None
42
46
 
43
47
  def declare_exchanges_implementation(self):
44
48
  # noinspection PyTypeChecker
@@ -55,15 +59,15 @@ class HttpClient:
55
59
  async with self._session_mutex:
56
60
  self.session = aiohttp.ClientSession(timeout=TIMEOUT)
57
61
 
58
- async def handle_errors(self, response):
62
+ async def handle_errors(self, response, path=None):
59
63
  if response.status >= 500:
60
64
  raise ExchangeError(
61
65
  f"{'API request rejected' if self.exchange == 'bitfinex' else 'An issue occurred on exchange side'}:"
62
66
  f" {response.status}: {response.url}: {response.reason}"
63
67
  )
64
- if response.status == 429:
68
+ if response.status == 429 or (self.exchange == 'bybit' and response.status == STATUS_FORBIDDEN):
65
69
  logger.error(f"API RateLimitReached: {response.url}")
66
- self.rate_limit_reached = self.exchange in ('binance', 'okx')
70
+ self.rate_limit_reached = self.exchange in ('binance', 'okx', 'bybit')
67
71
  raise RateLimitReached(RateLimitReached.message)
68
72
 
69
73
  try:
@@ -75,6 +79,7 @@ class HttpClient:
75
79
  if response.status == STATUS_BAD_REQUEST:
76
80
  if payload:
77
81
  if payload.get("error", "") == "ERR_RATE_LIMIT":
82
+ self.rate_limit_reached = True
78
83
  raise RateLimitReached(RateLimitReached.message)
79
84
  elif self.exchange == 'binance' and payload.get('code', 0) == -1021:
80
85
  raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
@@ -102,6 +107,10 @@ class HttpClient:
102
107
  return payload.get('result'), payload.get('time')
103
108
  elif payload.get('retCode') == 10002:
104
109
  raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
110
+ elif payload.get('retCode') == 10006:
111
+ self.rate_limit_handler.fire_exceeded_rate_limit(path)
112
+ logger.warning(f"ByBit API: {payload.get('retMsg')}")
113
+ return payload.get('result'), payload.get('time')
105
114
  else:
106
115
  raise ExchangeError(f"API request failed: {response.status}:{response.reason}:{payload}")
107
116
  elif self.exchange == 'huobi' and payload and (payload.get('status') == 'ok' or payload.get('ok')):
@@ -126,10 +135,14 @@ class HttpClient:
126
135
  raise QueryCanceled(QueryCanceled.message)
127
136
  return await self.ex_imps[self.exchange](path, method, signed, send_api_key, endpoint, timeout, **kwargs)
128
137
 
129
- async def send_request(self, method, url, timeout, query_kwargs):
138
+ async def send_request(self, method, url, timeout, query_kwargs, path=None):
130
139
  await self._create_session_if_required()
131
140
  try:
132
141
  async with self.session.request(method, url, timeout=timeout, **query_kwargs) as response:
142
+
143
+ if self.exchange == 'bybit':
144
+ self.rate_limit_handler.update(path, response.headers)
145
+
133
146
  return await self.handle_errors(response)
134
147
  except (aiohttp.ClientConnectionError, asyncio.exceptions.TimeoutError):
135
148
  await self.session.close()
@@ -184,6 +197,8 @@ class HttpClient:
184
197
  return await self.send_request(method, url, timeout, query_kwargs)
185
198
 
186
199
  async def _bybit_request(self, path, method, signed, _send_api_key, endpoint, timeout, **kwargs):
200
+ await self.rate_limit_handler.wait(path)
201
+
187
202
  url = endpoint or self.endpoint
188
203
  query_kwargs = {}
189
204
  data = None
@@ -214,7 +229,7 @@ class HttpClient:
214
229
 
215
230
  query_kwargs['data'] = data
216
231
  query_kwargs['headers'] = headers
217
- return await self.send_request(method, url, timeout, query_kwargs)
232
+ return await self.send_request(method, url, timeout, query_kwargs, path)
218
233
 
219
234
  async def _huobi_request(self, path, method, signed, _send_api_key, endpoint, timeout, **kwargs):
220
235
  _endpoint = endpoint or self.endpoint
@@ -1,6 +1,9 @@
1
1
  """
2
2
  Parser for convert Bybit REST API/WSS V5 response to Binance like result
3
3
  """
4
+ import asyncio
5
+ import random
6
+ import time
4
7
  from decimal import Decimal
5
8
  import logging
6
9
 
@@ -49,6 +52,57 @@ class OrderBook:
49
52
  # return self
50
53
 
51
54
 
55
+ class RateLimitHandler:
56
+ def __init__(self):
57
+ self.stats = {} # {'path': [limit_status, start_time, reset_time, range_window, limit]}
58
+
59
+ def update(self, path, headers):
60
+ if limit := int(headers.get('X-Bapi-Limit', '0')):
61
+ limit_status = int(headers['X-Bapi-Limit-Status'])
62
+ now = int(time.time() * 1000)
63
+ reset_time = max(int(headers['X-Bapi-Limit-Reset-Timestamp']), now)
64
+ if stats := self.stats.get(path):
65
+ _limit_status, start_time, _reset_time, _range_window, _limit = stats
66
+ else:
67
+ _limit_status = limit_status
68
+ start_time = now
69
+ _range_window = 1000
70
+ if limit_status == limit - 1:
71
+ start_time = now
72
+ range_window = max(_range_window, 1000)
73
+ else:
74
+ range_window = max(_range_window, now - start_time)
75
+ n = (_limit_status - limit_status) if _limit_status > limit_status else 1
76
+ reset_time += n * range_window / limit
77
+ self.stats[path] = [limit_status, start_time, reset_time, range_window, limit]
78
+
79
+ async def wait(self, path):
80
+ if self.stats.get(path) is None:
81
+ return
82
+ limit_status, start_time, reset_time, range_window, limit = self.stats[path]
83
+ min_delay = range_window / limit
84
+ now = int(time.time() * 1000)
85
+ if limit_status <= 1:
86
+ delay = max(
87
+ random.randint(1000, 2000), #NOSONAR python:S2245
88
+ max(reset_time, start_time + range_window) - now
89
+ ) / 1000
90
+ else:
91
+ delay = max(min_delay, reset_time - now) / 1000
92
+ await asyncio.sleep(delay)
93
+
94
+ def fire_exceeded_rate_limit(self, path):
95
+ if stats := self.stats.get(path):
96
+ limit_status, start_time, _reset_time, range_window, limit = stats
97
+ self.stats[path] = [
98
+ limit_status,
99
+ start_time,
100
+ int(time.time() * 1000) + range_window,
101
+ range_window,
102
+ limit
103
+ ]
104
+
105
+
52
106
  def fetch_server_time(res: dict) -> dict:
53
107
  return {'serverTime': int(res['timeNano']) // 1000000}
54
108