exchanges-wrapper 2.1.8__tar.gz → 2.1.10__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.8 → exchanges_wrapper-2.1.10}/PKG-INFO +2 -2
  2. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/client.py +46 -30
  4. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/events.py +2 -9
  5. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/exch_srv.py +6 -4
  6. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/parsers/bitfinex_parser.py +17 -4
  7. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/web_sockets.py +10 -7
  8. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/pyproject.toml +1 -1
  9. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/README.md +0 -0
  11. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/definitions.py +0 -0
  12. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/errors.py +0 -0
  13. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  14. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/http_client.py +0 -0
  15. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/lib.py +0 -0
  16. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/martin/__init__.py +0 -0
  17. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/parsers/bybit_parser.py +0 -0
  18. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/parsers/huobi_parser.py +0 -0
  19. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/parsers/okx_parser.py +0 -0
  20. {exchanges_wrapper-2.1.8 → exchanges_wrapper-2.1.10}/exchanges_wrapper/proto/martin.proto +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.8
3
+ Version: 2.1.10
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.8
14
+ Requires-Dist: crypto-ws-api==2.0.9
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.8"
15
+ __version__ = "2.1.10"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -108,6 +108,12 @@ class Client:
108
108
  self.main_account_uid = None
109
109
  self.ledgers_id = []
110
110
  self.ts_start = {}
111
+ self.tasks = set()
112
+
113
+ def tasks_manage(self, coro):
114
+ _t = asyncio.create_task(coro)
115
+ self.tasks.add(_t)
116
+ _t.add_done_callback(self.tasks.discard)
111
117
 
112
118
  async def fetch_object(self, key):
113
119
  res = None
@@ -145,7 +151,7 @@ class Client:
145
151
  # load rate limits
146
152
  self.rate_limits = infos["rateLimits"]
147
153
  self.loaded = True
148
- logger.info(f"Info for {self.exchange}:{symbol} loaded success")
154
+ logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
149
155
 
150
156
  async def close(self):
151
157
  await self.session.close()
@@ -176,7 +182,7 @@ class Client:
176
182
  user_data_stream = BBTPrivateEventsDataStream(self, self.endpoint_ws_auth, self.exchange, _trade_id)
177
183
  if user_data_stream:
178
184
  self.data_streams[_trade_id] |= {user_data_stream}
179
- asyncio.ensure_future(user_data_stream.start())
185
+ self.tasks_manage(user_data_stream.start())
180
186
  timeout = STATUS_TIMEOUT / 0.1
181
187
  while not user_data_stream.wss_started:
182
188
  timeout -= 1
@@ -190,8 +196,7 @@ class Client:
190
196
  if self.exchange == 'binance':
191
197
  market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
192
198
  self.data_streams[_trade_id] |= {market_data_stream}
193
- asyncio.ensure_future(market_data_stream.start())
194
- # start_list.append(market_data_stream.start())
199
+ self.tasks_manage(market_data_stream.start())
195
200
  else:
196
201
  for channel in _events:
197
202
  # https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url
@@ -202,7 +207,7 @@ class Client:
202
207
  #
203
208
  market_data_stream = MarketEventsDataStream(self, _endpoint, self.exchange, _trade_id, channel)
204
209
  self.data_streams[_trade_id] |= {market_data_stream}
205
- asyncio.ensure_future(market_data_stream.start())
210
+ self.tasks_manage(market_data_stream.start())
206
211
 
207
212
  async def stop_events_listener(self, _trade_id):
208
213
  logger.info(f"Stop events listener data streams for {_trade_id}")
@@ -339,7 +344,7 @@ class Client:
339
344
  "v2/tickers",
340
345
  send_api_key=False,
341
346
  endpoint=self.endpoint_api_public,
342
- symbols=bfx.get_symbols(symbols_details)
347
+ symbols=bfx.get_symbols(symbols_details, symbol)
343
348
  )
344
349
  if symbols_details and tickers:
345
350
  binance_res = bfx.exchange_info(symbols_details, tickers, symbol)
