exchanges-wrapper 2.1.0__tar.gz → 2.1.3__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.0 → exchanges_wrapper-2.1.3}/PKG-INFO +1 -1
  2. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/bybit_parser.py +18 -25
  4. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/client.py +11 -9
  5. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/exch_srv.py +35 -20
  6. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/http_client.py +6 -5
  7. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/lib.py +1 -1
  8. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/web_sockets.py +2 -2
  9. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/README.md +0 -0
  11. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/bitfinex_parser.py +0 -0
  12. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/definitions.py +0 -0
  13. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/errors.py +0 -0
  14. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/events.py +0 -0
  15. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  16. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/huobi_parser.py +0 -0
  17. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/martin/__init__.py +0 -0
  18. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/okx_parser.py +0 -0
  19. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/exchanges_wrapper/proto/martin.proto +0 -0
  20. {exchanges_wrapper-2.1.0 → exchanges_wrapper-2.1.3}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.0
3
+ Version: 2.1.3
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.0"
15
+ __version__ = "2.1.3"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -14,6 +14,7 @@ class OrderBook:
14
14
  self.asks = {i[0]: i[1] for i in snapshot["a"]}
15
15
  self.bids = {i[0]: i[1] for i in snapshot["b"]}
16
16
 
17
+ # noinspection PyTypeChecker
17
18
  def get_book(self) -> dict:
