exchanges-wrapper 2.1.13__tar.gz → 2.1.16__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.13 → exchanges_wrapper-2.1.16}/PKG-INFO +1 -2
  2. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/__init__.py +1 -1
  3. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/client.py +55 -42
  4. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/exch_srv.py +19 -1
  5. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/martin/__init__.py +36 -0
  6. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/parsers/bybit_parser.py +0 -3
  7. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/parsers/okx_parser.py +1 -1
  8. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/proto/martin.proto +2 -0
  9. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/web_sockets.py +23 -8
  10. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/pyproject.toml +0 -1
  11. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/LICENSE.md +0 -0
  12. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/README.md +0 -0
  13. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/definitions.py +0 -0
  14. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/errors.py +0 -0
  15. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/events.py +0 -0
  16. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/exch_srv_cfg.toml.template +0 -0
  17. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/http_client.py +0 -0
  18. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/lib.py +0 -0
  19. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/parsers/bitfinex_parser.py +0 -0
  20. {exchanges_wrapper-2.1.13 → exchanges_wrapper-2.1.16}/exchanges_wrapper/parsers/huobi_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: exchanges-wrapper
3
- Version: 2.1.13
3
+ Version: 2.1.16
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
@@ -16,7 +16,6 @@ Requires-Dist: grpcio==1.62.0
16
16
  Requires-Dist: pyotp~=2.9.0
17
17
  Requires-Dist: simplejson==3.19.2
18
18
  Requires-Dist: aiohttp==3.9.5
19
- Requires-Dist: Pympler~=1.0.1
20
19
  Requires-Dist: websockets~=12.0
21
20
  Requires-Dist: expiringdict~=1.2.2
22
21
  Requires-Dist: ujson~=5.10.0
@@ -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.13"
15
+ __version__ = "2.1.16"
16
16
 
17
17
  from pathlib import Path
18
18
  import shutil
@@ -354,13 +354,7 @@ class Client:
354
354
  server_time = await self.fetch_server_time()
355
355
  params = {'symbols': symbol.lower()}
356
356
  trading_symbol = await self.http.send_api_call("v1/settings/common/market-symbols", **params)
357
- if self.account_id is None:
358
- accounts = await self.http.send_api_call("v1/account/accounts", signed=True)
359
- for account in accounts:
360
- if account.get('type') == 'spot':
361
- self.account_id = account.get('id')
362
- break
363
- self.account_uid = await self.http.send_api_call("v2/user/uid", signed=True)
357
+ await self.set_htx_ids()
364
358
  binance_res = hbp.exchange_info(server_time.get('serverTime'), trading_symbol[0])
365
359
  elif self.exchange == 'okx':
366
360
  params = {'instType': 'SPOT'}
@@ -376,6 +370,15 @@ class Client:
376
370
  # logger.info(f"fetch_exchange_info: binance_res: {binance_res}")
377
371
  return binance_res
378
372
 
373
+ async def set_htx_ids(self):
374
+ if self.account_id is None:
375
+ accounts = await self.http.send_api_call("v1/account/accounts", signed=True)
376
+ for account in accounts:
377
+ if account.get('type') == 'spot':
378
+ self.account_id = account.get('id')
379
+ break
380
+ self.account_uid = await self.http.send_api_call("v2/user/uid", signed=True)
381
+
379
382
  # MARKET DATA ENDPOINTS
380
383
 
381
384
  # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#order-book
@@ -493,24 +496,24 @@ class Client:
493
496
  uid=self.account_uid
494
497
  )
495
498
 
496
- params.pop('status')
497
- params['accountType'] = 'UNIFIED'
498
- params['category'] = 'spot'
499
- params['type'] = 'TRANSFER_IN'
499
+ if not _res:
500
+ # Get Transaction Log
501
+ params.pop('status')
502
+ params['accountType'] = 'UNIFIED'
503
+ params['category'] = 'spot'
504
+ params['type'] = 'TRANSFER_IN'
500
505
 
