exchanges-wrapper 2.1.23__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.
Files changed (20) hide show
  1. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/PKG-INFO +1 -1
  2. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/client.py +17 -9
  4. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/exch_srv.py +4 -6
  5. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/http_client.py +24 -26
  6. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/huobi.py +17 -0
  7. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/web_sockets.py +4 -1
  8. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/LICENSE.md +0 -0
  9. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/README.md +0 -0
  10. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/definitions.py +0 -0
  11. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/errors.py +0 -0
  12. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/events.py +0 -0
  13. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  14. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/lib.py +0 -0
  15. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/martin/__init__.py +0 -0
  16. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  17. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/bybit.py +0 -0
  18. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/okx.py +0 -0
  19. {exchanges_wrapper-2.1.23 → exchanges_wrapper-2.1.24}/exchanges_wrapper/proto/martin.proto +0 -0
  20. {exchanges_wrapper-2.1.23 → 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.23
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
@@ -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.23"
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
- if origin_client_order_id:
981
- params = {'clientOrderId': str(origin_client_order_id)}
982
- res = await self.http.send_api_call("/v1/order/orders/getClientOrder", signed=True, **params)
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 = await self.http.send_api_call(f"v1/order/orders/{order_id}", signed=True)
985
- if res:
986
- b_res = hbp.order(res, response_type=response_type)
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=HEARTBEAT * 60)
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.rest_cycle_busy = None
38
- self.session = aiohttp.ClientSession()
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.ClientOSError, aiohttp.ServerDisconnectedError):
132
+ except (aiohttp.ClientConnectionError, asyncio.exceptions.TimeoutError):
134
133
  await self.session.close()
135
- raise ExchangeError("HTTP ClientOSError or ServerDisconnectedError, the connection will be restored")
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
- ts = int(time.time() * 1000)
172
- query_kwargs["headers"]["Content-Type"] = AJ
173
- if bfx_post:
174
- query_kwargs['data'] = _params
175
- if send_api_key:
176
- query_kwargs["headers"]["bfx-apikey"] = self.api_key
177
- signature_payload = f'/api/{path}{ts}'
178
- if _params:
179
- signature_payload += f"{_params}"
180
- query_kwargs["headers"]["bfx-signature"] = generate_signature(self.exchange,
181
- self.api_secret,
182
- signature_payload)
183
- query_kwargs["headers"]["bfx-nonce"] = str(ts)
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 = []
@@ -233,7 +233,10 @@ class EventsDataStream:
233
233
  async def bybit_heartbeat(self, req_id, interval=20):
234
234
  while True:
235
235
  await asyncio.sleep(interval)
236
- await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
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
237
240
 
238
241
  async def htx_keepalive(self, interval=60):
239
242
  await asyncio.sleep(interval * 10)