exchanges-wrapper 2.1.22__tar.gz → 2.1.24__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.22 → exchanges_wrapper-2.1.24}/PKG-INFO +2 -2
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/README.md +1 -1
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/__init__.py +1 -1
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/client.py +17 -9
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/exch_srv.py +4 -6
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/http_client.py +24 -26
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/huobi.py +17 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/web_sockets.py +20 -2
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/LICENSE.md +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/definitions.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/errors.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/events.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/lib.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/martin/__init__.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/bitfinex.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/bybit.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/okx.py +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/proto/martin.proto +0 -0
- {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: exchanges-wrapper
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.24
|
|
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
|
|
@@ -77,7 +77,7 @@ at real bidding. First, run applications on the [Binance Spot Test Network](http
|
|
|
77
77
|
|
|
78
78
|
## Get started
|
|
79
79
|
### Prepare exchange account
|
|
80
|
-
* Create account on [Binance](https://accounts.binance.com/en/register?ref=
|
|
80
|
+
* Create account on [Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O) and get 10% discount on all trading fee
|
|
81
81
|
* Create account on [Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP) and get 6% rebate fee
|
|
82
82
|
* Create account on [HUOBI](https://www.huobi.com/en-us/topic/double-reward/?invite_code=9uaw3223) and get 10% cashback on all trading fee
|
|
83
83
|
* Create account on [OKX](https://www.okx.com/join/2607649) and get Mystery Boxes worth up to $10,000
|
|
@@ -52,7 +52,7 @@ at real bidding. First, run applications on the [Binance Spot Test Network](http
|
|
|
52
52
|
|
|
53
53
|
## Get started
|
|
54
54
|
### Prepare exchange account
|
|
55
|
-
* Create account on [Binance](https://accounts.binance.com/en/register?ref=
|
|
55
|
+
* Create account on [Binance](https://accounts.binance.com/en/register?ref=FXQ6HY5O) and get 10% discount on all trading fee
|
|
56
56
|
* Create account on [Bitfinex](https://www.bitfinex.com/sign-up?refcode=v_4az2nCP) and get 6% rebate fee
|
|
57
57
|
* Create account on [HUOBI](https://www.huobi.com/en-us/topic/double-reward/?invite_code=9uaw3223) and get 10% cashback on all trading fee
|
|
58
58
|
* Create account on [OKX](https://www.okx.com/join/2607649) and get Mystery Boxes worth up to $10,000
|
|
@@ -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.24"
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
import shutil
|
|
@@ -10,9 +10,10 @@ import pyotp
|
|
|
10
10
|
from expiringdict import ExpiringDict
|
|
11
11
|
import uuid
|
|
12
12
|
from decimal import Decimal, ROUND_HALF_DOWN
|
|
13
|
+
import inspect
|
|
13
14
|
|
|
14
15
|
from exchanges_wrapper.http_client import HttpClient
|
|
15
|
-
from exchanges_wrapper.errors import ExchangePyError
|
|
16
|
+
from exchanges_wrapper.errors import ExchangePyError, ExchangeError
|
|
16
17
|
from exchanges_wrapper.web_sockets import UserEventsDataStream, \
|
|
17
18
|
MarketEventsDataStream, \
|
|
18
19
|
BfxPrivateEventsDataStream, \
|
|
@@ -33,7 +34,7 @@ USER_DATA_STREAM = "/api/v3/userDataStream"
|
|
|
33
34
|
ORDER_ENDPOINT = "/api/v3/order"
|
|
34
35
|
|
|
35
36
|
def fallback_warning(exchange, symbol=None):
|
|
36
|
-
logger.warning(f"{exchange}:{symbol}: Fallback to HTTP API call")
|
|
37
|
+
logger.warning(f"Called by: {inspect.stack()[1][3]}: {exchange}:{symbol}: Fallback to HTTP API call")
|
|
37
38
|
|
|
38
39
|
def truncate(f, n):
|
|
39
40
|
return math.floor(f * 10 ** n) / 10 ** n
|
|
@@ -977,14 +978,21 @@ class Client:
|
|
|
977
978
|
b_res = bfx.order(res, response_type=response_type)
|
|
978
979
|
self.active_order(b_res['orderId'], b_res['origQty'], b_res['executedQty'])
|
|
979
980
|
elif self.exchange == 'huobi':
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
981
|
+
try:
|
|
982
|
+
if origin_client_order_id:
|
|
983
|
+
params = {'clientOrderId': str(origin_client_order_id)}
|
|
984
|
+
res = await self.http.send_api_call("/v1/order/orders/getClientOrder", signed=True, **params)
|
|
985
|
+
else:
|
|
986
|
+
res = await self.http.send_api_call(f"v1/order/orders/{order_id}", signed=True)
|
|
987
|
+
except ExchangeError as ex:
|
|
988
|
+
# https://huobiapi.github.io/docs/spot/v1/en/#get-the-order-detail-of-an-order-based-on-client-order-id
|
|
989
|
+
# If an order is created via API, then it's no longer queryable after being cancelled for 2 hours
|
|
990
|
+
if "base-record-invalid" in ex:
|
|
991
|
+
return hbp.order_cancelled(symbol, order_id, origin_client_order_id)
|
|
983
992
|
else:
|
|
984
|
-
res
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
self.active_order(b_res['orderId'], b_res['origQty'], b_res['executedQty'])
|
|
993
|
+
if res:
|
|
994
|
+
b_res = hbp.order(res, response_type=response_type)
|
|
995
|
+
self.active_order(b_res['orderId'], b_res['origQty'], b_res['executedQty'])
|
|
988
996
|
elif self.exchange == 'okx':
|
|
989
997
|
params = {'instId': self.symbol_to_okx(symbol),
|
|
990
998
|
'ordId': str(order_id),
|
|
@@ -179,7 +179,7 @@ class Martin(mr.MartinBase):
|
|
|
179
179
|
client.request_event.clear()
|
|
180
180
|
|
|
181
181
|
try:
|
|
182
|
-
res = await asyncio.wait_for(getattr(client, client_method_name)(**kwargs), timeout=
|
|
182
|
+
res = await asyncio.wait_for(getattr(client, client_method_name)(**kwargs), timeout=90)
|
|
183
183
|
except asyncio.exceptions.CancelledError:
|
|
184
184
|
raise GRPCError(status=Status.UNAVAILABLE, message=f"{msg_header} Server Shutdown")
|
|
185
185
|
except asyncio.exceptions.TimeoutError:
|
|
@@ -745,10 +745,6 @@ class Martin(mr.MartinBase):
|
|
|
745
745
|
return response
|
|
746
746
|
|
|
747
747
|
async def start_stream(self, request: mr.StartStreamRequest) -> mr.SimpleResponse:
|
|
748
|
-
if request.update_max_queue_size:
|
|
749
|
-
global MAX_QUEUE_SIZE
|
|
750
|
-
MAX_QUEUE_SIZE += int(MAX_QUEUE_SIZE / 10)
|
|
751
|
-
logger.info(f"MAX_QUEUE_SIZE was updated: new value is {MAX_QUEUE_SIZE}")
|
|
752
748
|
open_client = OpenClient.get_client(request.client_id)
|
|
753
749
|
client = open_client.client
|
|
754
750
|
response = mr.SimpleResponse()
|
|
@@ -808,7 +804,9 @@ async def event_handler(_queue, client, trade_id, _event_type, event):
|
|
|
808
804
|
logger.warning(f"For {_event_type} asyncio queue full and wold be closed")
|
|
809
805
|
client.stream_queue.get(trade_id, set()).discard(_queue)
|
|
810
806
|
await stop_stream(client, trade_id)
|
|
811
|
-
|
|
807
|
+
global MAX_QUEUE_SIZE
|
|
808
|
+
MAX_QUEUE_SIZE += int(MAX_QUEUE_SIZE / 10)
|
|
809
|
+
logger.info(f"MAX_QUEUE_SIZE was updated: new value is {MAX_QUEUE_SIZE}")
|
|
812
810
|
|
|
813
811
|
def is_port_in_use(port: int) -> bool:
|
|
814
812
|
import socket
|
|
@@ -23,7 +23,7 @@ STATUS_UNAUTHORIZED = 401
|
|
|
23
23
|
STATUS_FORBIDDEN = 403
|
|
24
24
|
STATUS_I_AM_A_TEAPOT = 418 # HTTP status code for IPAddressBanned
|
|
25
25
|
ERR_TIMESTAMP_OUTSIDE_RECV_WINDOW = "Timestamp for this request is outside of the recvWindow"
|
|
26
|
-
|
|
26
|
+
TIMEOUT = aiohttp.ClientTimeout(total=60)
|
|
27
27
|
|
|
28
28
|
class HttpClient:
|
|
29
29
|
def __init__(self, params: dict):
|
|
@@ -34,8 +34,8 @@ class HttpClient:
|
|
|
34
34
|
self.exchange = params['exchange']
|
|
35
35
|
self.test_net = params['test_net']
|
|
36
36
|
self.rate_limit_reached = False
|
|
37
|
-
self.
|
|
38
|
-
self.session =
|
|
37
|
+
self.rest_cycle_lock = asyncio.Lock()
|
|
38
|
+
self.session = None
|
|
39
39
|
self._session_mutex = asyncio.Lock()
|
|
40
40
|
self.ex_imps = {} # exchanges implementation
|
|
41
41
|
self.declare_exchanges_implementation()
|
|
@@ -51,9 +51,9 @@ class HttpClient:
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async def _create_session_if_required(self):
|
|
54
|
-
if self.session.closed:
|
|
54
|
+
if self.session is None or self.session.closed:
|
|
55
55
|
async with self._session_mutex:
|
|
56
|
-
self.session = aiohttp.ClientSession()
|
|
56
|
+
self.session = aiohttp.ClientSession(timeout=TIMEOUT)
|
|
57
57
|
|
|
58
58
|
async def handle_errors(self, response):
|
|
59
59
|
if response.status >= 500:
|
|
@@ -128,11 +128,10 @@ class HttpClient:
|
|
|
128
128
|
await self._create_session_if_required()
|
|
129
129
|
try:
|
|
130
130
|
async with self.session.request(method, url, timeout=timeout, **query_kwargs) as response:
|
|
131
|
-
self.rest_cycle_busy = False
|
|
132
131
|
return await self.handle_errors(response)
|
|
133
|
-
except (aiohttp.
|
|
132
|
+
except (aiohttp.ClientConnectionError, asyncio.exceptions.TimeoutError):
|
|
134
133
|
await self.session.close()
|
|
135
|
-
raise ExchangeError("HTTP
|
|
134
|
+
raise ExchangeError("HTTP ClientConnectionError, the connection will be restored")
|
|
136
135
|
|
|
137
136
|
async def _binance_request(self, path, method, signed, send_api_key, endpoint, timeout, **kwargs):
|
|
138
137
|
_endpoint = endpoint or self.endpoint
|
|
@@ -162,25 +161,24 @@ class HttpClient:
|
|
|
162
161
|
if bfx_post and "params" in kwargs:
|
|
163
162
|
query_kwargs['data'] = _params
|
|
164
163
|
|
|
165
|
-
# To avoid breaking the correct sequence Nonce value
|
|
166
|
-
while self.rest_cycle_busy:
|
|
167
|
-
await asyncio.sleep(0.1)
|
|
168
|
-
self.rest_cycle_busy = True
|
|
169
|
-
|
|
170
164
|
if signed:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
165
|
+
async with self.rest_cycle_lock:
|
|
166
|
+
ts = int(time.time() * 1000000)
|
|
167
|
+
query_kwargs["headers"]["Content-Type"] = AJ
|
|
168
|
+
if bfx_post:
|
|
169
|
+
query_kwargs['data'] = _params
|
|
170
|
+
if send_api_key:
|
|
171
|
+
query_kwargs["headers"]["bfx-apikey"] = self.api_key
|
|
172
|
+
signature_payload = f'/api/{path}{ts}'
|
|
173
|
+
if _params:
|
|
174
|
+
signature_payload += f"{_params}"
|
|
175
|
+
query_kwargs["headers"]["bfx-signature"] = generate_signature(self.exchange,
|
|
176
|
+
self.api_secret,
|
|
177
|
+
signature_payload)
|
|
178
|
+
query_kwargs["headers"]["bfx-nonce"] = str(ts)
|
|
179
|
+
|
|
180
|
+
return await self.send_request(method, url, timeout, query_kwargs)
|
|
181
|
+
|
|
184
182
|
return await self.send_request(method, url, timeout, query_kwargs)
|
|
185
183
|
|
|
186
184
|
async def _bybit_request(self, path, method, signed, _send_api_key, endpoint, timeout, **kwargs):
|
|
@@ -170,6 +170,23 @@ def order(res: {}, response_type=None) -> {}:
|
|
|
170
170
|
"side": side,
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
def order_cancelled(symbol, order_id=None, origin_client_order_id=None,) -> {}:
|
|
174
|
+
return {
|
|
175
|
+
"symbol": symbol,
|
|
176
|
+
"origClientOrderId": origin_client_order_id,
|
|
177
|
+
"orderId": order_id,
|
|
178
|
+
"orderListId": -1,
|
|
179
|
+
"clientOrderId": origin_client_order_id,
|
|
180
|
+
"transactTime": int(time.time() * 1000),
|
|
181
|
+
"price": "0",
|
|
182
|
+
"origQty": "0",
|
|
183
|
+
"executedQty": "0",
|
|
184
|
+
"cummulativeQuoteQty": "0",
|
|
185
|
+
"status": 'CANCELED',
|
|
186
|
+
"timeInForce": "GTC",
|
|
187
|
+
"type": "LIMIT",
|
|
188
|
+
"side": '',
|
|
189
|
+
}
|
|
173
190
|
|
|
174
191
|
def account_information(res: {}) -> {}:
|
|
175
192
|
balances = []
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import asyncio
|
|
3
3
|
import ujson as json
|
|
4
|
-
import logging
|
|
4
|
+
import logging.handlers
|
|
5
|
+
from pathlib import Path
|
|
5
6
|
import time
|
|
6
7
|
from decimal import Decimal
|
|
7
8
|
import gzip
|
|
@@ -15,9 +16,23 @@ import exchanges_wrapper.parsers.huobi as hbp
|
|
|
15
16
|
import exchanges_wrapper.parsers.okx as okx
|
|
16
17
|
import exchanges_wrapper.parsers.bybit as bbt
|
|
17
18
|
from crypto_ws_api.ws_session import generate_signature, compose_htx_ws_auth
|
|
19
|
+
from exchanges_wrapper import LOG_PATH
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
22
|
+
formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
|
|
23
|
+
#
|
|
24
|
+
fh = logging.handlers.RotatingFileHandler(Path(LOG_PATH, 'websockets.log'), maxBytes=1000000, backupCount=10)
|
|
25
|
+
fh.setFormatter(formatter)
|
|
26
|
+
fh.setLevel(logging.INFO)
|
|
27
|
+
#
|
|
28
|
+
sh = logging.StreamHandler()
|
|
29
|
+
sh.setFormatter(formatter)
|
|
30
|
+
sh.setLevel(logging.INFO)
|
|
31
|
+
|
|
32
|
+
logger.addHandler(fh)
|
|
33
|
+
logger.addHandler(sh)
|
|
20
34
|
logger.propagate = False
|
|
35
|
+
|
|
21
36
|
sys.tracebacklimit = 0
|
|
22
37
|
|
|
23
38
|
|
|
@@ -218,7 +233,10 @@ class EventsDataStream:
|
|
|
218
233
|
async def bybit_heartbeat(self, req_id, interval=20):
|
|
219
234
|
while True:
|
|
220
235
|
await asyncio.sleep(interval)
|
|
221
|
-
|
|
236
|
+
try:
|
|
237
|
+
await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
|
|
238
|
+
except (ConnectionClosed, asyncio.exceptions.TimeoutError):
|
|
239
|
+
pass # handled elsewhere
|
|
222
240
|
|
|
223
241
|
async def htx_keepalive(self, interval=60):
|
|
224
242
|
await asyncio.sleep(interval * 10)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/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
|