501
- # Get Transaction Log
502
- res, ts = await self.http.send_api_call(
503
- "/v5/account/transaction-log",
504
- signed=True,
505
- **params
506
- )
507
-
508
- _res += bbt.on_balance_update(
509
- res['list'],
510
- ts,
511
- symbol,
512
- 'log'
513
- )
506
+ res, ts = await self.http.send_api_call(
507
+ "/v5/account/transaction-log",
508
+ signed=True,
509
+ **params
510
+ )
511
+ _res += bbt.on_balance_update(
512
+ res['list'],
513
+ ts,
514
+ symbol,
515
+ 'log'
516
+ )
514
517
 
515
518
  for i in _res:
516
519
  _id = next(iter(i))
@@ -703,7 +706,8 @@ class Client:
703
706
  # https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#symbol-price-ticker
704
707
  async def fetch_symbol_price_ticker(self, symbol=None):
705
708
  if symbol:
706
- self.assert_symbol_exists(symbol)
709
+ if not (self.exchange == 'binance' and symbol == 'BNBUSDT'):
710
+ self.assert_symbol_exists(symbol)
707
711
  binance_res = {}
708
712
  elif self.exchange in ('bitfinex', 'huobi'):
709
713
  raise ValueError('For fetch_symbol_price_ticker() symbol parameter required')
@@ -1603,25 +1607,34 @@ class Client:
1603
1607
  binance_res = bbt.funding_wallet(res["balance"])
1604
1608
  return binance_res
1605
1609
 
1610
+ # https://developers.binance.com/docs/sub_account/asset-management/Transfer-to-Sub-account-of-Same-Master
1611
+ async def transfer_to_sub(self, email, symbol, quantity, receive_window=None):
1612
+ if self.exchange == 'binance':
1613
+ quantity = any2str(Decimal(quantity).quantize(Decimal('0.01234567'), rounding=ROUND_HALF_DOWN))
1614
+ params = {"toEmail": email, "asset": symbol, "amount": quantity}
1615
+ if receive_window:
1616
+ params["recvWindow"] = receive_window
1617
+ return await self.http.send_api_call(
1618
+ "/sapi/v1/sub-account/transfer/subToSub",
1619
+ "POST",
1620
+ signed=True,
1621
+ params=params
1622
+ )
1623
+ else:
1624
+ raise ValueError(f"Can't implemented for {self.exchange}")
1625
+
1606
1626
  # https://binance-docs.github.io/apidocs/spot/en/#transfer-to-master-for-sub-account
1607
1627
  async def transfer_to_master(self, symbol, quantity, receive_window=None):
1608
- quantity = any2str(Decimal(quantity).quantize(Decimal('0.01234567'), rounding=ROUND_HALF_DOWN))
1609
-
1628
+ _quantity = any2str(Decimal(quantity).quantize(Decimal('0.01234567'), rounding=ROUND_HALF_DOWN))
1610
1629
  binance_res = {}
1611
1630
  if self.exchange == 'binance':
1612
- params = {"asset": symbol, "amount": quantity}
1613
- if receive_window:
1614
- params["recvWindow"] = receive_window
1615
1631
  if self.master_email:
1616
- logger.info(f"Collect {quantity}{symbol} to {self.master_email} sub-account")
1617
- params["toEmail"] = self.master_email
1618
- binance_res = await self.http.send_api_call(
1619
- "/sapi/v1/sub-account/transfer/subToSub",
1620
- "POST",
1621
- signed=True,
1622
- params=params
1623
- )
1632
+ logger.info(f"Collect {_quantity}{symbol} to {self.master_email} sub-account")
1633
+ binance_res = await self.transfer_to_sub(self.master_email, symbol, quantity, receive_window)
1624
1634
  else:
1635
+ params = {"asset": symbol, "amount": _quantity}
1636
+ if receive_window:
1637
+ params["recvWindow"] = receive_window
1625
1638
  binance_res = await self.http.send_api_call(
1626
1639
  "/sapi/v1/sub-account/transfer/subToMaster",
1627
1640
  "POST",
@@ -1636,7 +1649,7 @@ class Client:
1636
1649
  "from": "exchange",
1637
1650
  "to": "exchange",
1638
1651
  "currency": symbol,
1639
- "amount": quantity,
1652
+ "amount": _quantity,
1640
1653
  "email_dst": self.master_email,
1641
1654
  "tfaToken": {"method": "otp", "token": totp.now()}
1642
1655
  }
@@ -1658,7 +1671,7 @@ class Client:
1658
1671
  'to-account-type': "spot",
