exchanges-wrapper 2.1.42__tar.gz → 2.1.43__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.42 → exchanges_wrapper-2.1.43}/PKG-INFO +3 -3
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/__init__.py +1 -1
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/client.py +21 -24
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv.py +38 -39
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/web_sockets.py +4 -3
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/pyproject.toml +2 -2
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/README.md +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/events.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/http_client.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/lib.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/martin/__init__.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bitfinex.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/bybit.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/huobi.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/parsers/okx.py +0 -0
- {exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/exchanges_wrapper/proto/martin.proto +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.43
|
|
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,10 +12,10 @@ Classifier: Operating System :: Unix
|
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
13
|
Classifier: Operating System :: MacOS
|
|
14
14
|
License-File: LICENSE.md
|
|
15
|
-
Requires-Dist: crypto-ws-api==2.1.
|
|
15
|
+
Requires-Dist: crypto-ws-api==2.1.4
|
|
16
16
|
Requires-Dist: pyotp==2.9.0
|
|
17
17
|
Requires-Dist: simplejson==3.20.2
|
|
18
|
-
Requires-Dist: aiohttp~=3.13.
|
|
18
|
+
Requires-Dist: aiohttp~=3.13.2
|
|
19
19
|
Requires-Dist: expiringdict~=1.2.2
|
|
20
20
|
Requires-Dist: betterproto==2.0.0b7
|
|
21
21
|
Requires-Dist: grpclib~=0.4.8
|
|
@@ -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.43"
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
import shutil
|
|
@@ -51,7 +51,7 @@ class Client:
|
|
|
51
51
|
'passphrase', 'endpoint_api_public', 'endpoint_ws_public',
|
|
52
52
|
'endpoint_api_auth', 'endpoint_ws_auth', 'endpoint_ws_api',
|
|
53
53
|
'ws_add_on', 'master_email', 'master_name', 'two_fa', 'http',
|
|
54
|
-
'user_session', '
|
|
54
|
+
'user_session', 'symbols', 'highest_precision',
|
|
55
55
|
'rate_limits', 'data_streams', 'active_orders', 'wss_buffer',
|
|
56
56
|
'stream_queue', 'on_order_update_queues', 'account_id',
|
|
57
57
|
'account_uid', 'main_account_id', 'main_account_uid',
|
|
@@ -98,7 +98,6 @@ class Client:
|
|
|
98
98
|
else:
|
|
99
99
|
self.user_session = None
|
|
100
100
|
#
|
|
101
|
-
self.loaded = False
|
|
102
101
|
self.symbols = {}
|
|
103
102
|
self.highest_precision = None
|
|
104
103
|
self.rate_limits = None
|
|
@@ -150,7 +149,6 @@ class Client:
|
|
|
150
149
|
logger.info(f"Main ByBit account UID: {self.main_account_uid}, sub-UID: {self.account_uid}")
|
|
151
150
|
# load rate limits
|
|
152
151
|
self.rate_limits = infos["rateLimits"]
|
|
153
|
-
self.loaded = True
|
|
154
152
|
logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
|
|
155
153
|
|
|
156
154
|
async def close(self):
|
|
@@ -229,7 +227,7 @@ class Client:
|
|
|
229
227
|
await self.user_session.stop(_trade_id)
|
|
230
228
|
|
|
231
229
|
def assert_symbol_exists(self, symbol):
|
|
232
|
-
if
|
|
230
|
+
if symbol not in self.symbols:
|
|
233
231
|
raise ExchangePyError(f"Symbol {symbol} is not valid according to the loaded exchange infos")
|
|
234
232
|
|
|
235
233
|
def symbol_to_bfx(self, symbol) -> str:
|
|
@@ -279,32 +277,31 @@ class Client:
|
|
|
279
277
|
def refine_amount(self, symbol, amount: Union[str, Decimal], _quote=False):
|
|
280
278
|
if type(amount) is str: # to save time for developers
|
|
281
279
|
amount = Decimal(amount)
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
280
|
+
|
|
281
|
+
precision = self.symbols[symbol]["baseAssetPrecision"]
|
|
282
|
+
lot_size_filter = self.symbols[symbol]["filters"]["LOT_SIZE"]
|
|
283
|
+
step_size = Decimal(lot_size_filter["stepSize"])
|
|
284
|
+
# noinspection PyStringFormat
|
|
285
|
+
amount = (
|
|
286
|
+
(f"%.{precision}f" % truncate(amount if _quote else (amount - amount % step_size), precision))
|
|
287
|
+
.rstrip("0")
|
|
288
|
+
.rstrip(".")
|
|
289
|
+
)
|
|
292
290
|
return amount
|
|
293
291
|
|
|
294
292
|
def refine_price(self, symbol, price: Union[str, Decimal]):
|
|
295
293
|
if isinstance(price, str): # to save time for developers
|
|
296
294
|
price = Decimal(price)
|
|
297
295
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
)
|
|
296
|
+
precision = self.symbols[symbol]["baseAssetPrecision"]
|
|
297
|
+
price_filter = self.symbols[symbol]["filters"]["PRICE_FILTER"]
|
|
298
|
+
price = price - (price % Decimal(price_filter["tickSize"]))
|
|
299
|
+
# noinspection PyStringFormat
|
|
300
|
+
price = (
|
|
301
|
+
(f"%.{precision}f" % truncate(price, precision))
|
|
302
|
+
.rstrip("0")
|
|
303
|
+
.rstrip(".")
|
|
304
|
+
)
|
|
308
305
|
return price
|
|
309
306
|
|
|
310
307
|
def assert_symbol(self, symbol):
|
|
@@ -8,6 +8,8 @@ import grpclib.exceptions
|
|
|
8
8
|
from exchanges_wrapper import __version__ as VER_EW
|
|
9
9
|
# noinspection PyPep8Naming
|
|
10
10
|
from crypto_ws_api import __version__ as VER_CW
|
|
11
|
+
from crypto_ws_api.ws_session import set_logger
|
|
12
|
+
|
|
11
13
|
import time
|
|
12
14
|
import weakref
|
|
13
15
|
import gc
|
|
@@ -16,8 +18,9 @@ import asyncio
|
|
|
16
18
|
import functools
|
|
17
19
|
# noinspection PyPackageRequirements
|
|
18
20
|
import ujson as json
|
|
19
|
-
import logging
|
|
21
|
+
import logging
|
|
20
22
|
from decimal import Decimal
|
|
23
|
+
import ctypes, ctypes.util
|
|
21
24
|
|
|
22
25
|
import exchanges_wrapper.martin as mr
|
|
23
26
|
from exchanges_wrapper import WORK_PATH, LOG_FILE, errors, Server, Status, GRPCError, graceful_exit
|
|
@@ -41,24 +44,12 @@ HEARTBEAT = 1 # sec
|
|
|
41
44
|
MAX_QUEUE_SIZE = 100
|
|
42
45
|
WSS_TICKER_TIMEOUT = 600 # sec
|
|
43
46
|
#
|
|
44
|
-
logger = logging.
|
|
45
|
-
formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
|
|
46
|
-
#
|
|
47
|
-
fh = logging.handlers.RotatingFileHandler(LOG_FILE, maxBytes=1000000, backupCount=10)
|
|
48
|
-
fh.setFormatter(formatter)
|
|
49
|
-
fh.setLevel(logging.DEBUG)
|
|
50
|
-
#
|
|
51
|
-
sh = logging.StreamHandler()
|
|
52
|
-
sh.setFormatter(formatter)
|
|
53
|
-
sh.setLevel(logging.INFO)
|
|
54
|
-
#
|
|
55
|
-
root_logger = logging.getLogger()
|
|
56
|
-
root_logger.setLevel(min([fh.level, sh.level]))
|
|
57
|
-
root_logger.addHandler(fh)
|
|
58
|
-
root_logger.addHandler(sh)
|
|
59
|
-
|
|
60
|
-
logging.basicConfig()
|
|
47
|
+
logger = set_logger(__name__, LOG_FILE, file_level=logging.DEBUG, set_root_logger=True)
|
|
61
48
|
logging.getLogger('hpack').setLevel(logging.INFO)
|
|
49
|
+
logging.getLogger('grpclib.protocol').setLevel(logging.INFO)
|
|
50
|
+
|
|
51
|
+
def malloc_trim(trim_type: int = 0):
|
|
52
|
+
ctypes.CDLL(ctypes.util.find_library('c')).malloc_trim(trim_type)
|
|
62
53
|
|
|
63
54
|
|
|
64
55
|
class OpenClient:
|
|
@@ -94,9 +85,9 @@ class OpenClient:
|
|
|
94
85
|
return res
|
|
95
86
|
|
|
96
87
|
@classmethod
|
|
97
|
-
def remove_client(cls,
|
|
98
|
-
|
|
99
|
-
|
|
88
|
+
def remove_client(cls, _id):
|
|
89
|
+
# noinspection PyTypeHints
|
|
90
|
+
cls.open_clients[:] = [i for i in cls.open_clients if id(i) != _id]
|
|
100
91
|
|
|
101
92
|
# noinspection PyPep8Naming,PyMethodMayBeStatic
|
|
102
93
|
class Martin(mr.MartinBase):
|
|
@@ -144,7 +135,7 @@ class Martin(mr.MartinBase):
|
|
|
144
135
|
await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
|
|
145
136
|
except asyncio.exceptions.TimeoutError:
|
|
146
137
|
await OpenClient.get_client(client_id).client.http.close_session()
|
|
147
|
-
OpenClient.remove_client(
|
|
138
|
+
OpenClient.remove_client(client_id)
|
|
148
139
|
raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
|
|
149
140
|
except Exception as ex:
|
|
150
141
|
logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
|
|
@@ -258,7 +249,7 @@ class Martin(mr.MartinBase):
|
|
|
258
249
|
|
|
259
250
|
async def fetch_order(self, request: mr.FetchOrderRequest) -> mr.FetchOrderResponse:
|
|
260
251
|
response = mr.FetchOrderResponse()
|
|
261
|
-
res,
|
|
252
|
+
res, client, msg_header = await self.send_request(
|
|
262
253
|
'fetch_order',
|
|
263
254
|
request,
|
|
264
255
|
rate_limit=True,
|
|
@@ -269,14 +260,15 @@ class Martin(mr.MartinBase):
|
|
|
269
260
|
)
|
|
270
261
|
logger.debug(f"{msg_header}: {res}")
|
|
271
262
|
|
|
272
|
-
if
|
|
273
|
-
request.
|
|
274
|
-
|
|
263
|
+
if _queue := client.on_order_update_queues.get(request.trade_id):
|
|
264
|
+
if res and request.filled_update_call and Decimal(res['executedQty']):
|
|
265
|
+
request.order_id = res['orderId']
|
|
266
|
+
await self.create_trade_stream_event(request, res, _queue)
|
|
275
267
|
response.from_pydict(res)
|
|
276
268
|
return response
|
|
277
269
|
|
|
278
|
-
async def create_trade_stream_event(self, request, order):
|
|
279
|
-
trades,
|
|
270
|
+
async def create_trade_stream_event(self, request, order, _queue):
|
|
271
|
+
trades, _, msg_header = await self.send_request(
|
|
280
272
|
'fetch_order_trade_list',
|
|
281
273
|
request,
|
|
282
274
|
trade_id=request.trade_id,
|
|
@@ -284,8 +276,6 @@ class Martin(mr.MartinBase):
|
|
|
284
276
|
order_id=request.order_id
|
|
285
277
|
)
|
|
286
278
|
|
|
287
|
-
_queue = client.on_order_update_queues.get(request.trade_id)
|
|
288
|
-
|
|
289
279
|
for trade in trades:
|
|
290
280
|
trade['updateTime'] = trade.pop('time')
|
|
291
281
|
trade |= {
|
|
@@ -529,10 +519,12 @@ class Martin(mr.MartinBase):
|
|
|
529
519
|
_event_type = f"{_symbol}@miniTicker"
|
|
530
520
|
client.events.register_event(functools.partial(event_handler, _queue, client, request.trade_id, _event_type),
|
|
531
521
|
_event_type, client.exchange, request.trade_id)
|
|
522
|
+
Martin.ticker_update_time[request.trade_id] = time.time()
|
|
532
523
|
while True:
|
|
533
524
|
_event = await _queue.get()
|
|
534
525
|
if isinstance(_event, str) and _event == request.trade_id:
|
|
535
526
|
client.stream_queue.get(request.trade_id, set()).discard(_queue)
|
|
527
|
+
Martin.ticker_update_time.pop(request.trade_id, None)
|
|
536
528
|
logger.info(f"OnTickerUpdate: Stop loop for {open_client.name}: {request.symbol}")
|
|
537
529
|
return
|
|
538
530
|
else:
|
|
@@ -774,19 +766,23 @@ class Martin(mr.MartinBase):
|
|
|
774
766
|
return response
|
|
775
767
|
|
|
776
768
|
async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
777
|
-
|
|
778
|
-
check_time = time.time() -
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
769
|
+
last_update = Martin.ticker_update_time.get(request.trade_id, 0)
|
|
770
|
+
check_time = time.time() - last_update
|
|
771
|
+
success = check_time < WSS_TICKER_TIMEOUT
|
|
772
|
+
response = mr.SimpleResponse(success=success)
|
|
773
|
+
if not success:
|
|
774
|
+
Martin.ticker_update_time.pop(request.trade_id, None)
|
|
783
775
|
logger.warning(f"CheckStream request failed for {request.trade_id}")
|
|
784
776
|
return response
|
|
785
777
|
|
|
786
778
|
async def client_restart(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
779
|
+
await self.stop_stream(request)
|
|
780
|
+
if client := OpenClient.get_client(request.client_id).client:
|
|
781
|
+
if user_session := client.user_session:
|
|
782
|
+
await user_session.stop(request.trade_id)
|
|
783
|
+
if session := client.http:
|
|
784
|
+
await session.close_session()
|
|
785
|
+
OpenClient.remove_client(request.client_id)
|
|
790
786
|
return mr.SimpleResponse(success=True)
|
|
791
787
|
|
|
792
788
|
|
|
@@ -797,7 +793,9 @@ async def stop_stream_ex(client, trade_id):
|
|
|
797
793
|
await asyncio.sleep(0)
|
|
798
794
|
client.on_order_update_queues.pop(trade_id, None)
|
|
799
795
|
client.stream_queue.pop(trade_id, None)
|
|
796
|
+
Martin.ticker_update_time.pop(trade_id, None)
|
|
800
797
|
gc.collect(generation=2)
|
|
798
|
+
malloc_trim()
|
|
801
799
|
|
|
802
800
|
|
|
803
801
|
async def event_handler(_queue, client, trade_id, _event_type, event):
|
|
@@ -841,5 +839,6 @@ def main():
|
|
|
841
839
|
print(f"Exception: {expt}")
|
|
842
840
|
print(traceback.format_exc())
|
|
843
841
|
|
|
842
|
+
|
|
844
843
|
if __name__ == '__main__':
|
|
845
844
|
main()
|
|
@@ -84,7 +84,7 @@ class EventsDataStream:
|
|
|
84
84
|
except ConnectionClosed as ex:
|
|
85
85
|
self.websocket = None
|
|
86
86
|
ct = str(datetime.now(timezone.utc).replace(tzinfo=None) - start_time).rsplit('.')[0]
|
|
87
|
-
self.logger.info(f"WSS life time for {self.exchange} is {ct}")
|
|
87
|
+
self.logger.info(f"WSS life time for {self.exchange}:{self.trade_id} is {ct}")
|
|
88
88
|
if ex.rcvd and ex.rcvd.code == 4000:
|
|
89
89
|
self.logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
|
|
90
90
|
break
|
|
@@ -262,7 +262,7 @@ class EventsDataStream:
|
|
|
262
262
|
await asyncio.sleep(interval)
|
|
263
263
|
try:
|
|
264
264
|
await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
|
|
265
|
-
except (ConnectionClosed, asyncio.exceptions.TimeoutError):
|
|
265
|
+
except (ConnectionClosed, asyncio.exceptions.TimeoutError, AttributeError):
|
|
266
266
|
break
|
|
267
267
|
|
|
268
268
|
async def htx_keepalive(self, interval=60):
|
|
@@ -274,7 +274,8 @@ class EventsDataStream:
|
|
|
274
274
|
else:
|
|
275
275
|
self.ping = 1
|
|
276
276
|
self.logger.warning("From HTX server PING timeout exceeded")
|
|
277
|
-
|
|
277
|
+
if self.websocket:
|
|
278
|
+
await self.websocket.close()
|
|
278
279
|
|
|
279
280
|
|
|
280
281
|
class MarketEventsDataStream(EventsDataStream):
|
|
@@ -20,10 +20,10 @@ dynamic = ["version", "description"]
|
|
|
20
20
|
requires-python = ">=3.10"
|
|
21
21
|
|
|
22
22
|
dependencies = [
|
|
23
|
-
"crypto-ws-api==2.1.
|
|
23
|
+
"crypto-ws-api==2.1.4",
|
|
24
24
|
"pyotp==2.9.0",
|
|
25
25
|
"simplejson==3.20.2",
|
|
26
|
-
"aiohttp~=3.13.
|
|
26
|
+
"aiohttp~=3.13.2",
|
|
27
27
|
"expiringdict~=1.2.2",
|
|
28
28
|
"betterproto==2.0.0b7",
|
|
29
29
|
"grpclib~=0.4.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{exchanges_wrapper-2.1.42 → exchanges_wrapper-2.1.43}/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
|
|
File without changes
|