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.
Files changed (20) hide show
  1. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/PKG-INFO +2 -2
  2. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/README.md +1 -1
  3. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/__init__.py +1 -1
  4. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/client.py +17 -9
  5. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/exch_srv.py +4 -6
  6. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/http_client.py +24 -26
  7. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/huobi.py +17 -0
  8. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/web_sockets.py +20 -2
  9. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/definitions.py +0 -0
  11. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/errors.py +0 -0
  12. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/events.py +0 -0
  13. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  14. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/lib.py +0 -0
  15. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/martin/__init__.py +0 -0
  16. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/bitfinex.py +0 -0
  17. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/bybit.py +0 -0
  18. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/parsers/okx.py +0 -0
  19. {exchanges_wrapper-2.1.22 → exchanges_wrapper-2.1.24}/exchanges_wrapper/proto/martin.proto +0 -0
  20. {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.22
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=QCS4OGWR) and get 10% discount on all trading fee
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=QCS4OGWR) and get 10% discount on all trading fee
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.22"
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 = []
@@ -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
- 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
222
240
 
223
241
  async def htx_keepalive(self, interval=60):
224
242
  await asyncio.sleep(interval * 10)