exchanges-wrapper 2.1.7__tar.gz → 2.1.9__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.7 → exchanges_wrapper-2.1.9}/PKG-INFO +2 -2
  2. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/client.py +29 -20
  4. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/events.py +2 -9
  5. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/exch_srv.py +8 -4
  6. {exchanges_wrapper-2.1.7/exchanges_wrapper → exchanges_wrapper-2.1.9/exchanges_wrapper/parsers}/bybit_parser.py +6 -1
  7. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/web_sockets.py +14 -11
  8. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/pyproject.toml +1 -1
  9. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/LICENSE.md +0 -0
  10. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/README.md +0 -0
  11. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/definitions.py +0 -0
  12. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/errors.py +0 -0
  13. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  14. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/http_client.py +0 -0
  15. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/lib.py +0 -0
  16. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/exchanges_wrapper/martin/__init__.py +0 -0
  17. {exchanges_wrapper-2.1.7/exchanges_wrapper → exchanges_wrapper-2.1.9/exchanges_wrapper/parsers}/bitfinex_parser.py +0 -0
  18. {exchanges_wrapper-2.1.7/exchanges_wrapper → exchanges_wrapper-2.1.9/exchanges_wrapper/parsers}/huobi_parser.py +0 -0
  19. {exchanges_wrapper-2.1.7/exchanges_wrapper → exchanges_wrapper-2.1.9/exchanges_wrapper/parsers}/okx_parser.py +0 -0
  20. {exchanges_wrapper-2.1.7 → exchanges_wrapper-2.1.9}/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.7
3
+ Version: 2.1.9
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.7"
15
+ __version__ = "2.1.9"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -22,10 +22,10 @@ from exchanges_wrapper.web_sockets import UserEventsDataStream, \
22
22
  BBTPrivateEventsDataStream
23
23
  from exchanges_wrapper.definitions import OrderType
24
24
  from exchanges_wrapper.events import Events
25
- import exchanges_wrapper.bitfinex_parser as bfx
26
- import exchanges_wrapper.huobi_parser as hbp
27
- import exchanges_wrapper.okx_parser as okx
28
- import exchanges_wrapper.bybit_parser as bbt
25
+ import exchanges_wrapper.parsers.bitfinex_parser as bfx
26
+ import exchanges_wrapper.parsers.huobi_parser as hbp
27
+ import exchanges_wrapper.parsers.okx_parser as okx
28
+ import exchanges_wrapper.parsers.bybit_parser as bbt
29
29
  from crypto_ws_api.ws_session import UserWSSession
30
30
 
31
31
  logger = logging.getLogger(__name__)
@@ -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
@@ -117,6 +123,7 @@ class Client:
117
123
  return res
118
124
 
119
125
  async def load(self, symbol):
126
+ logger.info(f"Try load {self.exchange}:{symbol} info...")
120
127
  infos = await self.fetch_exchange_info(symbol)
121
128
  if not infos.get('serverTime'):
122
129
  raise UserWarning("Can't get exchange info, check availability and operational status of the exchange")
@@ -144,6 +151,7 @@ class Client:
144
151
  # load rate limits
145
152
  self.rate_limits = infos["rateLimits"]
146
153
  self.loaded = True
154
+ logger.info(f"Info for {self.exchange}:{symbol} loaded successfully")
147
155
 
148
156
  async def close(self):
149
157
  await self.session.close()
@@ -174,7 +182,7 @@ class Client:
174
182
  user_data_stream = BBTPrivateEventsDataStream(self, self.endpoint_ws_auth, self.exchange, _trade_id)
175
183
  if user_data_stream:
176
184
  self.data_streams[_trade_id] |= {user_data_stream}
177
- asyncio.ensure_future(user_data_stream.start())
185
+ self.tasks_manage(user_data_stream.start())
178
186
  timeout = STATUS_TIMEOUT / 0.1
179
187
  while not user_data_stream.wss_started:
180
188
  timeout -= 1
@@ -188,8 +196,7 @@ class Client:
188
196
  if self.exchange == 'binance':
189
197
  market_data_stream = MarketEventsDataStream(self, self.endpoint_ws_public, self.exchange, _trade_id)
190
198
  self.data_streams[_trade_id] |= {market_data_stream}
191
- asyncio.ensure_future(market_data_stream.start())
192
- # start_list.append(market_data_stream.start())
199
+ self.tasks_manage(market_data_stream.start())
193
200
  else:
194
201
  for channel in _events:
195
202
  # https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url
@@ -200,7 +207,7 @@ class Client:
200
207
  #
201
208
  market_data_stream = MarketEventsDataStream(self, _endpoint, self.exchange, _trade_id, channel)
202
209
  self.data_streams[_trade_id] |= {market_data_stream}
203
- asyncio.ensure_future(market_data_stream.start())
210
+ self.tasks_manage(market_data_stream.start())
204
211
 
205
212
  async def stop_events_listener(self, _trade_id):
206
213
  logger.info(f"Stop events listener data streams for {_trade_id}")
@@ -1177,18 +1184,19 @@ class Client:
1177
1184
  orders = await self.fetch_open_orders(trade_id, symbol, receive_window=receive_window, response_type=True)
1178
1185
  orders_id = [order.get('orderId') for order in orders]
1179
1186
  params = {'id': orders_id}