@@ -874,18 +879,22 @@ class Client:
874
879
  if new_client_order_id:
875
880
  params["cid"] = new_client_order_id
876
881
  self.active_order(new_client_order_id, quantity)
877
- res = (
878
- await self.user_wss_session.handle_request(trade_id, "on", _params=params)
879
- or await self.http.send_api_call(
880
- "v2/auth/w/order/submit",
881
- method="POST",
882
- signed=True,
883
- **params,
884
- )
885
- )
882
+
883
+ res = await self.user_wss_session.handle_request(trade_id, "on", _params=params)
884
+
885
+ if not res or (res and isinstance(res, list) and res[6] != 'SUCCESS'):
886
+ logger.debug(f"create_order.bitfinex {new_client_order_id}: {res}")
887
+ res = await self.http.send_api_call(
888
+ "v2/auth/w/order/submit",
889
+ method="POST",
890
+ signed=True,
891
+ **params
892
+ )
886
893
  if res and isinstance(res, list) and res[6] == 'SUCCESS':
887
894
  self.active_order(res[4][0][0], quantity)
888
895
  binance_res = bfx.order(res[4][0], response_type=False)
896
+ else:
897
+ logger.debug(f"create_order.bitfinex {new_client_order_id}: {res}")
889
898
  elif self.exchange == 'huobi':
890
899
  params = {
891
900
  'account-id': str(self.account_id),
@@ -961,9 +970,7 @@ class Client:
961
970
  response_type=None,
962
971
  ):
963
972
  self.assert_symbol(symbol)
964
- if self.exchange == 'bitfinex' and not order_id:
965
- raise ValueError("This query requires an order_id")
966
- elif self.exchange in ('binance', 'huobi', 'okx', 'bybit') and not order_id and not origin_client_order_id:
973
+ if not order_id and not origin_client_order_id:
967
974
  raise ValueError("This query requires an order_id or an origin_client_order_id")
968
975
 
969
976
  b_res = {}
@@ -990,20 +997,28 @@ class Client:
990
997
  )
991
998
  )
992
999
  elif self.exchange == 'bitfinex':
