exchanges-wrapper 2.1.2__tar.gz → 2.1.5__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.2 → exchanges_wrapper-2.1.5}/PKG-INFO +2 -2
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/__init__.py +1 -1
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/bybit_parser.py +3 -1
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/client.py +0 -1
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/exch_srv.py +19 -16
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/http_client.py +30 -7
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/pyproject.toml +1 -1
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/README.md +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/bitfinex_parser.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/events.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/huobi_parser.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/lib.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/martin/__init__.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/okx_parser.py +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/proto/martin.proto +0 -0
- {exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/exchanges_wrapper/web_sockets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: exchanges-wrapper
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.5
|
|
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
|
|
@@ -11,7 +11,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
11
11
|
Classifier: Operating System :: Unix
|
|
12
12
|
Classifier: Operating System :: Microsoft :: Windows
|
|
13
13
|
Classifier: Operating System :: MacOS
|
|
14
|
-
Requires-Dist: crypto-ws-api==2.0.
|
|
14
|
+
Requires-Dist: crypto-ws-api==2.0.7
|
|
15
15
|
Requires-Dist: grpcio==1.62.0
|
|
16
16
|
Requires-Dist: pyotp~=2.9.0
|
|
17
17
|
Requires-Dist: simplejson==3.19.2
|
|
@@ -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.5"
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
import shutil
|
|
@@ -450,7 +450,9 @@ def on_balance_update(data_in: list, ts: str, symbol: str, mode: str, uid=None)
|
|
|
450
450
|
|
|
451
451
|
elif mode == 'universal':
|
|
452
452
|
for i in data_in:
|
|
453
|
-
if i['coin'] in symbol and
|
|
453
|
+
if i['coin'] in symbol and \
|
|
454
|
+
((i['fromAccountType'] == 'UNIFIED' and i['fromMemberId'] == uid)
|
|
455
|
+
or (i['toAccountType'] == 'UNIFIED' and i['toMemberId'] == uid)):
|
|
454
456
|
data_out.append(
|
|
455
457
|
{
|
|
456
458
|
i['transferId']: {
|
|
@@ -1095,7 +1095,6 @@ class Client:
|
|
|
1095
1095
|
binance_res = bfx.order(res[4], response_type=True, cancelled=True)
|
|
1096
1096
|
break
|
|
1097
1097
|
await asyncio.sleep(0.1)
|
|
1098
|
-
logger.debug(f"cancel_order.bitfinex {order_id}: timeout: {timeout}")
|
|
1099
1098
|
else:
|
|
1100
1099
|
logger.warning(f"cancel_order.bitfinex {order_id}: res: {res}")
|
|
1101
1100
|
elif self.exchange == 'huobi':
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
+
import grpclib.exceptions
|
|
3
4
|
|
|
4
|
-
from exchanges_wrapper import __version__
|
|
5
|
+
from exchanges_wrapper import __version__ as ver_ew
|
|
6
|
+
from crypto_ws_api import __version__ as ver_cw
|
|
5
7
|
import time
|
|
6
8
|
import weakref
|
|
7
9
|
import gc
|
|
@@ -129,9 +131,9 @@ class OpenClient:
|
|
|
129
131
|
)
|
|
130
132
|
|
|
131
133
|
@classmethod
|
|
132
|
-
def get_client(cls, _id
|
|
134
|
+
def get_client(cls, _id):
|
|
133
135
|
res = next((client for client in cls.open_clients if id(client) == _id), None)
|
|
134
|
-
if res is None
|
|
136
|
+
if res is None:
|
|
135
137
|
logger.warning(f"No client exist: {_id}")
|
|
136
138
|
raise GRPCError(status=Status.UNAVAILABLE, message="No client exist")
|
|
137
139
|
return res
|
|
@@ -196,7 +198,7 @@ class Martin(mr.MartinBase):
|
|
|
196
198
|
Martin.rate_limiter = max(Martin.rate_limiter or 0, request.rate_limiter)
|
|
197
199
|
return mr.OpenClientConnectionId(
|
|
198
200
|
client_id=client_id,
|
|
199
|
-
srv_version=
|
|
201
|
+
srv_version=f"{ver_cw}:{ver_ew}",
|
|
200
202
|
exchange=open_client.client.exchange,
|
|
201
203
|
real_market=open_client.real_market
|
|
202
204
|
)
|
|
@@ -229,7 +231,7 @@ class Martin(mr.MartinBase):
|
|
|
229
231
|
await self.rate_limit_control(open_client)
|
|
230
232
|
try:
|
|
231
233
|
res = await getattr(client, client_method_name)(**kwargs)
|
|
232
|
-
except asyncio.CancelledError:
|
|
234
|
+
except (asyncio.CancelledError, asyncio.exceptions.CancelledError):
|
|
233
235
|
pass # Task cancellation should not be logged as an error
|
|
234
236
|
except (errors.RateLimitReached, errors.QueryCanceled) as ex:
|
|
235
237
|
Martin.rate_limit_reached_time = time.time()
|
|
@@ -700,7 +702,7 @@ class Martin(mr.MartinBase):
|
|
|
700
702
|
else:
|
|
701
703
|
event = vars(_event)
|
|
702
704
|
event.pop('handlers', None)
|
|
703
|
-
logger.
|
|
705
|
+
# logger.info(f"OnOrderUpdate: {open_client.name}: {event}")
|
|
704
706
|
response.success = True
|
|
705
707
|
response.result = json.dumps(event)
|
|
706
708
|
yield response
|
|
@@ -709,7 +711,7 @@ class Martin(mr.MartinBase):
|
|
|
709
711
|
async def create_limit_order(self, request: mr.CreateLimitOrderRequest) -> mr.CreateLimitOrderResponse:
|
|
710
712
|
response = mr.CreateLimitOrderResponse()
|
|
711
713
|
|
|
712
|
-
res, _,
|
|
714
|
+
res, _, _ = await self.send_request(
|
|
713
715
|
'create_order',
|
|
714
716
|
request,
|
|
715
717
|
rate_limit=True,
|
|
@@ -730,13 +732,12 @@ class Martin(mr.MartinBase):
|
|
|
730
732
|
)
|
|
731
733
|
|
|
732
734
|
response.from_pydict(res)
|
|
733
|
-
logger.debug(f"{msg_header}: order created: {res.get('orderId')}")
|
|
734
735
|
return response
|
|
735
736
|
|
|
736
737
|
async def cancel_order(self, request: mr.CancelOrderRequest) -> mr.CancelOrderResponse:
|
|
737
738
|
response = mr.CancelOrderResponse()
|
|
738
739
|
|
|
739
|
-
res, _,
|
|
740
|
+
res, _, _ = await self.send_request(
|
|
740
741
|
'cancel_order',
|
|
741
742
|
request,
|
|
742
743
|
rate_limit=True,
|
|
@@ -748,7 +749,6 @@ class Martin(mr.MartinBase):
|
|
|
748
749
|
receive_window=None
|
|
749
750
|
)
|
|
750
751
|
|
|
751
|
-
logger.debug(f"{msg_header}: order canceled: {res}")
|
|
752
752
|
response.from_pydict(res)
|
|
753
753
|
return response
|
|
754
754
|
|
|
@@ -802,11 +802,11 @@ class Martin(mr.MartinBase):
|
|
|
802
802
|
|
|
803
803
|
async def check_stream(self, request: mr.MarketRequest) -> mr.SimpleResponse:
|
|
804
804
|
response = mr.SimpleResponse()
|
|
805
|
-
|
|
805
|
+
response.success = False
|
|
806
|
+
if open_client := OpenClient.get_client(request.client_id):
|
|
806
807
|
client = open_client.client
|
|
807
808
|
response.success = bool(client.data_streams.get(request.trade_id))
|
|
808
809
|
else:
|
|
809
|
-
response.success = False
|
|
810
810
|
logger.warning(f"CheckStream request failed for {request.symbol}")
|
|
811
811
|
return response
|
|
812
812
|
|
|
@@ -843,20 +843,23 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
|
|
|
843
843
|
server = Server([Martin()])
|
|
844
844
|
with graceful_exit([server]):
|
|
845
845
|
await server.start(host, port)
|
|
846
|
-
logger.info(f"Starting server v:{
|
|
846
|
+
logger.info(f"Starting server v:{ver_cw}:{ver_ew} on {host}:{port}")
|
|
847
847
|
await server.wait_closed()
|
|
848
848
|
|
|
849
|
-
[task.cancel() for task in asyncio.all_tasks() if not task.done()]
|
|
850
|
-
|
|
851
849
|
for oc in OpenClient.open_clients:
|
|
852
850
|
await oc.client.session.close()
|
|
853
851
|
|
|
852
|
+
[task.cancel() for task in asyncio.all_tasks() if not task.done()]
|
|
853
|
+
|
|
854
854
|
|
|
855
855
|
def main():
|
|
856
856
|
try:
|
|
857
857
|
asyncio.run(amain())
|
|
858
|
-
except asyncio.CancelledError:
|
|
858
|
+
except (asyncio.exceptions.CancelledError, grpclib.exceptions.StreamTerminatedError):
|
|
859
859
|
pass # # Task cancellation should not be logged as an error
|
|
860
|
+
except Exception as ex:
|
|
861
|
+
print(f"Exception: {ex}")
|
|
862
|
+
print(traceback.format_exc())
|
|
860
863
|
|
|
861
864
|
|
|
862
865
|
if __name__ == '__main__':
|
|
@@ -18,9 +18,15 @@ from exchanges_wrapper.errors import (
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
AJ = 'application/json'
|
|
21
|
+
STATUS_BAD_REQUEST = 400
|
|
22
|
+
STATUS_UNAUTHORIZED = 401
|
|
23
|
+
STATUS_FORBIDDEN = 403
|
|
24
|
+
STATUS_I_AM_A_TEAPOT = 418 # HTTP status code for IPAddressBanned
|
|
25
|
+
ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW = "Timestamp for this request is outside of the recvWindow"
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
class HttpClient:
|
|
29
|
+
|
|
24
30
|
def __init__(self, **kwargs):
|
|
25
31
|
self.api_key = kwargs.get('api_key')
|
|
26
32
|
self.api_secret = kwargs.get('api_secret')
|
|
@@ -48,16 +54,33 @@ class HttpClient:
|
|
|
48
54
|
payload = None
|
|
49
55
|
|
|
50
56
|
if response.status >= 400:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
if response.status == STATUS_BAD_REQUEST:
|
|
58
|
+
if payload:
|
|
59
|
+
if payload.get("error", "") == "ERR_RATE_LIMIT":
|
|
60
|
+
raise RateLimitReached(RateLimitReached.message)
|
|
61
|
+
elif (
|
|
62
|
+
(self.exchange == 'binance' and payload.get('code', 0) == -1021)
|
|
63
|
+
or (self.exchange == 'bybit' and payload.get('retCode', 0) == 10002)
|
|
64
|
+
):
|
|
65
|
+
raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
|
|
66
|
+
else:
|
|
67
|
+
raise ExchangeError(f"ExchangeError: {payload}")
|
|
68
|
+
elif response.reason != "Bad Request":
|
|
69
|
+
raise ExchangeError(f"ExchangeError: {response.reason}:{response.text}")
|
|
70
|
+
|
|
71
|
+
elif (
|
|
72
|
+
response.status == STATUS_UNAUTHORIZED
|
|
73
|
+
and self.exchange == 'okx'
|
|
74
|
+
and payload
|
|
75
|
+
and payload.get('code', 0) == '50102'
|
|
76
|
+
):
|
|
77
|
+
raise ExchangeError(ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW)
|
|
78
|
+
elif response.status == STATUS_FORBIDDEN and self.exchange != 'okx':
|
|
56
79
|
raise WAFLimitViolated(WAFLimitViolated.message)
|
|
57
|
-
elif response.status ==
|
|
80
|
+
elif response.status == STATUS_I_AM_A_TEAPOT:
|
|
58
81
|
raise IPAddressBanned(IPAddressBanned.message)
|
|
59
82
|
else:
|
|
60
|
-
raise HTTPError(f"Malformed request:
|
|
83
|
+
raise HTTPError(f"Malformed request: {payload}:{response.reason}:{response.text}")
|
|
61
84
|
|
|
62
85
|
if self.exchange == 'bybit' and payload and payload.get('retCode') == 0:
|
|
63
86
|
return payload.get('result'), payload.get('time')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{exchanges_wrapper-2.1.2 → exchanges_wrapper-2.1.5}/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
|