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.
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/PKG-INFO +1 -1
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/__init__.py +2 -2
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/client.py +1 -1
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/exch_srv.py +5 -7
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/http_client.py +10 -7
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/martin/__init__.py +0 -1
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/bybit.py +39 -23
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/proto/martin.proto +0 -1
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/README.md +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/events.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/lib.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/bitfinex.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/huobi.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/parsers/okx.py +0 -0
- {exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/web_sockets.py +0 -0
- {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.
|
|
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.
|
|
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 =
|
|
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 >
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
|
|
119
|
+
if self.exchange == 'okx' and payload and payload.get('code') == '0':
|
|
118
120
|
return payload.get('data', [])
|
|
119
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{exchanges_wrapper-2.1.35 → exchanges_wrapper-2.1.37}/exchanges_wrapper/exch_srv_cfg.toml.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|