/hummingbot/connector/exchange/liquid/liquid_api_user_stream_data_source.py

https://github.com/CoinAlpha/hummingbot · Python · 140 lines · 100 code · 17 blank · 23 comment · 20 complexity · 968e761e5cc07d9e841d4758e8ccdb20 MD5 · raw file

  1. #!/usr/bin/env python
  2. import asyncio
  3. import logging
  4. from typing import (
  5. Any,
  6. AsyncIterable,
  7. Dict,
  8. Optional,
  9. List,
  10. )
  11. import time
  12. import ujson
  13. import websockets
  14. from websockets.exceptions import ConnectionClosed
  15. from hummingbot.logger import HummingbotLogger
  16. from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource
  17. from hummingbot.connector.exchange.liquid.constants import Constants
  18. from hummingbot.connector.exchange.liquid.liquid_auth import LiquidAuth
  19. from hummingbot.connector.exchange.liquid.liquid_order_book import LiquidOrderBook
  20. class LiquidAPIUserStreamDataSource(UserStreamTrackerDataSource):
  21. _lausds_logger: Optional[HummingbotLogger] = None
  22. @classmethod
  23. def logger(cls) -> HummingbotLogger:
  24. if cls._lausds_logger is None:
  25. cls._lausds_logger = logging.getLogger(__name__)
  26. return cls._lausds_logger
  27. def __init__(self, liquid_auth: LiquidAuth, trading_pairs: Optional[List[str]] = []):
  28. self._liquid_auth: LiquidAuth = liquid_auth
  29. self._trading_pairs = trading_pairs
  30. self._current_listen_key = None
  31. self._listen_for_user_stream_task = None
  32. self._last_recv_time: float = 0
  33. super().__init__()
  34. @property
  35. def order_book_class(self):
  36. """
  37. *required
  38. Get relevant order book class to access class specific methods
  39. :returns: OrderBook class
  40. """
  41. return LiquidOrderBook
  42. @property
  43. def last_recv_time(self) -> float:
  44. return self._last_recv_time
  45. async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue):
  46. """
  47. *required
  48. Subscribe to user stream via web socket, and keep the connection open for incoming messages
  49. :param ev_loop: ev_loop to execute this function in
  50. :param output: an async queue where the incoming messages are stored
  51. """
  52. while True:
  53. try:
  54. async with websockets.connect(Constants.BAEE_WS_URL) as ws:
  55. ws: websockets.WebSocketClientProtocol = ws
  56. ev_loop.create_task(self.custom_ping(ws))
  57. # Send a auth request first
  58. auth_request: Dict[str, Any] = {
  59. "event": Constants.WS_AUTH_REQUEST_EVENT,
  60. "data": self._liquid_auth.get_ws_auth_data()
  61. }
  62. await ws.send(ujson.dumps(auth_request))
  63. quoted_currencies = [
  64. trading_pair.split('-')[1]
  65. for trading_pair in self._trading_pairs
  66. ]
  67. for trading_pair, quoted_currency in zip(self._trading_pairs, quoted_currencies):
  68. subscribe_request: Dict[str, Any] = {
  69. "event": Constants.WS_PUSHER_SUBSCRIBE_EVENT,
  70. "data": {
  71. "channel": Constants.WS_USER_ACCOUNTS_SUBSCRIPTION.format(
  72. quoted_currency=quoted_currency.lower()
  73. )
  74. }
  75. }
  76. await ws.send(ujson.dumps(subscribe_request))
  77. async for raw_msg in self._inner_messages(ws):
  78. diff_msg = ujson.loads(raw_msg)
  79. event_type = diff_msg.get('event', None)
  80. if event_type == 'updated':
  81. output.put_nowait(diff_msg)
  82. self._last_recv_time = time.time()
  83. elif event_type == "pusher:pong":
  84. self._last_recv_time = time.time()
  85. elif not event_type:
  86. raise ValueError(f"Liquid Websocket message does not contain an event type - {diff_msg}")
  87. except asyncio.CancelledError:
  88. raise
  89. except Exception:
  90. self.logger().error("Unexpected error with Liquid WebSocket connection. "
  91. "Retrying after 30 seconds...", exc_info=True)
  92. await asyncio.sleep(30.0)
  93. async def _inner_messages(self,
  94. ws: websockets.WebSocketClientProtocol) -> AsyncIterable[str]:
  95. """
  96. Generator function that returns messages from the web socket stream
  97. :param ws: current web socket connection
  98. :returns: message in AsyncIterable format
  99. """
  100. # Terminate the recv() loop as soon as the next message timed out, so the outer loop can reconnect.
  101. try:
  102. while True:
  103. msg: str = await asyncio.wait_for(ws.recv(), timeout=Constants.MESSAGE_TIMEOUT)
  104. yield msg
  105. except asyncio.TimeoutError:
  106. self.logger().warning("WebSocket message timed out. Going to reconnect...")
  107. return
  108. except ConnectionClosed:
  109. return
  110. finally:
  111. await ws.close()
  112. async def custom_ping(self, ws: websockets.WebSocketClientProtocol):
  113. """
  114. Sends a ping meassage to the Liquid websocket
  115. :param ws: current web socket connection
  116. """
  117. ping_data: Dict[str, Any] = {"event": "pusher:ping", "data": {}}
  118. try:
  119. while True:
  120. await ws.send(ujson.dumps(ping_data))
  121. await asyncio.sleep(60.0)
  122. except Exception:
  123. return