993
- params = {'id': [order_id]}
994
- res = await self.http.send_api_call(
1000
+ params = {}
1001
+ if order_id:
1002
+ params['id'] = [order_id]
1003
+ _res = await self.http.send_api_call(
995
1004
  f"v2/auth/r/orders/{self.symbol_to_bfx(symbol)}",
996
1005
  method="POST",
997
1006
  signed=True,
998
1007
  **params
999
- ) or await self.http.send_api_call(
1000
- f"v2/auth/r/orders/{self.symbol_to_bfx(symbol)}/hist",
1001
- method="POST",
1002
- signed=True,
1003
- **params
1004
1008
  )
1009
+ res = bfx.find_order(_res, order_id, origin_client_order_id)
1010
+ if not res:
1011
+ if not order_id:
1012
+ params['start'] = int(time.time() * 1000) - 600000 # 10 mins
1013
+ _res = await self.http.send_api_call(
1014
+ f"v2/auth/r/orders/{self.symbol_to_bfx(symbol)}/hist",
1015
+ method="POST",
1016
+ signed=True,
1017
+ **params
1018
+ )
1019
+ res = bfx.find_order(_res, order_id, origin_client_order_id)
1005
1020
  if res:
1006
- b_res = bfx.order(res[0], response_type=response_type)
1021
+ b_res = bfx.order(res, response_type=response_type)
1007
1022
  self.active_order(b_res['orderId'], b_res['origQty'], b_res['executedQty'])
1008
1023
  elif self.exchange == 'huobi':
1009
1024
  if origin_client_order_id:
@@ -1180,7 +1195,7 @@ class Client:
1180
1195
  orders_id = [order.get('orderId') for order in orders]
1181
1196
  params = {'id': orders_id}
1182
1197
  res = await self.user_wss_session.handle_request(trade_id, "oc_multi", _params=params)
1183
- if res is None or (res and isinstance(res, list) and res[6] == 'ERROR'):
1198
+ if not res or (res and isinstance(res, list) and res[6] != 'SUCCESS'):
1184
1199
  logger.debug(f"cancel_all_orders.bitfinex {orders_id}: res1: {res}")
1185
1200
  res = await self.http.send_api_call(
1186
1201
  "v2/auth/w/order/cancel/multi",
@@ -1249,12 +1264,12 @@ class Client:
1249
1264
  params = {'category': 'spot', 'symbol': symbol}
1250
1265
  res, _ = await self.http.send_api_call("/v5/order/cancel-all", method="POST", signed=True, **params)
1251
1266
 
1252
- tasks = []
1267
+ tasks = set()
1253
1268
  for order in res.get('list', []):
1254
1269
  _id = order.get('orderId')
1255
- task = asyncio.ensure_future(self.fetch_object(f"oc-{_id}"))
1270
+ task = asyncio.create_task(self.fetch_object(f"oc-{_id}"))
1256
1271
  task.set_name(f"{_id}")
1257
- tasks.append(task)
1272
+ tasks.add(task)
1258
1273
 
1259
1274
  if tasks:
1260
1275
  done, pending = await asyncio.wait(tasks, timeout=STATUS_TIMEOUT)
@@ -1267,6 +1282,7 @@ class Client:
1267
1282
  _res = await self.fetch_order(trade_id, symbol, order_id=_id, response_type=True)
1268
1283
  binance_res.append(_res)
1269
1284
  pending.clear()
1285
+ tasks.clear()
1270
1286
 
1271
1287
  return binance_res
1272
1288
 
@@ -1,5 +1,4 @@
1
- import asyncio
2
- import functools
1
+
3
2
  from collections import defaultdict
4
3
  import logging
5
4
 
@@ -11,7 +10,6 @@ logger = logging.getLogger(__name__)
11
10
  # based on: https://stackoverflow.com/a/2022629/10144963
12
11
  class Handlers(list):
13
12
  async def __call__(self, *args, **kwargs):
14
- loop = asyncio.get_running_loop()
15
13
  trade_id = kwargs.pop('trade_id', None)
16
14
  _trade_id = None
17
15
  for func in self:
@@ -20,12 +18,7 @@ class Handlers(list):
20
18
  except Exception as ex: # skipcq: PYL-W0703
21
19
  logger.warning(f"Handlers error when try get trade_id: {ex}")
22
20
  if trade_id is None or trade_id == _trade_id:
23
- if asyncio.iscoroutinefunction(func):
24
- await func(*args, **kwargs)
25
- continue
26
- if kwargs:
27
- func = functools.partial(func, **kwargs)
28
- loop.run_in_executor(None, func, *args)
21
+ await func(*args, **kwargs)
29
22
 
30
23
  def __repr__(self):
31
24
  return f"Handlers({list.__repr__(self)})"
@@ -185,14 +185,16 @@ class Martin(mr.MartinBase):
185
185
  raise GRPCError(status=Status.FAILED_PRECONDITION, message=str(ex))
186
186
 
187
187
  try:
188
- await open_client.client.load(request.symbol)
188
+ await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
189
189
  except asyncio.CancelledError:
190
190
  pass # Task cancellation should not be logged as an error
191
+ except asyncio.exceptions.TimeoutError:
192
+ await OpenClient.get_client(client_id).client.session.close()
193
+ OpenClient.remove_client(request.account_name)
194
+ raise GRPCError(status=Status.UNAVAILABLE, message=f"'{open_client.name}' timeout error")
191
195
  except Exception as ex:
192
196
  logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
193
197
  logger.debug(f"Exception traceback: {traceback.format_exc()}")
194
- await OpenClient.get_client(client_id).client.session.close()
195
- OpenClient.remove_client(request.account_name)
196
198
  raise GRPCError(status=Status.RESOURCE_EXHAUSTED, message=str(ex))
197
199
 
198
200
  # Set rate_limiter
@@ -851,7 +853,7 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
851
853
  for oc in OpenClient.open_clients:
852
854
  await oc.client.session.close()
853
855
 
854
- [task.cancel() for task in asyncio.all_tasks() if not task.done()]
856
+ [task.cancel() for task in asyncio.all_tasks() if not task.done() and task is not asyncio.current_task()]
855
857
 
856
858
 
857
859
  def main():
@@ -50,13 +50,13 @@ class OrderBook:
50
50
  return self
51
51
 
52
52
 
53
- def get_symbols(symbols_details: []) -> str:
53
+ def get_symbols(symbols_details: [], symbol) -> str:
54
54
  symbols = []
55
55
  res = ",t"
56
56
  for symbol_details in symbols_details:
57
- symbol = symbol_details['pair']
58
- if 'f0' not in symbol:
59
- symbols.append(symbol.upper())
57
+ _symbol = symbol_details['pair']
58
+ if 'f0' not in _symbol and _symbol.replace(":", "").upper() == symbol:
59
+ symbols.append(_symbol.upper())
60
60
  return f"t{res.join(symbols)}"
61
61
 
62
62
 
@@ -623,3 +623,16 @@ def funding_wallet(res: []) -> []:
623
623
  balances.append(_binance_res)
624
624
 
625
625
  return balances
626
+
627
+
628
+ def find_order(_res, order_id, origin_client_order_id):
629
+ res = []
630
+ if _res:
631
+ if order_id:
632
+ res = _res[0]
633
+ else:
634
+ for i in _res:
635
+ if i[2] == int(origin_client_order_id):
636
+ res = i
637
+ break
638
+ return res
@@ -48,7 +48,7 @@ class EventsDataStream:
48
48
  self.wss_event_buffer = {}
49
49
  self._order_book = None
50
50
  self._price = None
51
- self.tasks_list = []
51
+ self.tasks = set()
52
52
  self.wss_started = False
53
53
 
54
54
  async def start(self):
@@ -84,8 +84,13 @@ class EventsDataStream:
84
84
  await self.websocket.close(code=4000)
85
85
 
86
86
  def tasks_cancel(self):
87
- [task.cancel() for task in self.tasks_list if not task.done()]
88
- self.tasks_list.clear()
87
+ [task.cancel() for task in self.tasks if not task.done()]
88
+ self.tasks.clear()
89
+
90
+ def tasks_manage(self, coro):
91
+ _t = asyncio.create_task(coro)
92
+ self.tasks.add(_t)
93
+ _t.add_done_callback(self.tasks.discard)
89
94
 
90
95
  async def _handle_event(self, *args):
91
96
  pass # meant to be overridden in a subclass
@@ -110,8 +115,7 @@ class EventsDataStream:
110
115
  return
111
116
  elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
112
117
  and msg_data.get("success")):
113
- _task = asyncio.ensure_future(self.bybit_heartbeat(ch_type or "private"))
114
- self.tasks_list.append(_task)
118
+ self.tasks_manage(self.bybit_heartbeat(ch_type or "private"))
115
119
  if msg_data["op"] == "subscribe" and msg_data["success"] and not msg_data["ret_msg"]:
116
120
  self.wss_started = True
117
121
  elif ((msg_data.get("ret_msg") == "subscribe" or msg_data.get("op") in ("auth", "subscribe"))
@@ -602,8 +606,7 @@ class UserEventsDataStream(EventsDataStream):
602
606
 
603
607
  async def start_wss(self):
604
608
  logger.info(f"Start User WSS for {self.exchange}")
605
- _task = asyncio.ensure_future(self._heartbeat(self.listen_key))
606
- self.tasks_list.append(_task)
609
+ self.tasks_manage(self._heartbeat(self.listen_key))
607
610
  try:
608
611
  await self.ws_listener()
609
612
  finally:
@@ -17,7 +17,7 @@ dynamic = ["version", "description"]
17
17
  requires-python = ">=3.9"
18
18
 
19
19
  dependencies = [
20
- "crypto-ws-api==2.0.8",
20
+ "crypto-ws-api==2.0.9",
21
21
  "grpcio==1.62.0",
22
22
  "pyotp~=2.9.0",
23
23
  "simplejson==3.19.2",