1659
1672
  'to-account': self.main_account_id,
1660
1673
  'currency': symbol.lower(),
1661
- 'amount': quantity
1674
+ 'amount': _quantity
1662
1675
  }
1663
1676
  res = await self.http.send_api_call(
1664
1677
  "v1/account/transfer",
@@ -1670,7 +1683,7 @@ class Client:
1670
1683
  elif self.exchange == 'okx':
1671
1684
  params = {
1672
1685
  "ccy": symbol,
1673
- "amt": quantity,
1686
+ "amt": _quantity,
1674
1687
  "from": '18',
1675
1688
  "to": '18',
1676
1689
  "type": '3'
@@ -1692,7 +1705,7 @@ class Client:
1692
1705
  params = {
1693
1706
  'transferId': str(uuid.uuid4()),
1694
1707
  'coin': symbol,
1695
- 'amount': str(math.floor(float(quantity) * 10 ** n) / 10 ** n),
1708
+ 'amount': str(math.floor(float(_quantity) * 10 ** n) / 10 ** n),
1696
1709
  'fromMemberId': self.account_uid,
1697
1710
  'toMemberId': self.main_account_uid,
1698
1711
  'fromAccountType': 'UNIFIED',
@@ -173,7 +173,7 @@ class Martin(mr.MartinBase):
173
173
  # For HuobiPro get master account uid and account_id
174
174
  main_account = get_account(open_client.client.master_name)
175
175
  main_client = Client(*main_account)
176
- await main_client.fetch_exchange_info(request.symbol)
176
+ await asyncio.wait_for(main_client.set_htx_ids(), timeout=HEARTBEAT * 60)
177
177
  if main_client.account_uid and main_client.account_id:
178
178
  open_client.client.main_account_uid = main_client.account_uid
179
179
  open_client.client.main_account_id = main_client.account_id
@@ -767,6 +767,24 @@ class Martin(mr.MartinBase):
767
767
  response.from_pydict(res)
768
768
  return response
769
769
 
770
+ async def transfer_to_sub(self, request: mr.MarketRequest) -> mr.SimpleResponse:
771
+ response = mr.SimpleResponse()
772
+ response.success = False
773
+
774
+ res, _, _ = await self.send_request(
775
+ 'transfer_to_sub',
776
+ request,
777
+ rate_limit=True,
778
+ email=request.data,
779
+ symbol=request.symbol,
780
+ quantity=request.amount
781
+ )
782
+
783
+ if res and res.get("txnId"):
784
+ response.success = True
785
+ response.result = json.dumps(res)
786
+ return response
787
+
770
788
  async def transfer_to_master(self, request: mr.MarketRequest) -> mr.SimpleResponse:
771
789
  response = mr.SimpleResponse()
772
790
  response.success = False
@@ -407,6 +407,7 @@ class MarketRequest(betterproto.Message):
407
407
  trade_id: str = betterproto.string_field(2)
408
408
  symbol: str = betterproto.string_field(3)
409
409
  amount: str = betterproto.string_field(4)
410
+ data: str = betterproto.string_field(5)
410
411
 
411
412
 
412
413
  @dataclass(eq=False, repr=False)
@@ -916,6 +917,23 @@ class MartinStub(betterproto.ServiceStub):
916
917
  metadata=metadata,
917
918
  )
918
919
 
920
+ async def transfer_to_sub(
921
+ self,
922
+ market_request: "MarketRequest",
923
+ *,
924
+ timeout: Optional[float] = None,
925
+ deadline: Optional["Deadline"] = None,
926
+ metadata: Optional["MetadataLike"] = None
927
+ ) -> "SimpleResponse":
928
+ return await self._unary_unary(
929
+ "/martin.Martin/TransferToSub",
930
+ market_request,
931
+ SimpleResponse,
932
+ timeout=timeout,
933
+ deadline=deadline,
934
+ metadata=metadata,
935
+ )
936
+
919
937
 
920
938
  class MartinBase(ServiceBase):
921
939
 
@@ -1056,6 +1074,11 @@ class MartinBase(ServiceBase):
1056
1074
  ) -> "SimpleResponse":
1057
1075
  raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
1058
1076
 
1077
+ async def transfer_to_sub(
1078
+ self, market_request: "MarketRequest"
1079
+ ) -> "SimpleResponse":
1080
+ raise grpclib.GRPCError(grpclib.const.Status.UNIMPLEMENTED)
1081
+
1059
1082
  async def __rpc_cancel_all_orders(
1060
1083
  self, stream: "grpclib.server.Stream[MarketRequest, SimpleResponse]"
1061
1084
  ) -> None:
@@ -1270,6 +1293,13 @@ class MartinBase(ServiceBase):
1270
1293
  response = await self.transfer_to_master(request)
1271
1294
  await stream.send_message(response)
1272
1295
 
1296
+ async def __rpc_transfer_to_sub(
1297
+ self, stream: "grpclib.server.Stream[MarketRequest, SimpleResponse]"
1298
+ ) -> None:
1299
+ request = await stream.recv_message()
1300
+ response = await self.transfer_to_sub(request)
1301
+ await stream.send_message(response)
1302
+
1273
1303
  def __mapping__(self) -> Dict[str, grpclib.const.Handler]:
1274
1304
  return {
1275
1305
  "/martin.Martin/CancelAllOrders": grpclib.const.Handler(
@@ -1434,4 +1464,10 @@ class MartinBase(ServiceBase):
1434
1464
  MarketRequest,
1435
1465
  SimpleResponse,
1436
1466
  ),
1467
+ "/martin.Martin/TransferToSub": grpclib.const.Handler(
1468
+ self.__rpc_transfer_to_sub,
1469
+ grpclib.const.Cardinality.UNARY_UNARY,
1470
+ MarketRequest,
1471
+ SimpleResponse,
1472
+ ),
1437
1473
  }
@@ -452,7 +452,6 @@ def on_balance_update(data_in: list, ts: str, symbol: str, mode: str, uid=None)
452
452
  }