18
19
  return {
19
20
  'stream': f"{self.symbol}@depth5",
@@ -498,32 +499,24 @@ def funding_wallet(res: []) -> []:
498
499
  return balances
499
500
 
500
501
 
501
- def order_trade_list(res: [], order_id: str) -> []:
502
- trade_rows = {}
503
- order_trades = []
504
- [order_trades.append(trade) for trade in res if trade['orderId'] == order_id]
505
-
506
- for trade in order_trades:
507
- trade_id = trade['tradeId']
508
- qty = Decimal(trade['cashFlow'])
509
- fee = Decimal(trade['fee'])
510
- if (trade['side'] == 'Buy' and qty > 0) or (trade['side'] == 'Sell' and qty < 0):
511
- price = trade['tradePrice']
512
- qty = abs(qty)
513
- quote_qty = str(Decimal(price) * qty)
514
- trade_rows[trade_id] = {
502
+ def order_trade_list(res: []) -> []:
503
+ trades = []
504
+ for trade in reversed(res):
505
+ trades.append(
506
+ {
515
507
  "symbol": trade['symbol'],
516
- "id": int(trade['tradeId']),
517
- "orderId": int(order_id),
508
+ "id": int(trade['execId']),
509
+ "orderId": int(trade['orderId']),
518
510
  "orderListId": -1,
519
- "price": price,
520
- "qty": str(qty),
521
- "quoteQty": quote_qty,
522
- "commission": str(fee) if fee else '0',
523
- "commissionAsset": trade['currency'] if fee else '',
524
- "time": int(trade['transactionTime']),
511
+ "price": trade['execPrice'],
512
+ "qty": trade['execQty'],
513
+ "quoteQty": trade['execValue'],
514
+ "commission": trade['execFee'],
515
+ "commissionAsset": trade['feeCurrency'],
516
+ "time": int(trade['execTime']),
525
517
  "isBuyer": trade['side'] == 'Buy',
526
- "isMaker": True,
527
- "isBestMatch": True,
518
+ "isMaker": trade['isMaker'],
519
+ "isBestMatch": True
528
520
  }
529
- return list(trade_rows.values())
521
+ )
522
+ return trades
@@ -1029,7 +1029,6 @@ class Client:
1029
1029
  res, _ = await self.http.send_api_call("/v5/order/history", signed=True, **params)
1030
1030
  if res["list"]:
1031
1031
  b_res = bbt.order(res["list"][0], response_type=response_type)
1032
- logger.debug(f"fetch_order.b_res: {b_res}")
1033
1032
  return b_res
1034
1033
 
1035
1034
  # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#cancel-order-trade
@@ -1151,7 +1150,6 @@ class Client:
1151
1150
  except asyncio.TimeoutError:
1152
1151
  logger.warning(f"WSS CancelOrder for ByBit:{symbol}:{order_id} timeout exception")
1153
1152
 
1154
- logger.debug(f"cancel_order.binance_res: {binance_res}")
1155
1153
  return binance_res
1156
1154
 
1157
1155
  # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#cancel-all-open-orders-on-a-symbol-trade
@@ -1791,10 +1789,9 @@ class Client:
1791
1789
  params["end"] = str(end_time)
1792
1790
  res = await self.http.send_api_call("/api/v5/trade/fills-history", signed=True, **params)
1793
1791
  binance_res = okx.order_trade_list(res)
1794
- logger.debug(f"fetch_account_trade_list.binance_res: {binance_res}")
1795
1792
  return binance_res
1796
1793
 
1797
- async def fetch_order_trade_list(self, trade_id, symbol, order_id, order=None):
1794
+ async def fetch_order_trade_list(self, trade_id, symbol, order_id):
1798
1795
  if not order_id:
1799
1796
  raise ValueError("This query (fetch_order_trade_list) requires an order_id")
1800
1797
  self.assert_symbol(symbol)
@@ -1821,14 +1818,19 @@ class Client:
1821
1818
  }
1822
1819
  res = await self.http.send_api_call("/api/v5/trade/fills", signed=True, **params)
1823
1820
  b_res = okx.order_trade_list(res)
1824
- elif self.exchange == 'bybit' and order:
1821
+ elif self.exchange == 'bybit':
1822
+ res_list = []
1825
1823
  params = {
1826
- 'accountType': "UNIFIED",
1827
1824
  'category': "spot",
1828
- 'startTime': order.get('time') - 1000,
1825
+ 'execType': 'Trade',
1826
+ 'orderId': str(order_id)
1829
1827
  }
1830
- res, _ = await self.http.send_api_call("/v5/account/transaction-log", signed=True, **params)
1831
- b_res = bbt.order_trade_list(res['list'], str(order_id))
1828
+ next_page_cursor = 1
1829
+ while next_page_cursor:
1830
+ res, _ = await self.http.send_api_call("/v5/execution/list", signed=True, **params)
1831
+ next_page_cursor = params['cursor'] = res['nextPageCursor']
1832
+ res_list.extend(res['list'])
1833
+ b_res = bbt.order_trade_list(res_list)
1832
1834
  return b_res
1833
1835
 
1834
1836
  # endregion
@@ -18,6 +18,7 @@ from exchanges_wrapper import WORK_PATH, CONFIG_FILE, LOG_FILE, errors, Server,
18
18
  from exchanges_wrapper.client import Client
19
19
  from exchanges_wrapper.definitions import Side, OrderType, TimeInForce, ResponseType
20
20
  from exchanges_wrapper.lib import OrderTradesEvent, REST_RATE_LIMIT_INTERVAL, FILTER_TYPE_MAP
21
+ from exchanges_wrapper.errors import ExchangeError
21
22
  #
22
23
  HEARTBEAT = 1 # Sec
23
24
  MAX_QUEUE_SIZE = 100
@@ -217,7 +218,7 @@ class Martin(mr.MartinBase):
217
218
  async def send_request(self, client_method_name, request, rate_limit=False, **kwargs):
218
219
  open_client = OpenClient.get_client(request.client_id)
219
220
  client = open_client.client
220
- msg_header = f"Send request failed: {client_method_name}:{open_client.name}:"
221
+ msg_header = f"Send request: {client_method_name}:{open_client.name}:"
221
222
  if hasattr(request, 'symbol'):
222
223
  msg_header += f"{request.symbol}:"
223
224
  if hasattr(request, 'order_id'):
@@ -239,6 +240,10 @@ class Martin(mr.MartinBase):
239
240
  msg = f"{msg_header} HTTPError: {ex}"
240
241
  logger.error(msg)
241
242
  raise GRPCError(status=Status.FAILED_PRECONDITION, message=msg)
243
+ except ExchangeError as ex:
244
+ msg = f"{msg_header}: {ex}"
245
+ logger.warning(msg)
246
+ raise GRPCError(status=Status.OUT_OF_RANGE, message=msg)
242
247
  except Exception as ex:
243
248
  msg = f"{msg_header} exception: {ex}"
244
249
  logger.error(msg)
@@ -283,7 +288,7 @@ class Martin(mr.MartinBase):
283
288
 
284
289
  async def fetch_order(self, request: mr.FetchOrderRequest) -> mr.FetchOrderResponse:
285
290
  response = mr.FetchOrderResponse()
286
- res, client, _ = await self.send_request(
291
+ res, _, msg_header = await self.send_request(
287
292
  'fetch_order',
288
293
  request,
289
294
  rate_limit=True,
@@ -293,26 +298,27 @@ class Martin(mr.MartinBase):
293
298
  origin_client_order_id=request.client_order_id,
294
299
  receive_window=None
295
300
  )
301
+ logger.debug(f"{msg_header}: {res}")
296
302
 
297
- _queue = client.on_order_update_queues.get(request.trade_id)
298
- if _queue and request.filled_update_call and Decimal(res.get('executedQty', '0')):
299
- request.order_id = res.get('orderId', 0)
300
- await self.create_trade_stream_event(_queue, request, res)
303
+ if res and request.filled_update_call and Decimal(res['executedQty']):
304
+ request.order_id = res['orderId']
305
+ await self.create_trade_stream_event(request, res)
301
306
  response.from_pydict(res)
302
307
  return response
303
308
 
304
- async def create_trade_stream_event(self, _queue, request, order):
305
- trades, _, msg_header = await self.send_request(
309
+ async def create_trade_stream_event(self, request, order):
310
+ trades, client, msg_header = await self.send_request(
306
311
  'fetch_order_trade_list',
307
312
  request,
308
313
  trade_id=request.trade_id,
309
314
  symbol=request.symbol,
310
- order_id=request.order_id,
311
- order=order
315
+ order_id=request.order_id
312
316
  )
313
317
 
314
- logger.debug(f"{msg_header}: {trades}")
318
+ _queue = client.on_order_update_queues.get(request.trade_id)
319
+
315
320
  for trade in trades:
321
+ trade['updateTime'] = trade.pop('time')
316
322
  trade |= {
317
323
  'clientOrderId': order['clientOrderId'],
318
324
  'orderPrice': order['price'],
@@ -320,9 +326,11 @@ class Martin(mr.MartinBase):
320
326
  'executedQty': order['executedQty'],
321
327
  'cummulativeQuoteQty': order['cummulativeQuoteQty'],
322
328
  'status': order['status'],
329
+ "time": order['time']
323
330
  }
324
331
  event = OrderTradesEvent(trade)
325
332
  await _queue.put(weakref.ref(event)())
333
+ logger.debug(f"{msg_header}: {trades}")
326
334
 
327
335
  async def cancel_all_orders(self, request: mr.MarketRequest) -> mr.SimpleResponse():
328
336
  response = mr.SimpleResponse()
@@ -692,7 +700,7 @@ class Martin(mr.MartinBase):
692
700
  else:
693
701
  event = vars(_event)
694
702
  event.pop('handlers', None)
695
- logger.debug(f"OnOrderUpdate: {open_client.name}: {event}")
703
+ # logger.debug(f"OnOrderUpdate: {open_client.name}: {event}")
696
704
  response.success = True
697
705
  response.result = json.dumps(event)
698
706
  yield response
@@ -701,7 +709,7 @@ class Martin(mr.MartinBase):
701
709
  async def create_limit_order(self, request: mr.CreateLimitOrderRequest) -> mr.CreateLimitOrderResponse:
702
710
  response = mr.CreateLimitOrderResponse()
703
711
 
704
- res, _, msg_header = await self.send_request(
712
+ res, _, _ = await self.send_request(
705
713
  'create_order',
706
714
  request,
707
715
  rate_limit=True,
@@ -722,13 +730,12 @@ class Martin(mr.MartinBase):
722
730
  )
723
731
 
724
732
  response.from_pydict(res)
725
- logger.debug(f"{msg_header}: order created: {res.get('orderId')}")
726
733
  return response
727
734
 
728
735
  async def cancel_order(self, request: mr.CancelOrderRequest) -> mr.CancelOrderResponse:
729
736
  response = mr.CancelOrderResponse()
730
737
 
731
- res, client, _ = await self.send_request(
738
+ res, _, _ = await self.send_request(
732
739
  'cancel_order',
733
740
  request,
734
741
  rate_limit=True,
@@ -740,10 +747,7 @@ class Martin(mr.MartinBase):
740
747
  receive_window=None
741
748
  )
742
749
 
743
- _queue = client.on_order_update_queues.get(request.trade_id)
744
- if Decimal(res.get('executedQty', '0')):
745
- await self.create_trade_stream_event(_queue, request, res)
746
- response.from_pydict(res)
750
+ response.from_pydict(res)
747
751
  return response
748
752
 
749
753
  async def transfer_to_master(self, request: mr.MarketRequest) -> mr.SimpleResponse:
@@ -840,9 +844,20 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
840
844
  logger.info(f"Starting server v:{__version__} on {host}:{port}")
841
845
  await server.wait_closed()
842
846
 
847
+ for oc in OpenClient.open_clients:
848
+ await oc.client.session.close()
849
+
850
+ [task.cancel() for task in asyncio.all_tasks() if not task.done()]
851
+
843
852
 
844
853
  def main():
845
- asyncio.run(amain())
854
+ try:
855
+ asyncio.run(amain())
856
+ except asyncio.exceptions.CancelledError:
857
+ pass # # Task cancellation should not be logged as an error
858
+ except Exception as ex:
859
+ print(f"Exception: {ex}")
860
+ print(traceback.format_exc())
846
861
 
847
862
 
848
863
  if __name__ == '__main__':
@@ -5,7 +5,7 @@ from urllib.parse import urlencode, urlparse
5
5
  import aiohttp
6
6
  import logging
7
7
  import time
8
- from datetime import datetime
8
+ from datetime import datetime, timezone
9
9
  from crypto_ws_api.ws_session import generate_signature
10
10
  from exchanges_wrapper.errors import (
11
11
  RateLimitReached,
@@ -48,15 +48,16 @@ class HttpClient:
48
48
  payload = None
49
49
 
50
50
  if response.status >= 400:
51
- logger.debug(f"handle_errors.response: {response.text}")
52
51
  if response.status == 400 and payload and payload.get("error", str()) == "ERR_RATE_LIMIT":
53
52
  raise RateLimitReached(RateLimitReached.message)
53
+ elif response.status == 400 and response.reason != "Bad Request":
54
+ raise ExchangeError(f"ExchangeError: {payload}:{response.reason}:{response.text}")
54
55
  elif response.status == 403 and self.exchange != 'okx':
55
56
  raise WAFLimitViolated(WAFLimitViolated.message)
56
57
  elif response.status == 418:
57
58
  raise IPAddressBanned(IPAddressBanned.message)
58
59
  else:
59
- raise HTTPError(f"Malformed request: status: {response.status}, reason: {response.reason}")
60
+ raise HTTPError(f"Malformed request: {payload}:{response.reason}:{response.text}")
60
61
 
61
62
  if self.exchange == 'bybit' and payload and payload.get('retCode') == 0:
62
63
  return payload.get('result'), payload.get('time')
@@ -224,7 +225,7 @@ class ClientHBP(HttpClient):
224
225
  _params = {}
225
226
  url = f"{_endpoint}/{path}?"
226
227
  if signed:
227
- ts = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
228
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
228
229
  _params = {
229
230
  "AccessKeyId": self.api_key,
230
231
  "SignatureMethod": 'HmacSHA256',
@@ -267,7 +268,7 @@ class ClientOKX(HttpClient):
267
268
  path += f"?{urlencode(kwargs)}"
268
269
  url = f'{_endpoint}{path}'
269
270
  if signed:
270
- ts = f"{datetime.utcnow().isoformat('T', 'milliseconds')}Z"
271
+ ts = f"{datetime.now(timezone.utc).replace(tzinfo=None).isoformat('T', 'milliseconds')}Z"
271
272
  if method == 'POST' and kwargs:
272
273
  query_kwargs = json.dumps(kwargs.get('data') if 'data' in kwargs else kwargs)
273
274
  signature_payload = f"{ts}{method}{path}{query_kwargs}"
@@ -43,7 +43,7 @@ class OrderTradesEvent:
43
43
  self.last_executed_price = event_data["price"]
44
44
  self.commission_amount = event_data["commission"]
45
45
  self.commission_asset = event_data["commissionAsset"]
46
- self.transaction_time = event_data["time"]
46
+ self.transaction_time = event_data["updateTime"]
47
47
  self.trade_id = event_data["id"]
48
48
  self.ignore_a = int()
49
49
  self.in_order_book = True
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  import time
7
7
  from decimal import Decimal
8
8
  import gzip
9
- from datetime import datetime
9
+ from datetime import datetime, timezone
10
10
  from urllib.parse import urlencode, urlparse
11
11
  import websockets.client
12
12
  from websockets import ConnectionClosed
@@ -352,7 +352,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
352
352
  self.symbol = symbol
353
353
 
354
354
  async def start_wss(self):
355
- ts = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
355
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
356
356
  _params = {
357
357
  "accessKey": self.client.api_key,
358
358
  "signatureMethod": "HmacSHA256",