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.
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/PKG-INFO +3 -4
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/README.md +0 -1
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/__init__.py +2 -2
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/client.py +3 -5
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/events.py +2 -8
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/exch_srv.py +4 -4
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/http_client.py +10 -7
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/bybit.py +39 -23
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/pyproject.toml +2 -2
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/lib.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/martin/__init__.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/bitfinex.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/huobi.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/parsers/okx.py +0 -0
- {exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/exchanges_wrapper/proto/martin.proto +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"] =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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")
|
|
@@ -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:
|
|
@@ -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.
|
|
23
|
+
"aiohttp~=3.12.13",
|
|
24
24
|
"expiringdict~=1.2.2",
|
|
25
25
|
"betterproto==2.0.0b7",
|
|
26
|
-
"grpclib~=0.4.
|
|
26
|
+
"grpclib~=0.4.8"
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[tool.flit.module]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{exchanges_wrapper-2.1.36 → exchanges_wrapper-2.1.40}/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
|
|
File without changes
|