/hummingbot/connector/exchange/binance/binance_api_user_stream_data_source.py

https://github.com/CoinAlpha/hummingbot · Python · 148 lines · 125 code · 19 blank · 4 comment · 34 complexity · 1ece9aff133a64d25b8e808489073702 MD5 · raw file

  1. #!/usr/bin/env python
  2. import asyncio
  3. import aiohttp
  4. import logging
  5. import time
  6. from typing import (
  7. AsyncIterable,
  8. Dict,
  9. Optional
  10. )
  11. import ujson
  12. import websockets
  13. from hummingbot.core.data_type.user_stream_tracker_data_source import UserStreamTrackerDataSource
  14. from hummingbot.core.utils.async_utils import safe_ensure_future
  15. from binance.client import Client as BinanceClient
  16. from hummingbot.logger import HummingbotLogger
  17. BINANCE_API_ENDPOINT = "https://api.binance.com/api/v1/"
  18. BINANCE_USER_STREAM_ENDPOINT = "userDataStream"
  19. class BinanceAPIUserStreamDataSource(UserStreamTrackerDataSource):
  20. MESSAGE_TIMEOUT = 30.0
  21. PING_TIMEOUT = 10.0
  22. _bausds_logger: Optional[HummingbotLogger] = None
  23. @classmethod
  24. def logger(cls) -> HummingbotLogger:
  25. if cls._bausds_logger is None:
  26. cls._bausds_logger = logging.getLogger(__name__)
  27. return cls._bausds_logger
  28. def __init__(self, binance_client: BinanceClient):
  29. self._binance_client: BinanceClient = binance_client
  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 last_recv_time(self) -> float:
  36. return self._last_recv_time
  37. async def get_listen_key(self):
  38. async with aiohttp.ClientSession() as client:
  39. async with client.post(f"{BINANCE_API_ENDPOINT}{BINANCE_USER_STREAM_ENDPOINT}",
  40. headers={"X-MBX-APIKEY": self._binance_client.API_KEY}) as response:
  41. response: aiohttp.ClientResponse = response
  42. if response.status != 200:
  43. raise IOError(f"Error fetching Binance user stream listen key. HTTP status is {response.status}.")
  44. data: Dict[str, str] = await response.json()
  45. return data["listenKey"]
  46. async def ping_listen_key(self, listen_key: str) -> bool:
  47. async with aiohttp.ClientSession() as client:
  48. async with client.put(f"{BINANCE_API_ENDPOINT}{BINANCE_USER_STREAM_ENDPOINT}",
  49. headers={"X-MBX-APIKEY": self._binance_client.API_KEY},
  50. params={"listenKey": listen_key}) as response:
  51. data: [str, any] = await response.json()
  52. if "code" in data:
  53. self.logger().warning(f"Failed to refresh the listen key {listen_key}: {data}")
  54. return False
  55. return True
  56. async def _inner_messages(self, ws: websockets.WebSocketClientProtocol) -> AsyncIterable[str]:
  57. # Terminate the recv() loop as soon as the next message timed out, so the outer loop can reconnect.
  58. try:
  59. while True:
  60. try:
  61. msg: str = await asyncio.wait_for(ws.recv(), timeout=self.MESSAGE_TIMEOUT)
  62. self._last_recv_time = time.time()
  63. yield msg
  64. except asyncio.TimeoutError:
  65. try:
  66. pong_waiter = await ws.ping()
  67. await asyncio.wait_for(pong_waiter, timeout=self.PING_TIMEOUT)
  68. self._last_recv_time = time.time()
  69. except asyncio.TimeoutError:
  70. raise
  71. except asyncio.TimeoutError:
  72. self.logger().warning("WebSocket ping timed out. Going to reconnect...")
  73. return
  74. except websockets.exceptions.ConnectionClosed:
  75. return
  76. finally:
  77. await ws.close()
  78. async def messages(self) -> AsyncIterable[str]:
  79. async with (await self.get_ws_connection()) as ws:
  80. async for msg in self._inner_messages(ws):
  81. yield msg
  82. async def get_ws_connection(self) -> websockets.WebSocketClientProtocol:
  83. stream_url: str = f"wss://stream.binance.com:9443/ws/{self._current_listen_key}"
  84. self.logger().info(f"Reconnecting to {stream_url}.")
  85. # Create the WS connection.
  86. return websockets.connect(stream_url)
  87. async def listen_for_user_stream(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue):
  88. try:
  89. while True:
  90. try:
  91. if self._current_listen_key is None:
  92. self._current_listen_key = await self.get_listen_key()
  93. self.logger().debug(f"Obtained listen key {self._current_listen_key}.")
  94. if self._listen_for_user_stream_task is not None:
  95. self._listen_for_user_stream_task.cancel()
  96. self._listen_for_user_stream_task = safe_ensure_future(self.log_user_stream(output))
  97. await self.wait_til_next_tick(seconds=60.0)
  98. success: bool = await self.ping_listen_key(self._current_listen_key)
  99. if not success:
  100. self._current_listen_key = None
  101. if self._listen_for_user_stream_task is not None:
  102. self._listen_for_user_stream_task.cancel()
  103. self._listen_for_user_stream_task = None
  104. continue
  105. self.logger().debug(f"Refreshed listen key {self._current_listen_key}.")
  106. await self.wait_til_next_tick(seconds=60.0)
  107. except asyncio.CancelledError:
  108. raise
  109. except Exception:
  110. self.logger().error("Unexpected error while maintaining the user event listen key. Retrying after "
  111. "5 seconds...", exc_info=True)
  112. await asyncio.sleep(5)
  113. finally:
  114. # Make sure no background task is leaked.
  115. if self._listen_for_user_stream_task is not None:
  116. self._listen_for_user_stream_task.cancel()
  117. self._listen_for_user_stream_task = None
  118. self._current_listen_key = None
  119. async def log_user_stream(self, output: asyncio.Queue):
  120. while True:
  121. try:
  122. async for message in self.messages():
  123. decoded: Dict[str, any] = ujson.loads(message)
  124. output.put_nowait(decoded)
  125. except asyncio.CancelledError:
  126. raise
  127. except Exception:
  128. self.logger().error("Unexpected error. Retrying after 5 seconds...", exc_info=True)
  129. await asyncio.sleep(5.0)