453
453
  }
454
454
  )
455
-
456
455
  elif mode == 'universal':
457
456
  for i in data_in:
458
457
  if i['coin'] in symbol and \
@@ -469,7 +468,6 @@ def on_balance_update(data_in: list, ts: str, symbol: str, mode: str, uid=None)
469
468
  }
470
469
  }
471
470
  )
472
-
473
471
  elif mode == 'log':
474
472
  for i in data_in:
475
473
  if i['currency'] in symbol:
@@ -484,7 +482,6 @@ def on_balance_update(data_in: list, ts: str, symbol: str, mode: str, uid=None)
484
482
  }
485
483
  }
486
484
  )
487
-
488
485
  return data_out
489
486
 
490
487
 
@@ -16,7 +16,7 @@ def fetch_server_time(res: []) -> {}:
16
16
  def exchange_info(server_time: int, trading_symbol: [], tickers: [], symbol_t) -> {}:
17
17
  symbols = []
18
18
  symbols_price = {
19
- pair.get('instId').replace('-', ''): Decimal(pair.get('last'))
19
+ pair.get('instId').replace('-', ''): Decimal(pair.get('last', '0'))
20
20
  for pair in tickers
21
21
  }
22
22
  for market in trading_symbol:
@@ -38,6 +38,7 @@ service Martin {
38
38
  rpc StartStream (StartStreamRequest) returns (SimpleResponse) {}
39
39
  rpc StopStream (MarketRequest) returns (SimpleResponse) {}
40
40
  rpc TransferToMaster(MarketRequest) returns (SimpleResponse) {}
41
+ rpc TransferToSub(MarketRequest) returns (SimpleResponse) {}
41
42
  }
42
43
 