1180
- res = (
1181
- await self.user_wss_session.handle_request(trade_id, "oc_multi", _params=params)
1182
- or await self.http.send_api_call(
1187
+ res = await self.user_wss_session.handle_request(trade_id, "oc_multi", _params=params)
1188
+ if res is None or (res and isinstance(res, list) and res[6] == 'ERROR'):
1189
+ logger.debug(f"cancel_all_orders.bitfinex {orders_id}: res1: {res}")
1190
+ res = await self.http.send_api_call(
1183
1191
  "v2/auth/w/order/cancel/multi",
1184
1192
  method="POST",
1185
1193
  signed=True,
1186
- **params,
1187
- )
1188
- )
1189
- if res and res[6] == 'SUCCESS':
1190
- return bfx.orders(res[4], response_type=True, cancelled=True)
1191
- logger.debug(f"bitfinex: cancel_all_orders.res: {res}")
1194
+ **params
1195
+ )
1196
+ if res and isinstance(res, list) and res[6] == 'SUCCESS':
1197
+ binance_res = bfx.orders(res[4], response_type=True, cancelled=True)
1198
+ else:
1199
+ logger.debug(f"bitfinex: cancel_all_orders.res: {res}")
1192
1200
 
1193
1201
  elif self.exchange == 'huobi':
1194
1202
  orders = await self.fetch_open_orders(trade_id, symbol, receive_window=receive_window, response_type=True)
@@ -1246,12 +1254,12 @@ class Client:
1246
1254
  params = {'category': 'spot', 'symbol': symbol}
1247
1255
  res, _ = await self.http.send_api_call("/v5/order/cancel-all", method="POST", signed=True, **params)
1248
1256
 
1249
- tasks = []
1257
+ tasks = set()
1250
1258
  for order in res.get('list', []):
1251
1259
  _id = order.get('orderId')
1252
- task = asyncio.ensure_future(self.fetch_object(f"oc-{_id}"))
1260
+ task = asyncio.create_task(self.fetch_object(f"oc-{_id}"))
1253
1261
  task.set_name(f"{_id}")
1254
- tasks.append(task)
1262
+ tasks.add(task)
1255
1263
 
1256
1264
  if tasks:
1257
1265
  done, pending = await asyncio.wait(tasks, timeout=STATUS_TIMEOUT)
@@ -1264,6 +1272,7 @@ class Client:
1264
1272
  _res = await self.fetch_order(trade_id, symbol, order_id=_id, response_type=True)
1265
1273
  binance_res.append(_res)
1266
1274
  pending.clear()
1275
+ tasks.clear()
1267
1276
 
1268
1277
  return binance_res
1269
1278
 
@@ -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)})"
@@ -160,9 +160,11 @@ class Martin(mr.MartinBase):
160
160
  logger.info(f"OpenClientConnection start trade: {request.account_name}:{request.trade_id}")
161
161
  client_id = OpenClient.get_id(request.account_name)
162
162
  if client_id:
163
+ logger.debug(f"OpenClientConnection: {request.account_name}:{request.trade_id}:{client_id}")
163
164
  open_client = OpenClient.get_client(client_id)
164
165
  open_client.client.http.rate_limit_reached = False
165
166
  else:
167
+ logger.debug(f"OpenClientConnection: {request.account_name}:{request.trade_id}: set new client_id")
166
168
  try:
167
169
  open_client = OpenClient(request.account_name)
168
170
  client_id = id(open_client)
@@ -183,14 +185,16 @@ class Martin(mr.MartinBase):
183
185
  raise GRPCError(status=Status.FAILED_PRECONDITION, message=str(ex))
184
186
 
185
187
  try:
186
- await open_client.client.load(request.symbol)
188
+ await asyncio.wait_for(open_client.client.load(request.symbol), timeout=HEARTBEAT * 60)
187
189
  except asyncio.CancelledError:
188
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")
189
195
  except Exception as ex:
190
196
  logger.warning(f"OpenClientConnection for '{open_client.name}' exception: {ex}")
191
197
  logger.debug(f"Exception traceback: {traceback.format_exc()}")
192
- await OpenClient.get_client(client_id).client.session.close()
193
- OpenClient.remove_client(request.account_name)
194
198
  raise GRPCError(status=Status.RESOURCE_EXHAUSTED, message=str(ex))
195
199
 
196
200
  # Set rate_limiter
@@ -849,7 +853,7 @@ async def amain(host: str = '127.0.0.1', port: int = 50051):
849
853
  for oc in OpenClient.open_clients:
850
854
  await oc.client.session.close()
851
855
 
852
- [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()]
853
857
 
854
858
 
855
859
  def main():
@@ -374,7 +374,12 @@ def on_trade_update(res: dict) -> dict:
374
374
  last_executed_price = res.get("execPrice", res.get("avgPrice"))
375
375
  last_quote_asset_transacted = res.get("execValue", cumulative_quote_asset)
376
376
 
377
- status = 'NEW' if leaves_qty == order_quantity else 'PARTIALLY_FILLED' if leaves_qty > 0 else 'FILLED'
377
+ if leaves_qty == order_quantity:
378
+ status = 'NEW'
379
+ elif leaves_qty > 0:
380
+ status = 'PARTIALLY_FILLED'
381
+ else:
382
+ status = 'FILLED'
378
383
 
379
384
  return {
380
385
  "e": "executionReport",
@@ -10,10 +10,10 @@ from datetime import datetime, timezone
10
10
  from urllib.parse import urlencode, urlparse
11
11
  import websockets
12
12
 
13
- import exchanges_wrapper.bitfinex_parser as bfx
14
- import exchanges_wrapper.huobi_parser as hbp
15
- import exchanges_wrapper.okx_parser as okx
16
- import exchanges_wrapper.bybit_parser as bbt
13
+ import exchanges_wrapper.parsers.bitfinex_parser as bfx
14
+ import exchanges_wrapper.parsers.huobi_parser as hbp
15
+ import exchanges_wrapper.parsers.okx_parser as okx
16
+ import exchanges_wrapper.parsers.bybit_parser as bbt
17
17
  from crypto_ws_api.ws_session import generate_signature
18
18
  from exchanges_wrapper import LOG_PATH
19
19
 
@@ -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",