43
44
  message JSONResponse {
@@ -368,6 +369,7 @@ message MarketRequest {
368
369
  string trade_id = 2;
369
370
  string symbol = 3;
370
371
  string amount = 4;
372
+ string data = 5;
371
373
  }
372
374
 
373
375
  message StartStreamRequest {
@@ -17,10 +17,7 @@ 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
 
20
- logger = logging.getLogger('exch_srv_logger')
21
-
22
- logger_ws = logging.getLogger(__name__)
23
- logger_ws.level = logging.INFO
20
+ logger = logging.getLogger(__name__)
24
21
  formatter = logging.Formatter(fmt="[%(asctime)s: %(levelname)s] %(message)s")
25
22
  #
26
23
  fh = logging.handlers.RotatingFileHandler(Path(LOG_PATH, 'websockets.log'), maxBytes=1000000, backupCount=10)
@@ -31,8 +28,9 @@ sh = logging.StreamHandler()
31
28
  sh.setFormatter(formatter)
32
29
  sh.setLevel(logging.INFO)
33
30
 
34
- logger_ws.addHandler(fh)
35
- logger_ws.addHandler(sh)
31
+ logger.addHandler(fh)
32
+ logger.addHandler(sh)
33
+ logger.propagate = False
36
34
 
37
35
  sys.tracebacklimit = 0
38
36
 
@@ -50,12 +48,13 @@ class EventsDataStream:
50
48
  self._price = None
51
49
  self.tasks = set()
52
50
  self.wss_started = False
51
+ self.ping = 0
53
52
 
54
53
  async def start(self):
55
54
  ping_interval = None if self.exchange == 'huobi' else 20
56
55
  async for self.websocket in websockets.connect(
57
56
  self.endpoint,
58
- logger=logger_ws,
57
+ logger=logger,
59
58
  ping_interval=ping_interval
60
59
  ):
61
60
  try:
@@ -66,7 +65,7 @@ class EventsDataStream:
66
65
  logger.info(f"WSS closed for {self.exchange}:{self.trade_id}")
67
66
  break
68
67
  else:
69
- logger.warning(f"Restart WSS for {self.exchange}")
68
+ logger.warning(f"Restart WSS for {self.exchange}: {ex}")
70
69
  continue
71
70
  except Exception as ex:
72
71
  self.tasks_cancel()
@@ -187,9 +186,11 @@ class EventsDataStream:
187
186
  logger.debug(f"Bitfinex undefined WSS: symbol: {symbol}, ch_type: {ch_type}, msg_data: {msg_data}")
188
187
  elif self.exchange == 'huobi':
189
188
  if ping := msg_data.get('ping'):
189
+ self.ping = 0
190
190
  await self.websocket.send(json.dumps({"pong": ping}))
191
191
  await asyncio.sleep(0)
192
192
  elif msg_data.get('action') == 'ping':
193
+ self.ping = 0
193
194
  pong = {
194
195
  "action": "pong",
195
196
  "data": {
@@ -229,6 +230,17 @@ class EventsDataStream:
229
230
  await asyncio.sleep(interval)
230
231
  await self.websocket.send(json.dumps({"req_id": req_id, "op": "ping"}))
231
232
 
233
+ async def htx_keepalive(self, interval=60):
234
+ await asyncio.sleep(interval * 10)
235
+ while True:
236
+ await asyncio.sleep(interval)
237
+ if self.ping:
238
+ break
239
+ else:
240
+ self.ping = 1
241
+ logger.warning("From HTX server PING timeout exceeded")
242
+ await self.websocket.close()
243
+
232
244
 
233
245
  class MarketEventsDataStream(EventsDataStream):
234
246
  def __init__(self, client, endpoint, exchange, trade_id, channel=None):
@@ -300,6 +312,8 @@ class MarketEventsDataStream(EventsDataStream):
300
312
  elif ch_type == 'depth5':
301
313
  request = {'sub': f"market.{symbol}.depth.step0"}
302
314
 
315
+ self.tasks_manage(self.htx_keepalive(interval=30))
316
+
303
317
  await self.ws_listener(request, symbol, ch_type)
304
318
 
305
319
  async def _handle_event(self, content, symbol=None, ch_type=str()):
@@ -404,6 +418,7 @@ class HbpPrivateEventsDataStream(EventsDataStream):
404
418
  "action": "sub",
405
419
  "ch": f"trade.clearing#{self.symbol.lower()}#0"
406
420
  }
421
+ self.tasks_manage(self.htx_keepalive())
407
422
  await self.ws_listener(request, symbol=self.symbol)
408
423
 
409
424
  async def _handle_event(self, msg_data, *args):
@@ -22,7 +22,6 @@ dependencies = [
22
22
  "pyotp~=2.9.0",
23
23
  "simplejson==3.19.2",
24
24
  "aiohttp==3.9.5",
25
- "Pympler~=1.0.1",
26
25
  "websockets~=12.0",
27
26
  "expiringdict~=1.2.2",
28
27
  "ujson~=5.10.0",