class CryptoComAPIUserStreamDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()

        cls.api_key = "someKey"
        cls.secret_key = "someSecretKey"
        cls.auth = CryptoComAuth(api_key=cls.api_key,
                                 secret_key=cls.secret_key)

    def setUp(self) -> None:
        super().setUp()

        self.log_records = []
        self.async_task: Optional[asyncio.Task] = None

        self.data_source = CryptoComAPIUserStreamDataSource(
            crypto_com_auth=self.auth)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def test_get_shared_client_not_shared_client_provided(self):
        self.assertIsNone(self.data_source._shared_client)
        self.assertIsInstance(self.data_source._get_shared_client(),
                              aiohttp.ClientSession)

    def test_get_shared_client_shared_client_provided(self):
        aiohttp_client = aiohttp.ClientSession()
        data_source = CryptoComAPIUserStreamDataSource(
            crypto_com_auth=self.auth, shared_client=aiohttp_client)
        self.assertEqual(data_source._get_shared_client(), aiohttp_client)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_create_websocket_connection_raised_cancelled(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source._create_websocket_connection())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_create_websocket_connection_logs_exception(self, ws_connect_mock):
        ws_connect_mock.side_effect = Exception("TEST ERROR")

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(
                self.data_source._create_websocket_connection())

        self.assertTrue(
            self._is_logged(
                "NETWORK",
                "Unexpected error occured connecting to crypto_com WebSocket API. (TEST ERROR)"
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_user_stream_raises_cancelled_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(asyncio.Queue()))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_user_stream_raises_logs_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR"))
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(asyncio.Queue()))

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when listening to user streams. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_user_stream_successful(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        auth_response = {"id": 1, "method": "public/auth", "code": 0}

        balance_response = {
            "method": "subscribe",
            "result": {
                "subscription":
                "user.balance",
                "channel":
                "user.balance",
                "data": [{
                    "currency": "COINALPHA",
                    "balance": 1,
                    "available": 1,
                    "order": 0,
                    "stake": 0
                }],
                "channel":
                "user.balance"
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, ujson.dumps(auth_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, ujson.dumps(balance_response))

        user_stream_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(user_stream_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(self.data_source.ready)
        self.assertEqual(1, user_stream_queue.qsize())
class KrakenAPIOrderBookDataSourceTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.api_tier = KrakenAPITier.STARTER

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(
            build_rate_limits_by_tier(self.api_tier))
        self.data_source = KrakenAPIOrderBookDataSource(
            self.throttler, trading_pairs=[self.trading_pair])

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def get_last_traded_prices_mock(self, last_trade_close: Decimal) -> Dict:
        last_traded_prices = {
            "error": [],
            "result": {
                f"X{self.base_asset}{self.quote_asset}": {
                    "a": ["52609.60000", "1", "1.000"],
                    "b": ["52609.50000", "1", "1.000"],
                    "c": [str(last_trade_close), "0.00080000"],
                    "v": ["1920.83610601", "7954.00219674"],
                    "p": ["52389.94668", "54022.90683"],
                    "t": [23329, 80463],
                    "l": ["51513.90000", "51513.90000"],
                    "h": ["53219.90000", "57200.00000"],
                    "o": "52280.40000"
                }
            }
        }
        return last_traded_prices

    def get_depth_mock(self) -> Dict:
        depth = {
            "error": [],
            "result": {
                f"X{self.base_asset}{self.quote_asset}": {
                    "asks": [["52523.00000", "1.199", 1616663113],
                             ["52536.00000", "0.300", 1616663112]],
                    "bids": [["52522.90000", "0.753", 1616663112],
                             ["52522.80000", "0.006", 1616663109]]
                }
            }
        }
        return depth

    def get_public_asset_pair_mock(self) -> Dict:
        asset_pairs = {
            "error": [],
            "result": {
                f"X{self.base_asset}{self.quote_asset}": {
                    "altname": f"{self.base_asset}{self.quote_asset}",
                    "wsname": f"{self.base_asset}/{self.quote_asset}",
                    "aclass_base": "currency",
                    "base": self.base_asset,
                    "aclass_quote": "currency",
                    "quote": self.quote_asset,
                    "lot": "unit",
                    "pair_decimals": 5,
                    "lot_decimals": 8,
                    "lot_multiplier": 1,
                    "leverage_buy": [2, 3, 4, 5],
                    "leverage_sell": [2, 3, 4, 5],
                    "fees": [
                        [0, 0.26],
                        [50000, 0.24],
                    ],
                    "fees_maker": [
                        [0, 0.16],
                        [50000, 0.14],
                    ],
                    "fee_volume_currency": "ZUSD",
                    "margin_call": 80,
                    "margin_stop": 40,
                    "ordermin": "0.005"
                },
            }
        }
        return asset_pairs

    def get_trade_data_mock(self) -> List:
        trade_data = [
            0,
            [["5541.20000", "0.15850568", "1534614057.321597", "s", "l", ""],
             ["6060.00000", "0.02455000", "1534614057.324998", "b", "l", ""]],
            "trade", f"{self.base_asset}/{self.quote_asset}"
        ]
        return trade_data

    @aioresponses()
    def test_get_last_traded_prices(self, mocked_api):
        url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        last_traded_price = Decimal("52641.10000")
        resp = self.get_last_traded_prices_mock(
            last_trade_close=last_traded_price)
        mocked_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            KrakenAPIOrderBookDataSource.get_last_traded_prices(
                trading_pairs=[self.trading_pair], throttler=self.throttler))

        self.assertIn(self.trading_pair, ret)
        self.assertEqual(float(last_traded_price), ret[self.trading_pair])

    @aioresponses()
    def test_get_new_order_book(self, mocked_api):
        url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_depth_mock()
        mocked_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertTrue(isinstance(ret, OrderBook))

        bids_df, asks_df = ret.snapshot
        pair_data = resp["result"][f"X{self.base_asset}{self.quote_asset}"]
        first_bid_price = float(pair_data["bids"][0][0])
        first_ask_price = float(pair_data["asks"][0][0])

        self.assertEqual(first_bid_price, bids_df.iloc[0]["price"])
        self.assertEqual(first_ask_price, asks_df.iloc[0]["price"])

    @aioresponses()
    def test_fetch_trading_pairs(self, mocked_api):
        url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_public_asset_pair_mock()
        mocked_api.get(regex_url, body=json.dumps(resp))

        resp = self.async_run_with_timeout(
            KrakenAPIOrderBookDataSource.fetch_trading_pairs())

        self.assertTrue(len(resp) == 1)
        self.assertIn(self.trading_pair, resp)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_trade_data_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        output_queue = asyncio.Queue()

        self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, output_queue))
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            websocket_mock=ws_connect_mock.return_value)

        self.assertTrue(not output_queue.empty())
        msg = output_queue.get_nowait()
        self.assertTrue(isinstance(msg, OrderBookMessage))
        first_trade_price = resp[1][0][0]
        self.assertEqual(msg.content["price"], first_trade_price)

        self.assertTrue(not output_queue.empty())
        msg = output_queue.get_nowait()
        self.assertTrue(isinstance(msg, OrderBookMessage))
        second_trade_price = resp[1][1][0]
        self.assertEqual(msg.content["price"], second_trade_price)
Beispiel #3
0
class BitmexUserStreamDataSourceUnitTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}"
        cls.domain = CONSTANTS.TESTNET_DOMAIN

        cls.api_key = "TEST_API_KEY"
        cls.secret_key = "TEST_SECRET_KEY"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.emulated_time = 1640001112.223
        self.auth = BitmexAuth(api_key=self.api_key,
                               api_secret=self.secret_key)
        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.data_source = BitmexUserStreamDataSource(auth=self.auth,
                                                      domain=self.domain,
                                                      throttler=self.throttler)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mock_done_event = asyncio.Event()
        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _mock_responses_done_callback(self, *_, **__):
        self.mock_done_event.set()

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _error_response(self) -> Dict[str, Any]:
        resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"}

        return resp

    def _simulate_user_update_event(self):
        # Order Trade Update
        resp = {
            "table":
            "execution",
            "data": [{
                "orderID": "1",
                "clordID": "2",
                "price": 20,
                "orderQty": 100,
                "symbol": "COINALPHA_HBOT",
                "side": "Sell",
                "leavesQty": "1"
            }],
        }
        return ujson.dumps(resp)

    def time(self):
        # Implemented to emulate a TimeSynchronizer
        return self.emulated_time

    def test_last_recv_time(self):
        # Initial last_recv_time
        self.assertEqual(0, self.data_source.last_recv_time)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_create_websocket_connection_log_exception(self, mock_ws):
        mock_ws.side_effect = Exception("TEST ERROR.")

        msg_queue = asyncio.Queue()
        try:
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(
                    self.ev_loop, msg_queue))
        except asyncio.exceptions.TimeoutError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.",
            ))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_create_websocket_connection_failed(
            self, mock_api, mock_ws):
        mock_ws.side_effect = Exception("TEST ERROR.")

        msg_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(msg_queue.get())
        except asyncio.exceptions.TimeoutError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.",
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listen_for_user_stream_iter_message_throws_exception(
            self, _, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = Exception("TEST ERROR")
        mock_ws.return_value.closed = False
        mock_ws.return_value.close.side_effect = Exception

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(msg_queue.get())
        except Exception:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR",
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_successful(self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._simulate_user_update_event())

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop, msg_queue))

        msg = self.async_run_with_timeout(msg_queue.get())
        self.assertTrue(msg, self._simulate_user_update_event)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_empty_payload(
            self, mock_api, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, "")

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop, msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())
Beispiel #4
0
class ProbitAPIUserStreamDataSourceTest(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        cls.base_asset = "BTC"
        cls.quote_asset = "USDT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()

        self.ev_loop = asyncio.get_event_loop()

        self.api_key = "someKey"
        self.api_secret = "someSecret"
        self.auth = ProbitAuth(self.api_key, self.api_secret)
        self.data_source = ProbitAPIUserStreamDataSource(
            self.auth, trading_pairs=[self.trading_pair])
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.log_records = []
        self.mocking_assistant = NetworkMockingAssistant()

        self.async_task: Optional[asyncio.Task] = None

    def tearDown(self) -> None:
        self.async_task and self.async_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def check_is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.probit.probit_auth.ProbitAuth.get_ws_auth_payload",
        new_callable=AsyncMock,
    )
    def test_listen_for_user_stream(self, get_ws_auth_payload_mock,
                                    ws_connect_mock):
        auth_msg = {"type": "authorization", "token": "someToken"}
        get_ws_auth_payload_mock.return_value = auth_msg

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.mocking_assistant.add_websocket_json_message(
            ws_connect_mock.return_value,
            message={"result": "ok"}  # authentication
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps({"my_msg": "test"})  # first message
        )

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))

        self.mocking_assistant.run_until_all_json_messages_delivered(
            ws_connect_mock.return_value)
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        sent_text_msgs = self.mocking_assistant.text_messages_sent_through_websocket(
            ws_connect_mock.return_value)
        self.assertEqual(auth_msg, json.loads(sent_text_msgs[0]))

        sent_json_msgs = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)
        for sent_json_msg in sent_json_msgs:
            self.assertEqual("subscribe", sent_json_msg["type"])
            self.assertIn(sent_json_msg["channel"],
                          CONSTANTS.WS_PRIVATE_CHANNELS)
            CONSTANTS.WS_PRIVATE_CHANNELS.remove(sent_json_msg["channel"])

        self.assertEqual(0, len(CONSTANTS.WS_PRIVATE_CHANNELS))
        self.assertNotEqual(0, self.data_source.last_recv_time)

    @patch("aiohttp.client.ClientSession.ws_connect")
    @patch(
        "hummingbot.connector.exchange.probit.probit_api_user_stream_data_source.ProbitAPIUserStreamDataSource._sleep",
        new_callable=AsyncMock,
    )
    def test_listen_for_user_stream_attempts_again_on_exception(
            self, sleep_mock, ws_connect_mock):
        called_event = asyncio.Event()

        async def _sleep(delay):
            called_event.set()
            await asyncio.sleep(delay)

        sleep_mock.side_effect = _sleep

        ws_connect_mock.side_effect = Exception
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(asyncio.Queue()))

        self.async_run_with_timeout(called_event.wait())

        self.check_is_logged(
            log_level="ERROR",
            message=
            "Unexpected error with Probit WebSocket connection. Retrying after 30 seconds...",
        )

    @patch("aiohttp.client.ClientSession.ws_connect")
    def test_listen_for_user_stream_stops_on_asyncio_cancelled_error(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(asyncio.Queue()))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.probit.probit_auth.ProbitAuth.get_ws_auth_payload",
        new_callable=AsyncMock,
    )
    def test_listen_for_user_stream_registers_ping_msg(
            self, get_ws_auth_payload_mock, ws_connect_mock):
        auth_msg = {"type": "authorization", "token": "someToken"}
        get_ws_auth_payload_mock.return_value = auth_msg

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.mocking_assistant.add_websocket_json_message(
            ws_connect_mock.return_value,
            message={"result": "ok"}  # authentication
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=WSMsgType.PING)
        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())
        ws_connect_mock.return_value.pong.assert_called()
Beispiel #5
0
class CoinbaseProAPIOrderBookDataSourceTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        auth = CoinbaseProAuth(api_key="SomeAPIKey", secret_key="SomeSecretKey", passphrase="SomePassPhrase")
        web_assistants_factory = build_coinbase_pro_web_assistant_factory(auth)
        self.data_source = CoinbaseProAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair], web_assistants_factory=web_assistants_factory
        )
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.log_records = []
        self.async_tasks: List[asyncio.Task] = []

    def tearDown(self) -> None:
        for task in self.async_tasks:
            task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(record.levelname == log_level and record.getMessage() == message
                   for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout))
        return ret

    @staticmethod
    def get_products_ticker_response_mock(price: float) -> Dict:
        products_ticker_mock = {
            "trade_id": 86326522,
            "price": str(price),
            "size": "0.00698254",
            "time": "2020-03-20T00:22:57.833897Z",
            "bid": "6265.15",
            "ask": "6267.71",
            "volume": "53602.03940154"
        }
        return products_ticker_mock

    def get_products_response_mock(self, other_pair: str) -> List:
        products_mock = [
            {
                "id": self.trading_pair,
                "base_currency": self.base_asset,
                "quote_currency": self.quote_asset,
                "base_min_size": "0.00100000",
                "base_max_size": "280.00000000",
                "quote_increment": "0.01000000",
                "base_increment": "0.00000001",
                "display_name": f"{self.base_asset}/{self.quote_asset}",
                "min_market_funds": "10",
                "max_market_funds": "1000000",
                "margin_enabled": False,
                "post_only": False,
                "limit_only": False,
                "cancel_only": False,
                "status": "online",
                "status_message": "",
                "auction_mode": True,
            },
            {
                "id": other_pair,
                "base_currency": other_pair.split("-")[0],
                "quote_currency": other_pair.split("-")[1],
                "base_min_size": "0.00100000",
                "base_max_size": "280.00000000",
                "quote_increment": "0.01000000",
                "base_increment": "0.00000001",
                "display_name": other_pair.replace("-", "/"),
                "min_market_funds": "10",
                "max_market_funds": "1000000",
                "margin_enabled": False,
                "post_only": False,
                "limit_only": False,
                "cancel_only": False,
                "status": "online",
                "status_message": "",
                "auction_mode": True,
            }
        ]
        return products_mock

    @staticmethod
    def get_products_book_response_mock(
        bids: Optional[List[List[str]]] = None, asks: Optional[List[List[str]]] = None
    ) -> Dict:
        bids = bids or [["1", "2", "3"]]
        asks = asks or [["4", "5", "6"]]
        products_book_mock = {
            "sequence": 13051505638,
            "bids": bids,
            "asks": asks,
        }
        return products_book_mock

    def get_ws_open_message_mock(self) -> Dict:
        message = {
            "type": "open",
            "time": "2014-11-07T08:19:27.028459Z",
            "product_id": self.trading_pair,
            "sequence": 10,
            "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
            "price": "200.2",
            "remaining_size": "1.00",
            "side": "sell"
        }
        return message

    def get_ws_match_message_mock(self) -> Dict:
        message = {
            "type": "match",
            "trade_id": 10,
            "sequence": 50,
            "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
            "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1",
            "time": "2014-11-07T08:19:27.028459Z",
            "product_id": self.trading_pair,
            "size": "5.23512",
            "price": "400.23",
            "side": "sell"
        }
        return message

    def get_ws_change_message_mock(self) -> Dict:
        message = {
            "type": "change",
            "time": "2014-11-07T08:19:27.028459Z",
            "sequence": 80,
            "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
            "product_id": self.trading_pair,
            "new_size": "5.23512",
            "old_size": "12.234412",
            "price": "400.23",
            "side": "sell"
        }
        return message

    def get_ws_done_message_mock(self) -> Dict:
        message = {
            "type": "done",
            "time": "2014-11-07T08:19:27.028459Z",
            "product_id": self.trading_pair,
            "sequence": 10,
            "price": "200.2",
            "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
            "reason": "filled",
            "side": "sell",
            "remaining_size": "0"
        }
        return message

    @aioresponses()
    def test_get_last_traded_prices(self, mock_api):
        alt_pair = "BTC-USDT"
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/ticker"
        alt_url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{alt_pair}/ticker"
        price = 10.0
        alt_price = 15.0
        resp = self.get_products_ticker_response_mock(price=price)
        alt_resp = self.get_products_ticker_response_mock(price=alt_price)
        mock_api.get(url, body=json.dumps(resp))
        mock_api.get(alt_url, body=json.dumps(alt_resp))

        trading_pairs = [self.trading_pair, alt_pair]
        ret = self.async_run_with_timeout(
            coroutine=CoinbaseProAPIOrderBookDataSource.get_last_traded_prices(trading_pairs)
        )

        self.assertEqual(ret[self.trading_pair], Decimal(resp["price"]))
        self.assertEqual(ret[alt_pair], Decimal(alt_resp["price"]))

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}"
        alt_pair = "BTC-USDT"
        resp = self.get_products_response_mock(alt_pair)
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(coroutine=CoinbaseProAPIOrderBookDataSource.fetch_trading_pairs())

        self.assertIn(self.trading_pair, ret)
        self.assertIn(alt_pair, ret)

    @aioresponses()
    def test_get_snapshot(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3"
        resp = self.get_products_book_response_mock()
        mock_api.get(url, body=json.dumps(resp))

        rest_assistant = self.ev_loop.run_until_complete(
            build_coinbase_pro_web_assistant_factory().get_rest_assistant()
        )
        ret = self.async_run_with_timeout(
            coroutine=CoinbaseProAPIOrderBookDataSource.get_snapshot(rest_assistant, self.trading_pair)
        )

        self.assertEqual(resp, ret)  # shallow comparison ok

    @aioresponses()
    def test_get_snapshot_raises_on_status_code(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3"
        resp = self.get_products_book_response_mock()
        mock_api.get(url, body=json.dumps(resp), status=401)

        rest_assistant = self.ev_loop.run_until_complete(
            build_coinbase_pro_web_assistant_factory().get_rest_assistant()
        )
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                coroutine=CoinbaseProAPIOrderBookDataSource.get_snapshot(rest_assistant, self.trading_pair)
            )

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3"
        resp = self.get_products_book_response_mock(bids=[["1", "2", "3"]], asks=[["4", "5", "6"]])
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(self.data_source.get_new_order_book(self.trading_pair))

        self.assertIsInstance(ret, OrderBook)

        bid_entries = list(ret.bid_entries())
        ask_entries = list(ret.ask_entries())

        self.assertEqual(1, len(bid_entries))
        self.assertEqual(1, len(ask_entries))

        bid_entry = bid_entries[0]
        ask_entry = ask_entries[0]

        self.assertEqual(1, bid_entry.price)
        self.assertEqual(4, ask_entry.price)

    @aioresponses()
    def test_get_tracking_pairs(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3"
        resp = self.get_products_book_response_mock(bids=[["1", "2", "3"]])
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(self.data_source.get_tracking_pairs())

        self.assertEqual(1, len(ret))

        tracker_entry = ret[self.trading_pair]

        self.assertIsInstance(tracker_entry, CoinbaseProOrderBookTrackerEntry)
        self.assertEqual(1, list(tracker_entry.order_book.bid_entries())[0].price)

    @aioresponses()
    def test_get_tracking_pairs_logs_io_error(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3"
        mock_api.get(url, exception=IOError)

        ret = self.async_run_with_timeout(self.data_source.get_tracking_pairs())

        self.assertEqual(0, len(ret))
        self.assertTrue(self._is_logged(
            log_level="NETWORK", message=f"Error getting snapshot for {self.trading_pair}.")
        )

    @aioresponses()
    def test_get_tracking_pairs_logs_other_exceptions(self, mock_api):
        url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3"
        mock_api.get(url, exception=RuntimeError)

        ret = self.async_run_with_timeout(self.data_source.get_tracking_pairs())

        self.assertEqual(0, len(ret))
        self.assertTrue(self._is_logged(
            log_level="ERROR", message=f"Error initializing order book for {self.trading_pair}. ")
        )

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_processes_open_message(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = self.get_ws_open_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        ob_message = output_queue.get_nowait()

        self.assertEqual(resp, ob_message.content)  # shallow comparison is ok

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_processes_match_message(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = self.get_ws_match_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        ob_message = output_queue.get_nowait()

        self.assertEqual(resp, ob_message.content)  # shallow comparison is ok

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_processes_change_message(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = self.get_ws_change_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        ob_message = output_queue.get_nowait()

        self.assertEqual(resp, ob_message.content)  # shallow comparison is ok

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_processes_done_message(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = self.get_ws_done_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        ob_message = output_queue.get_nowait()

        self.assertEqual(resp, ob_message.content)  # shallow comparison is ok

    @patch(
        "hummingbot.connector.exchange.coinbase_pro"
        ".coinbase_pro_api_order_book_data_source.CoinbaseProAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_raises_on_no_type(self, ws_connect_mock, _):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = {}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(log_level="NETWORK", message="Unexpected error with WebSocket connection.")
        )
        self.assertTrue(output_queue.empty())

    @patch(
        "hummingbot.connector.exchange.coinbase_pro"
        ".coinbase_pro_api_order_book_data_source.CoinbaseProAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_raises_on_error_msg(self, ws_connect_mock, _):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = {"type": "error", "message": "some error"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(log_level="NETWORK", message="Unexpected error with WebSocket connection.")
        )
        self.assertTrue(output_queue.empty())

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_ignores_irrelevant_messages(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps({"type": "received"})
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps({"type": "activate"})
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps({"type": "subscriptions"})
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())

    @patch(
        "hummingbot.connector.exchange.coinbase_pro"
        ".coinbase_pro_api_order_book_data_source.CoinbaseProAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_raises_on_unrecognized_message(self, ws_connect_mock, _):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = {"type": "some-new-message-type"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(log_level="NETWORK", message="Unexpected error with WebSocket connection.")
        )
        self.assertTrue(output_queue.empty())
Beispiel #6
0
class AscendExAPIOrderBookDataSourceTests(TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    def setUp(self) -> None:
        super().setUp()

        self.ev_loop = asyncio.get_event_loop()

        self.base_asset = "BTC"
        self.quote_asset = "USDT"
        self.trading_pair = f"{self.base_asset}-{self.quote_asset}"
        self.ex_trading_pair = f"{self.base_asset}/{self.quote_asset}"

        self.log_records = []
        self.listening_task = None
        self.async_task: Optional[asyncio.Task] = None

        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.api_factory = build_api_factory(throttler=self.throttler)

        self.data_source = AscendExAPIOrderBookDataSource(
            api_factory=self.api_factory,
            throttler=self.throttler,
            trading_pairs=[self.trading_pair])
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()

        AscendExAPIOrderBookDataSource._trading_pair_symbol_map = bidict(
            {self.ex_trading_pair: f"{self.base_asset}-{self.quote_asset}"})

    def tearDown(self) -> None:
        self.async_task and self.async_task.cancel()
        self.listening_task and self.listening_task.cancel()
        AscendExAPIOrderBookDataSource._trading_pair_symbol_map = None
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @aioresponses()
    def test_fetch_trading_pairs(self, api_mock):
        AscendExAPIOrderBookDataSource._trading_pair_symbol_map = None

        mock_response = {
            "data": [
                {
                    "symbol": self.ex_trading_pair,
                    "baseAsset": self.base_asset,
                    "quoteAsset": self.quote_asset,
                },
                {
                    "symbol": "ETH/USDT",
                    "baseAsset": "ETH",
                    "quoteAsset": "USDT",
                },
                {
                    "symbol": "DOGE/USDT",
                    "baseAsset": "DOGE",
                    "quoteAsset": "USDT",
                },
            ],
        }

        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.PRODUCTS_PATH_URL}"
        api_mock.get(url, body=json.dumps(mock_response))

        trading_pairs = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())

        self.assertEqual(3, len(trading_pairs))
        self.assertEqual("ETH-USDT", trading_pairs[1])

    @aioresponses()
    def test_get_last_traded_prices_requests_rest_api_price_when_subscription_price_not_available(
            self, api_mock):
        mock_response = {
            "code": 0,
            "data": {
                "m":
                "trades",
                "symbol":
                "BTC/USDT",
                "data": [
                    {
                        "seqnum": 144115191800016553,
                        "p": "0.06762",
                        "q": "400",
                        "ts": 1573165890854,
                        "bm": False
                    },
                    {
                        "seqnum": 144115191800070421,
                        "p": "0.06797",
                        "q": "341",
                        "ts": 1573166037845,
                        "bm": True
                    },
                ],
            },
        }

        self.data_source._trading_pairs = ["BTC-USDT"]

        url = re.escape(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.TRADES_PATH_URL}?symbol=")
        regex_url = re.compile(f"^{url}")
        api_mock.get(regex_url, body=json.dumps(mock_response))

        results = self.ev_loop.run_until_complete(
            self.data_source.get_last_traded_prices(
                trading_pairs=[self.trading_pair],
                api_factory=self.data_source._api_factory,
                throttler=self.throttler))

        self.assertEqual(results[self.trading_pair],
                         float(mock_response["data"]["data"][1]["p"]))

    @aioresponses()
    def test_get_order_book_http_error_raises_exception(self, api_mock):
        mock_response = "ERROR WITH REQUEST"
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}"
        regex_url = re.compile(f"^{url}")
        api_mock.get(regex_url, status=400, body=mock_response)

        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_order_book_data(
                    trading_pair=self.trading_pair, throttler=self.throttler))

    @aioresponses()
    def test_get_order_book_resp_code_erro_raises_exception(self, api_mock):
        mock_response = {
            "code": 100001,
            "reason": "INVALID_HTTP_INPUT",
            "message": "Http request is invalid"
        }
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}"
        regex_url = re.compile(f"^{url}")
        api_mock.get(regex_url, body=json.dumps(mock_response))

        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_order_book_data(
                    trading_pair=self.trading_pair, throttler=self.throttler))

    @aioresponses()
    def test_get_order_book_data_successful(self, api_mock):
        mock_response = {
            "code": 0,
            "data": {
                "m": "depth-snapshot",
                "symbol": self.ex_trading_pair,
                "data": {
                    "seqnum": 5068757,
                    "ts": 1573165838976,
                    "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]],
                    "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]],
                },
            },
        }
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}"
        regex_url = re.compile(f"^{url}")
        api_mock.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_order_book_data(
                trading_pair=self.trading_pair, throttler=self.throttler))

        self.assertTrue(result.get("symbol") == self.ex_trading_pair)

    @aioresponses()
    def test_get_new_order_book(self, api_mock):
        mock_response = {
            "code": 0,
            "data": {
                "m": "depth-snapshot",
                "symbol": "BTC/USDT",
                "data": {
                    "seqnum": 5068757,
                    "ts": 1573165838976,
                    "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]],
                    "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]],
                },
            },
        }

        self.data_source._trading_pairs = ["BTC-USDT"]

        # path_url = ascend_ex_utils.rest_api_path_for_endpoint(CONSTANTS.ORDER_BOOK_ENDPOINT, self.trading_pair)
        url = re.escape(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}?symbol=")
        regex_url = re.compile(f"^{url}")
        api_mock.get(regex_url, body=json.dumps(mock_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.get_new_order_book(self.trading_pair))
        order_book = self.ev_loop.run_until_complete(self.listening_task)
        bids = list(order_book.bid_entries())
        asks = list(order_book.ask_entries())

        self.assertEqual(2, len(bids))
        self.assertEqual(0.06703, round(bids[0].price, 5))
        self.assertEqual(13500, round(bids[0].amount, 1))
        self.assertEqual(1573165838976, bids[0].update_id)
        self.assertEqual(2, len(asks))
        self.assertEqual(0.06848, round(asks[0].price, 5))
        self.assertEqual(4084.2, round(asks[0].amount, 1))
        self.assertEqual(1573165838976, asks[0].update_id)

    @patch("aiohttp.client.ClientSession.ws_connect")
    def test_subscribe_to_order_book_streams_raises_exception(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = Exception("TEST ERROR")

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(
                self.data_source._subscribe_to_order_book_streams())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred subscribing to order book trading and delta streams..."
            ))

    @patch("aiohttp.client.ClientSession.ws_connect")
    def test_subscribe_to_order_book_streams_raises_cancel_exception(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source._subscribe_to_order_book_streams())

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_subscribe_to_order_book_streams_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.async_run_with_timeout(
            self.data_source._subscribe_to_order_book_streams())

        self.assertTrue(
            self._is_logged(
                "INFO",
                f"Subscribed to ['{self.trading_pair}'] orderbook trading and delta streams..."
            ))

        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)
        stream_topics = [payload["ch"] for payload in sent_messages]
        self.assertEqual(2, len(stream_topics))
        self.assertTrue(
            f"{self.data_source.DIFF_TOPIC_ID}:{self.ex_trading_pair}" in
            stream_topics)
        self.assertTrue(
            f"{self.data_source.TRADE_TOPIC_ID}:{self.ex_trading_pair}" in
            stream_topics)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_exception_raised_cancelled_when_connecting(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_exception_raised_cancelled_when_subscribing(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_exception_raised_cancelled_when_listening(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscription_logs_exception(self, ws_connect_mock,
                                                    sleep_mock):
        ws_connect_mock.side_effect = Exception("TEST ERROR.")
        sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(
            asyncio.CancelledError())

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_enqueues_diff_and_trade_messages(
            self, ws_connect_mock):
        diffs_queue = self.data_source._message_queue[
            self.data_source.DIFF_TOPIC_ID]
        trade_queue = self.data_source._message_queue[
            self.data_source.TRADE_TOPIC_ID]

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add diff event message be processed
        diff_response = {
            "m": "depth",
            "symbol": self.ex_trading_pair,
            "data": {
                "ts": 1573069021376,
                "seqnum": 2097965,
                "asks": [["0.06844", "10760"]],
                "bids": [["0.06777", "562.4"], ["0.05", "221760.6"]],
            },
        }
        # Add trade event message be processed
        trade_response = {
            "m":
            "trades",
            "symbol":
            "BTC/USDT",
            "data": [
                {
                    "seqnum": 144115191800016553,
                    "p": "0.06762",
                    "q": "400",
                    "ts": 1573165890854,
                    "bm": False
                },
                {
                    "seqnum": 144115191800070421,
                    "p": "0.06797",
                    "q": "341",
                    "ts": 1573166037845,
                    "bm": True
                },
            ],
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(diff_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(trade_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(1, diffs_queue.qsize())
        self.assertEqual(1, trade_queue.qsize())

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diff_raises_cancel_exceptions(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop,
                                                         output=queue))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task.cancel()
            self.ev_loop.run_until_complete(self.listening_task)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_handle_ping_message(
            self, ws_connect_mock):
        # In AscendEx Ping message is sent as a aiohttp.WSMsgType.TEXT message
        mock_response = {"m": "ping", "hp": 3}
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(mock_response),
            message_type=aiohttp.WSMsgType.TEXT,
        )

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)
        sent_json = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)

        self.assertTrue(any(["pong" in str(payload) for payload in sent_json]))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_order_book_diff_logs_exception_parsing_message(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add incomplete diff event message be processed
        diff_response = {"m": "depth", "symbol": "incomplete response"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(diff_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop,
                                                         output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with WebSocket connection. Retrying after 30 seconds..."
            ))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diff_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add diff event message be processed
        diff_response = {
            "m": "depth",
            "symbol": self.ex_trading_pair,
            "data": {
                "ts": 1573069021376,
                "seqnum": 2097965,
                "asks": [["0.06844", "10760"]],
                "bids": [["0.06777", "562.4"], ["0.05", "221760.6"]],
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(diff_response))

        diffs_queue = self.data_source._message_queue[
            self.data_source.DIFF_TOPIC_ID]
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop,
                                                         output=output_queue))

        order_book_message = self.async_run_with_timeout(output_queue.get())

        self.assertTrue(diffs_queue.empty())
        self.assertEqual(1573069021376, order_book_message.update_id)
        self.assertEqual(1573069021376, order_book_message.timestamp)
        self.assertEqual(0.06777, order_book_message.bids[0].price)
        self.assertEqual(0.05, order_book_message.bids[1].price)
        self.assertEqual(0.06844, order_book_message.asks[0].price)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades_raises_cancel_exceptions(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=queue))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task.cancel()
            self.ev_loop.run_until_complete(self.listening_task)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_trades_logs_exception_parsing_message(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add incomplete diff event message be processed
        diff_response = {"m": "trades", "symbol": "incomplete response"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(diff_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with WebSocket connection. Retrying after 30 seconds..."
            ))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add trade event message be processed
        trade_response = {
            "m":
            "trades",
            "symbol":
            "BTC/USDT",
            "data": [
                {
                    "seqnum": 144115191800016553,
                    "p": "0.06762",
                    "q": "400",
                    "ts": 1573165890854,
                    "bm": False
                },
                {
                    "seqnum": 144115191800070421,
                    "p": "0.06797",
                    "q": "341",
                    "ts": 1573166037845,
                    "bm": True
                },
            ],
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(trade_response))

        trades_queue = self.data_source._message_queue[
            self.data_source.DIFF_TOPIC_ID]
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=output_queue))

        first_trade_message = self.async_run_with_timeout(output_queue.get())
        second_trade_message = self.async_run_with_timeout(output_queue.get())

        self.assertTrue(trades_queue.empty())
        self.assertEqual(1573165890854, first_trade_message.timestamp)
        self.assertEqual(1573166037845, second_trade_message.timestamp)

    @aioresponses()
    def test_listen_for_order_book_snapshot_event(self, api_mock):
        mock_response = {
            "code": 0,
            "data": {
                "m": "depth-snapshot",
                "symbol": self.ex_trading_pair,
                "data": {
                    "seqnum": 5068757,
                    "ts": 1573165838976,
                    "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]],
                    "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]],
                },
            },
        }

        self.data_source._trading_pairs = ["BTC-USDT"]

        # Add trade event message be processed
        url = re.escape(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}?symbol=")
        regex_url = re.compile(f"^{url}")
        api_mock.get(regex_url, body=json.dumps(mock_response))

        order_book_messages = asyncio.Queue()

        task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                ev_loop=self.ev_loop, output=order_book_messages))

        order_book_message = self.ev_loop.run_until_complete(
            order_book_messages.get())

        try:
            task.cancel()
            self.ev_loop.run_until_complete(task)
        except asyncio.CancelledError:
            # The exception will happen when cancelling the task
            pass

        self.assertTrue(order_book_messages.empty())
        self.assertEqual(1573165838976, order_book_message.update_id)
        self.assertEqual(1573165838976, order_book_message.timestamp)
        self.assertEqual(0.06703, order_book_message.bids[0].price)
        self.assertEqual(0.06848, order_book_message.asks[0].price)
class TestCoinbaseProAPIUserStreamDataSource(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        auth = CoinbaseProAuth(api_key="SomeAPIKey",
                               secret_key="shht",
                               passphrase="SomePassPhrase")
        self.mocking_assistant = NetworkMockingAssistant()
        web_assistants_factory = build_coinbase_pro_web_assistant_factory(auth)
        self.data_source = CoinbaseProAPIUserStreamDataSource(
            trading_pairs=[self.trading_pair],
            web_assistants_factory=web_assistants_factory)
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.log_records = []
        self.async_tasks: List[asyncio.Task] = []

    def tearDown(self) -> None:
        for task in self.async_tasks:
            task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def get_ws_open_message_mock(self) -> Dict:
        message = {
            "type": "open",
            "time": "2014-11-07T08:19:27.028459Z",
            "product_id": self.trading_pair,
            "sequence": 10,
            "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
            "price": "200.2",
            "remaining_size": "1.00",
            "side": "sell"
        }
        return message

    def get_ws_match_message_mock(self) -> Dict:
        message = {
            "type": "match",
            "trade_id": 10,
            "sequence": 50,
            "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
            "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1",
            "time": "2014-11-07T08:19:27.028459Z",
            "product_id": self.trading_pair,
            "size": "5.23512",
            "price": "400.23",
            "side": "sell"
        }
        return message

    def get_ws_change_message_mock(self) -> Dict:
        message = {
            "type": "change",
            "time": "2014-11-07T08:19:27.028459Z",
            "sequence": 80,
            "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8",
            "product_id": self.trading_pair,
            "new_size": "5.23512",
            "old_size": "12.234412",
            "price": "400.23",
            "side": "sell"
        }
        return message

    def get_ws_done_message_mock(self) -> Dict:
        message = {
            "type": "done",
            "time": "2014-11-07T08:19:27.028459Z",
            "product_id": self.trading_pair,
            "sequence": 10,
            "price": "200.2",
            "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
            "reason": "filled",
            "side": "sell",
            "remaining_size": "0"
        }
        return message

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_processes_open_message(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_ws_open_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        content = output_queue.get_nowait()

        self.assertEqual(resp, content)  # shallow comparison is ok

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_processes_match_message(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_ws_match_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        content = output_queue.get_nowait()

        self.assertEqual(resp, content)  # shallow comparison is ok

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_processes_change_message(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_ws_change_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        content = output_queue.get_nowait()

        self.assertEqual(resp, content)  # shallow comparison is ok

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_processes_done_message(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_ws_done_message_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertFalse(output_queue.empty())

        content = output_queue.get_nowait()

        self.assertEqual(resp, content)  # shallow comparison is ok

    @patch(
        "hummingbot.connector.exchange.coinbase_pro"
        ".coinbase_pro_api_user_stream_data_source.CoinbaseProAPIUserStreamDataSource._sleep"
    )
    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_raises_on_no_type(self, ws_connect_mock,
                                                      _):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = {}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(
                log_level="NETWORK",
                message="Unexpected error with WebSocket connection."))
        self.assertTrue(output_queue.empty())

    @patch(
        "hummingbot.connector.exchange.coinbase_pro"
        ".coinbase_pro_api_user_stream_data_source.CoinbaseProAPIUserStreamDataSource._sleep"
    )
    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_raises_on_error_message(
            self, ws_connect_mock, _):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = {"type": "error", "message": "some error"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(
                log_level="NETWORK",
                message="Unexpected error with WebSocket connection."))
        self.assertTrue(output_queue.empty())

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_ignores_irrelevant_messages(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps({"type": "received"}))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps({"type": "activate"}))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps({"type":
                                                      "subscriptions"}))
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output_queue))
        self.async_tasks.append(t)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())
Beispiel #8
0
class CoinflexPerpetualAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.domain = "coinflex_perpetual_testnet"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task = None
        self.async_tasks: List[asyncio.Task] = []

        self.data_source = CoinflexPerpetualAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            domain=self.domain,
        )

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()
        CoinflexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {
            self.domain: bidict({self.ex_trading_pair: self.trading_pair})
        }

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        for task in self.async_tasks:
            task.cancel()
        CoinflexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout))
        return ret

    def resume_test_callback(self, *_, **__):
        self.resume_test_event.set()
        return None

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(record.levelname == log_level and record.getMessage() == message for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _raise_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _successfully_subscribed_event(self):
        resp = {
            "result": None,
            "id": 1
        }
        return resp

    def _login_message(self):
        resp = {
            "tag": "1234567890",
            "event": "login",
            "success": True,
            "timestamp": "1234567890"
        }
        return resp

    def _trade_update_event(self):
        resp = {
            "table": "trade",
            "data": [{
                "timestamp": 123456789,
                "marketCode": self.ex_trading_pair,
                "tradeId": 12345,
                "side": "BUY",
                "price": "0.001",
                "quantity": "100",
            }]
        }
        return resp

    def _order_diff_event(self):
        resp = {
            "table": "depth",
            "data": [{
                "timestamp": 123456789,
                "instrumentId": self.ex_trading_pair,
                "seqNum": 157,
                "bids": [["0.0024", "10"]],
                "asks": [["0.0026", "100"]]
            }]
        }
        return resp

    def _snapshot_response(self,
                           update_id=1027024):
        resp = {
            "event": "depthL1000",
            "timestamp": update_id,
            "data": [{
                "bids": [
                    [
                        "4.00000000",
                        "431.00000000"
                    ]
                ],
                "asks": [
                    [
                        "4.00000200",
                        "12.00000000"
                    ]
                ],
                "marketCode": self.ex_trading_pair,
                "timestamp": update_id,
            }]
        }
        return resp

    def _ticker_response(self):
        mock_response = [{
            "last": "100.0",
            "open24h": "38719",
            "high24h": "38840",
            "low24h": "36377",
            "volume24h": "3622970.9407847790",
            "currencyVolume24h": "96.986",
            "openInterest": "0",
            "marketCode": "COINALPHA-HBOT",
            "timestamp": "1645546950025",
            "lastQty": "0.086",
            "markPrice": "37645",
            "lastMarkPrice": "37628",
        }]

        return mock_response

    def _funding_info_response(self):
        mock_response = [{
            "instrumentId": self.ex_trading_pair,
            "fundingRate": "0.00010000",
            "timestamp": "2022-04-11 21:00:03",
        }]

        return mock_response

    def _get_regex_url(self,
                       endpoint,
                       return_url=False,
                       endpoint_api_version=None,
                       public=True):
        prv_or_pub = web_utils.public_rest_url if public else web_utils.private_rest_url
        url = prv_or_pub(endpoint, domain=self.domain, endpoint_api_version=endpoint_api_version)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?").replace("{}", r".*"))
        if return_url:
            return url, regex_url
        return regex_url

    @aioresponses()
    def test_get_last_traded_prices(self, mock_api):

        regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_URL)
        mock_response = [{
            "last": "10.0",
            "open24h": "38719",
            "high24h": "38840",
            "low24h": "36377",
            "volume24h": "3622970.9407847790",
            "currencyVolume24h": "96.986",
            "openInterest": "0",
            "marketCode": "COINALPHA-HBOT",
            "timestamp": "1645546950025",
            "lastQty": "0.086",
            "markPrice": "37645",
            "lastMarkPrice": "37628",
        }]
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result: Dict[str, Any] = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices(trading_pairs=[self.trading_pair], domain=self.domain)
        )
        self.assertTrue(self.trading_pair in result)
        self.assertEqual(10.0, result[self.trading_pair])

    @aioresponses()
    @patch("hummingbot.connector.derivative.coinflex_perpetual.coinflex_perpetual_web_utils.retry_sleep_time")
    def test_init_trading_pair_symbols_failure(self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0

        CoinflexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        regex_url = self._get_regex_url(CONSTANTS.EXCHANGE_INFO_URL)

        mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"]))

        map = self.async_run_with_timeout(self.data_source.trading_pair_symbol_map(
            domain=self.domain))
        self.assertEqual(0, len(map))

    @aioresponses()
    def test_init_trading_pair_symbols_successful(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.EXCHANGE_INFO_URL)

        mock_response: Dict[str, Any] = {
            "event": "markets",
            "timestamp": "1639598493658",
            "data": [
                {
                    "marketId": "2001000000000",
                    "marketCode": self.ex_trading_pair,
                    "name": f"{self.base_asset}/{self.quote_asset} Perp",
                    "referencePair": f"{self.base_asset}/{self.quote_asset}",
                    "base": self.base_asset,
                    "counter": self.quote_asset,
                    "type": "FUTURE",
                    "tickSize": "1",
                    "qtyIncrement": "0.001",
                    "marginCurrency": self.quote_asset,
                    "contractValCurrency": self.base_asset,
                    "upperPriceBound": "39203",
                    "lowerPriceBound": "36187",
                    "marketPrice": "37695",
                    "markPrice": None,
                    "listingDate": 1593316800000,
                    "endDate": 0,
                    "marketPriceLastUpdated": 1645547473153,
                    "markPriceLastUpdated": 0
                },
                {
                    "marketId": "2001000000000",
                    "marketCode": self.ex_trading_pair,
                    "name": f"{self.base_asset}/{self.quote_asset}",
                    "referencePair": f"{self.base_asset}/{self.quote_asset}",
                    "base": self.base_asset,
                    "counter": self.quote_asset,
                    "type": "SPOT",
                    "tickSize": "1",
                    "qtyIncrement": "0.001",
                    "marginCurrency": self.quote_asset,
                    "contractValCurrency": self.base_asset,
                    "upperPriceBound": "39203",
                    "lowerPriceBound": "36187",
                    "marketPrice": "37695",
                    "markPrice": None,
                    "listingDate": 1593316800000,
                    "endDate": 0,
                    "marketPriceLastUpdated": 1645547473153,
                    "markPriceLastUpdated": 0
                },
            ]
        }
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        self.async_run_with_timeout(self.data_source.init_trading_pair_symbols(
            domain=self.domain))
        self.assertEqual(1, len(self.data_source._trading_pair_symbol_map[self.domain]))

    @aioresponses()
    def test_trading_pair_symbol_map_dictionary_not_initialized(self, mock_api):
        CoinflexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        regex_url = self._get_regex_url(CONSTANTS.EXCHANGE_INFO_URL)
        mock_response: Dict[str, Any] = {
            "event": "markets",
            "timestamp": "1639598493658",
            "data": [
                {
                    "marketId": "2001000000000",
                    "marketCode": self.ex_trading_pair,
                    "name": f"{self.base_asset}/{self.quote_asset} Perp",
                    "referencePair": f"{self.base_asset}/{self.quote_asset}",
                    "base": self.base_asset,
                    "counter": self.quote_asset,
                    "type": "FUTURE",
                    "tickSize": "1",
                    "qtyIncrement": "0.001",
                    "marginCurrency": self.quote_asset,
                    "contractValCurrency": self.base_asset,
                    "upperPriceBound": "39203",
                    "lowerPriceBound": "36187",
                    "marketPrice": "37695",
                    "markPrice": None,
                    "listingDate": 1593316800000,
                    "endDate": 0,
                    "marketPriceLastUpdated": 1645547473153,
                    "markPriceLastUpdated": 0
                }
            ]
        }
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        self.async_run_with_timeout(self.data_source.trading_pair_symbol_map(
            domain=self.domain))
        self.assertEqual(1, len(self.data_source._trading_pair_symbol_map[self.domain]))

    def test_trading_pair_symbol_map_dictionary_initialized(self):
        result = self.async_run_with_timeout(self.data_source.trading_pair_symbol_map(
            domain=self.domain))
        self.assertEqual(1, len(result))

    def test_convert_from_exchange_trading_pair_not_found(self):
        unknown_pair = "UNKNOWN-PAIR"
        with self.assertRaisesRegex(ValueError, f"There is no symbol mapping for exchange trading pair {unknown_pair}"):
            self.async_run_with_timeout(
                self.data_source.convert_from_exchange_trading_pair(unknown_pair, domain=self.domain))

    def test_convert_from_exchange_trading_pair_successful(self):
        result = self.async_run_with_timeout(
            self.data_source.convert_from_exchange_trading_pair(self.ex_trading_pair, domain=self.domain))
        self.assertEqual(result, self.trading_pair)

    def test_convert_to_exchange_trading_pair_not_found(self):
        unknown_pair = "UNKNOWN-PAIR"
        with self.assertRaisesRegex(ValueError, f"There is no symbol mapping for trading pair {unknown_pair}"):
            self.async_run_with_timeout(
                self.data_source.convert_to_exchange_trading_pair(unknown_pair, domain=self.domain))

    def test_convert_to_exchange_trading_pair_successful(self):
        result = self.async_run_with_timeout(
            self.data_source.convert_to_exchange_trading_pair(self.trading_pair, domain=self.domain))
        self.assertEqual(result, self.ex_trading_pair)

    @aioresponses()
    @patch("hummingbot.connector.derivative.coinflex_perpetual.coinflex_perpetual_web_utils.retry_sleep_time")
    def test_get_snapshot_exception_raised(self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0

        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL)
        for x in range(CONSTANTS.API_MAX_RETRIES):
            mock_api.get(regex_url, status=200, body=json.dumps(["ERROR"]))

        with self.assertRaises(IOError) as context:
            self.async_run_with_timeout(
                self.data_source.get_snapshot(
                    trading_pair=self.trading_pair)
            )

        self.assertEqual(f"Error fetching market snapshot for {self.trading_pair}. Response: ['ERROR'].",
                         str(context.exception))

    @aioresponses()
    def test_get_snapshot_successful(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL)
        mock_response = self._snapshot_response()
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))

        result: Dict[str, Any] = self.async_run_with_timeout(
            self.data_source.get_snapshot(
                trading_pair=self.trading_pair)
        )
        self.assertEqual(mock_response['data'][0], result)

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL)
        mock_response = self._snapshot_response(update_id=1027024)
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        result = self.async_run_with_timeout(self.data_source.get_new_order_book(trading_pair=self.trading_pair))
        self.assertIsInstance(result, OrderBook)
        self.assertEqual(1027024, result.snapshot_uid)

    @aioresponses()
    @patch("hummingbot.connector.derivative.coinflex_perpetual.coinflex_perpetual_web_utils.retry_sleep_time")
    def test_get_funding_info_from_exchange_error_response(self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0

        regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_URL)
        mock_response = self._ticker_response()
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))

        regex_url = self._get_regex_url(CONSTANTS.MARK_PRICE_URL)
        mock_api.get(regex_url, status=400)

        result = self.async_run_with_timeout(self.data_source._get_funding_info_from_exchange(self.trading_pair))
        self.assertIsNone(result)
        self._is_logged("ERROR", f"Unable to fetch FundingInfo for {self.trading_pair}. Error: None")

    @aioresponses()
    def test_get_funding_info_from_exchange_successful(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_URL)
        mock_ticker_resp = self._ticker_response()
        mock_api.get(regex_url, status=200, body=json.dumps(mock_ticker_resp))

        regex_url = self._get_regex_url(CONSTANTS.MARK_PRICE_URL)
        mock_response = self._funding_info_response()
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(self.data_source._get_funding_info_from_exchange(self.trading_pair))

        next_fund_ts = datetime.strptime(mock_response[0]["timestamp"], "%Y-%m-%d %H:%M:%S").timestamp() + CONSTANTS.ONE_HOUR

        self.assertIsInstance(result, FundingInfo)
        self.assertEqual(result.trading_pair, self.trading_pair)
        self.assertEqual(result.index_price, Decimal(mock_ticker_resp[0]["last"]))
        self.assertEqual(result.mark_price, Decimal(mock_ticker_resp[0]["markPrice"]))
        self.assertEqual(result.next_funding_utc_timestamp, next_fund_ts)
        self.assertEqual(result.rate, Decimal(mock_response[0]["fundingRate"]))

    @aioresponses()
    def test_get_funding_info(self, mock_api):
        self.assertNotIn(self.trading_pair, self.data_source._funding_info)

        regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_URL)
        mock_ticker_resp = self._ticker_response()
        mock_api.get(regex_url, status=200, body=json.dumps(mock_ticker_resp))

        regex_url = self._get_regex_url(CONSTANTS.MARK_PRICE_URL)
        mock_response = self._funding_info_response()
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(self.data_source.get_funding_info(trading_pair=self.trading_pair))

        next_fund_ts = datetime.strptime(mock_response[0]["timestamp"], "%Y-%m-%d %H:%M:%S").timestamp() + CONSTANTS.ONE_HOUR

        self.assertIsInstance(result, FundingInfo)
        self.assertEqual(result.trading_pair, self.trading_pair)
        self.assertEqual(result.index_price, Decimal(mock_ticker_resp[0]["last"]))
        self.assertEqual(result.mark_price, Decimal(mock_ticker_resp[0]["markPrice"]))
        self.assertEqual(result.next_funding_utc_timestamp, next_fund_ts)
        self.assertEqual(result.rate, Decimal(mock_response[0]["fundingRate"]))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep")
    def test_listen_for_subscriptions_cancelled_when_connecting(self, _, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)
        self.assertEqual(msg_queue.qsize(), 0)

    @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep")
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_logs_exception(self, mock_ws, *_):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.close.return_value = None
        incomplete_resp = 1
        self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, json.dumps(incomplete_resp))
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._order_diff_event())
        )

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.exceptions.TimeoutError:
            pass

        print(self.log_records)

        self.assertTrue(
            self._is_logged("ERROR", "Unexpected error with Websocket connection. Retrying after 30 seconds...")
        )

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_successful(self, mock_ws):
        msg_queue_diffs: asyncio.Queue = asyncio.Queue()
        msg_queue_trades: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.close.return_value = None

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._order_diff_event())
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._trade_update_event())
        )

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.listening_task_diffs = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue_diffs)
        )
        self.listening_task_trades = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue_trades)
        )

        result: OrderBookMessage = self.async_run_with_timeout(msg_queue_diffs.get())
        self.assertIsInstance(result, OrderBookMessage)
        self.assertEqual(OrderBookMessageType.DIFF, result.type)
        self.assertTrue(result.has_update_id)
        self.assertEqual(result.update_id, 123456789)
        self.assertEqual(self.trading_pair, result.content["trading_pair"])
        self.assertEqual(1, len(result.content["bids"]))
        self.assertEqual(1, len(result.content["asks"]))

        result: OrderBookMessage = self.async_run_with_timeout(msg_queue_trades.get())
        self.assertIsInstance(result, OrderBookMessage)
        self.assertEqual(OrderBookMessageType.TRADE, result.type)
        self.assertTrue(result.has_trade_id)
        self.assertEqual(result.trade_id, 12345)
        self.assertEqual(self.trading_pair, result.content["trading_pair"])

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(mock_ws.return_value)

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_error_raised(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL)
        mock_api.get(regex_url, exception=asyncio.CancelledError)

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
            )
            self.async_run_with_timeout(self.listening_task)

        self.assertEqual(0, msg_queue.qsize())

    @aioresponses()
    @patch("hummingbot.connector.derivative.coinflex_perpetual.coinflex_perpetual_web_utils.retry_sleep_time")
    def test_listen_for_order_book_snapshots_logs_exception_error_with_response(self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0

        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL)

        mock_response = {
            "m": 1,
            "i": 2,
        }
        mock_api.get(regex_url, body=json.dumps(mock_response), callback=self.resume_test_callback)

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
        )

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}.")
        )

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL.format(self.trading_pair, 1000))

        mock_api.get(regex_url, exception=asyncio.CancelledError)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_order_book_snapshots(self.ev_loop, asyncio.Queue())
            )

    @aioresponses()
    @patch("hummingbot.connector.derivative.coinflex_perpetual.coinflex_perpetual_api_order_book_data_source"
           ".CoinflexPerpetualAPIOrderBookDataSource._sleep")
    def test_listen_for_order_book_snapshots_log_outer_exception(self, mock_api, sleep_mock):
        msg_queue: asyncio.Queue = asyncio.Queue()
        sleep_mock.side_effect = lambda _: self._raise_exception_and_unlock_test_with_event(Exception("Dummy"))

        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL.format(self.trading_pair, 1000))

        mock_api.get(regex_url, body=json.dumps(self._snapshot_response()))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
        )
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("ERROR", "Unexpected error occurred fetching orderbook snapshots. Retrying in 5 seconds..."))

    @aioresponses()
    def test_listen_for_order_book_snapshots_successful(self, mock_api):
        regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_REST_URL.format(self.trading_pair, 1000))
        mock_api.get(regex_url, body=json.dumps(self._snapshot_response()))

        msg_queue: asyncio.Queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
        )

        result = self.async_run_with_timeout(msg_queue.get())

        self.assertIsInstance(result, OrderBookMessage)
        self.assertEqual(OrderBookMessageType.SNAPSHOT, result.type)
        self.assertTrue(result.has_update_id)
        self.assertEqual(result.update_id, 1027024)
        self.assertEqual(self.trading_pair, result.content["trading_pair"])
Beispiel #9
0
class TestKucoinAPIOrderBookDataSource(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ws_endpoint = "ws://someEndpoint"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.async_task = None

        self.mocking_assistant = NetworkMockingAssistant()

        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = KucoinExchange(client_config_map=client_config_map,
                                        kucoin_api_key="",
                                        kucoin_passphrase="",
                                        kucoin_secret_key="",
                                        trading_pairs=[],
                                        trading_required=False)

        self.ob_data_source = KucoinAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)

        self.ob_data_source.logger().setLevel(1)
        self.ob_data_source.logger().addHandler(self)

        self.connector._set_trading_pair_symbol_map(
            bidict({self.trading_pair: self.trading_pair}))

    def tearDown(self) -> None:
        self.async_task and self.async_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @staticmethod
    def get_snapshot_mock() -> Dict:
        snapshot = {
            "code": "200000",
            "data": {
                "time": 1630556205455,
                "sequence": "1630556205456",
                "bids": [["0.3003", "4146.5645"]],
                "asks": [["0.3004", "1553.6412"]]
            }
        }
        return snapshot

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_snapshot_mock()
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=self.ob_data_source.get_new_order_book(
                self.trading_pair))
        bid_entries = list(ret.bid_entries())
        ask_entries = list(ret.ask_entries())
        self.assertEqual(1, len(bid_entries))
        self.assertEqual(0.3003, bid_entries[0].price)
        self.assertEqual(4146.5645, bid_entries[0].amount)
        self.assertEqual(int(resp["data"]["sequence"]),
                         bid_entries[0].update_id)
        self.assertEqual(1, len(ask_entries))
        self.assertEqual(0.3004, ask_entries[0].price)
        self.assertEqual(1553.6412, ask_entries[0].amount)
        self.assertEqual(int(resp["data"]["sequence"]),
                         ask_entries[0].update_id)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id"
    )
    def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(
            self, mock_api, id_mock, ws_connect_mock):
        id_mock.side_effect = [1, 2]
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL)

        resp = {
            "code": "200000",
            "data": {
                "instanceServers": [{
                    "endpoint": "wss://test.url/endpoint",
                    "protocol": "websocket",
                    "encrypt": True,
                    "pingInterval": 50000,
                    "pingTimeout": 10000
                }],
                "token":
                "testToken"
            }
        }
        mock_api.post(url, body=json.dumps(resp))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {"type": "ack", "id": 1}
        result_subscribe_diffs = {"type": "ack", "id": 2}

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(2, len(sent_subscription_messages))
        expected_trade_subscription = {
            "id": 1,
            "type": "subscribe",
            "topic": f"/market/match:{self.trading_pair}",
            "privateChannel": False,
            "response": False
        }
        self.assertEqual(expected_trade_subscription,
                         sent_subscription_messages[0])
        expected_diff_subscription = {
            "id": 2,
            "type": "subscribe",
            "topic": f"/market/level2:{self.trading_pair}",
            "privateChannel": False,
            "response": False
        }
        self.assertEqual(expected_diff_subscription,
                         sent_subscription_messages[1])

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Subscribed to public order book and trade channels..."))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id"
    )
    @patch(
        "hummingbot.connector.exchange.kucoin.kucoin_api_order_book_data_source.KucoinAPIOrderBookDataSource._time"
    )
    def test_listen_for_subscriptions_sends_ping_message_before_ping_interval_finishes(
            self, mock_api, time_mock, id_mock, ws_connect_mock):

        id_mock.side_effect = [1, 2, 3, 4]
        time_mock.side_effect = [
            1000, 1100, 1101, 1102
        ]  # Simulate first ping interval is already due
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL)

        resp = {
            "code": "200000",
            "data": {
                "instanceServers": [{
                    "endpoint": "wss://test.url/endpoint",
                    "protocol": "websocket",
                    "encrypt": True,
                    "pingInterval": 20000,
                    "pingTimeout": 10000
                }],
                "token":
                "testToken"
            }
        }
        mock_api.post(url, body=json.dumps(resp))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {"type": "ack", "id": 1}
        result_subscribe_diffs = {"type": "ack", "id": 2}

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        expected_ping_message = {
            "id": 3,
            "type": "ping",
        }
        self.assertEqual(expected_ping_message, sent_messages[-1])

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_subscriptions_raises_cancel_exception(
            self, mock_api, _, ws_connect_mock):
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL)

        resp = {
            "code": "200000",
            "data": {
                "instanceServers": [{
                    "endpoint": "wss://test.url/endpoint",
                    "protocol": "websocket",
                    "encrypt": True,
                    "pingInterval": 50000,
                    "pingTimeout": 10000
                }],
                "token":
                "testToken"
            }
        }
        mock_api.post(url, body=json.dumps(resp))

        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_subscriptions_logs_exception_details(
            self, mock_api, sleep_mock, ws_connect_mock):
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.PUBLIC_WS_DATA_PATH_URL)

        resp = {
            "code": "200000",
            "data": {
                "instanceServers": [{
                    "endpoint": "wss://test.url/endpoint",
                    "protocol": "websocket",
                    "encrypt": True,
                    "pingInterval": 50000,
                    "pingTimeout": 10000
                }],
                "token":
                "testToken"
            }
        }
        mock_api.post(url, body=json.dumps(resp))

        sleep_mock.side_effect = asyncio.CancelledError
        ws_connect_mock.side_effect = Exception("TEST ERROR.")

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    def test_listen_for_trades_cancelled_when_listening(self):
        mock_queue = MagicMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.ob_data_source._message_queue[
            self.ob_data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_trades_logs_exception(self):
        incomplete_resp = {
            "type": "message",
            "topic": f"/market/match:{self.trading_pair}",
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.ob_data_source._message_queue[
            self.ob_data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public trade updates from exchange"
            ))

    def test_listen_for_trades_successful(self):
        mock_queue = AsyncMock()
        trade_event = {
            "type": "message",
            "topic": f"/market/match:{self.trading_pair}",
            "subject": "trade.l3match",
            "data": {
                "sequence": "1545896669145",
                "type": "match",
                "symbol": self.trading_pair,
                "side": "buy",
                "price": "0.08200000000000000000",
                "size": "0.01022222000000000000",
                "tradeId": "5c24c5da03aa673885cd67aa",
                "takerOrderId": "5c24c5d903aa6772d55b371e",
                "makerOrderId": "5c2187d003aa677bd09d5c93",
                "time": "1545913818099033203"
            }
        }
        mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()]
        self.ob_data_source._message_queue[
            self.ob_data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(trade_event["data"]["tradeId"], msg.trade_id)

    def test_listen_for_order_book_diffs_cancelled(self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.ob_data_source._message_queue[
            self.ob_data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_order_book_diffs_logs_exception(self):
        incomplete_resp = {
            "type": "message",
            "topic": f"/market/level2:{self.trading_pair}",
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.ob_data_source._message_queue[
            self.ob_data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public order book updates from exchange"
            ))

    def test_listen_for_order_book_diffs_successful(self):
        mock_queue = AsyncMock()
        diff_event = {
            "type": "message",
            "topic": "/market/level2:BTC-USDT",
            "subject": "trade.l2update",
            "data": {
                "sequenceStart": 1545896669105,
                "sequenceEnd": 1545896669106,
                "symbol": f"{self.trading_pair}",
                "changes": {
                    "asks": [["6", "1", "1545896669105"]],
                    "bids": [["4", "1", "1545896669106"]]
                }
            }
        }
        mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()]
        self.ob_data_source._message_queue[
            self.ob_data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(diff_event["data"]["sequenceEnd"], msg.update_id)

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(
            self, mock_api):
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=asyncio.CancelledError)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.ob_data_source.listen_for_order_book_snapshots(
                    self.ev_loop, asyncio.Queue()))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.kucoin.kucoin_api_order_book_data_source"
        ".KucoinAPIOrderBookDataSource._sleep")
    def test_listen_for_order_book_snapshots_log_exception(
            self, mock_api, sleep_mock):
        msg_queue: asyncio.Queue = asyncio.Queue()
        sleep_mock.side_effect = asyncio.CancelledError

        url = web_utils.public_rest_url(
            path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=Exception)

        try:
            self.async_run_with_timeout(
                self.ob_data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                f"Unexpected error fetching order book snapshot for {self.trading_pair}."
            ))

    @aioresponses()
    def test_listen_for_order_book_snapshots_successful(
        self,
        mock_api,
    ):
        msg_queue: asyncio.Queue = asyncio.Queue()
        url = web_utils.public_rest_url(
            path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        snapshot_data = {
            "code": "200000",
            "data": {
                "sequence": "3262786978",
                "time": 1550653727731,
                "bids": [["6500.12", "0.45054140"], ["6500.11", "0.45054140"]],
                "asks": [["6500.16", "0.57753524"], ["6500.15", "0.57753524"]]
            }
        }

        mock_api.get(regex_url, body=json.dumps(snapshot_data))

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(int(snapshot_data["data"]["sequence"]), msg.update_id)
class TestBybitAPIUserStreamDataSource(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = cls.base_asset + cls.quote_asset
        cls.domain = CONSTANTS.DEFAULT_DOMAIN
        cls.api_key = "someKey"
        cls.api_passphrase = "somePassPhrase"
        cls.api_secret_key = "someSecretKey"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.mock_time_provider = MagicMock()
        self.mock_time_provider.time.return_value = 1000
        # self.time_synchronizer = TimeSynchronizer()
        # self.time_synchronizer.add_time_offset_ms_sample(0)
        self.auth = BybitAuth(self.api_key,
                              self.api_secret_key,
                              time_provider=self.mock_time_provider)

        self.api_factory = web_utils.build_api_factory(
            throttler=self.throttler,
            time_synchronizer=self.mock_time_provider,
            auth=self.auth)

        self.data_source = BybitAPIUserStreamDataSource(
            auth=self.auth,
            domain=self.domain,
            api_factory=self.api_factory,
            throttler=self.throttler,
            time_synchronizer=self.mock_time_provider)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def test_last_recv_time(self):
        # Initial last_recv_time
        self.assertEqual(0, self.data_source.last_recv_time)

        ws_assistant = self.async_run_with_timeout(
            self.data_source._get_ws_assistant())
        ws_assistant._connection._last_recv_time = 1000
        self.assertEqual(1000, self.data_source.last_recv_time)

    @patch("hummingbot.connector.exchange.bybit.bybit_auth.BybitAuth._time")
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_auth(self, ws_connect_mock,
                                         auth_time_mock):
        auth_time_mock.side_effect = [1000]
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_auth = {'auth': 'success', 'userId': 24068148}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_auth))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(1, len(sent_subscription_messages))

        expires = int((1000 + 10) * 1000)
        _val = f'GET/realtime{expires}'
        signature = hmac.new(self.api_secret_key.encode("utf8"),
                             _val.encode("utf8"), hashlib.sha256).hexdigest()
        auth_subscription = {
            "op": "auth",
            "args": [self.api_key, expires, signature]
        }

        self.assertEqual(auth_subscription, sent_subscription_messages[0])

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_pong_payload(self, mock_ws):

        mock_pong = {"pong": "1545910590801"}
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(mock_pong))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_ticket_info(self, mock_ws):

        ticket_info = [{
            "e": "ticketInfo",
            "E": "1621912542359",
            "s": "BTCUSDT",
            "q": "0.001639",
            "t": "1621912542314",
            "p": "61000.0",
            "T": "899062000267837441",
            "o": "899048013515737344",
            "c": "1621910874883",
            "O": "899062000118679808",
            "a": "10043",
            "A": "10024",
            "m": True
        }]
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(ticket_info))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @patch("hummingbot.connector.exchange.bybit.bybit_auth.BybitAuth._time")
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_auth_failed_throws_exception(
            self, ws_connect_mock, auth_time_mock):
        auth_time_mock.side_effect = [100]
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_auth = {'auth': 'fail', 'userId': 24068148}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_auth))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(1, len(sent_subscription_messages))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listen_for_user_stream_iter_message_throws_exception(
            self, sleep_mock, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = Exception("TEST ERROR")
        sleep_mock.side_effect = asyncio.CancelledError  # to finish the task execution

        try:
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.bybit.bybit_api_user_stream_data_source.BybitAPIUserStreamDataSource"
        "._time")
    def test_listen_for_user_stream_sends_ping_message_before_ping_interval_finishes(
            self, time_mock, ws_connect_mock):

        time_mock.side_effect = [
            1000, 1100, 1101, 1102
        ]  # Simulate first ping interval is already due

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_auth = {'auth': 'success', 'userId': 24068148}

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_auth))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        expected_ping_message = {
            "ping": 1101 * 1e3,
        }
        self.assertEqual(expected_ping_message, sent_messages[-1])
class CryptoComWebSocketUnitTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.trading_pairs = ["COINALPHA-HBOT"]

        cls.api_key = "someKey"
        cls.secret_key = "someSecretKey"
        cls.auth = CryptoComAuth(api_key=cls.api_key,
                                 secret_key=cls.secret_key)

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []

        self.websocket = CryptoComWebsocket(auth=self.auth)
        self.websocket.logger().setLevel(1)
        self.websocket.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.async_task: Optional[asyncio.Task] = None

        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        self.ev_loop.run_until_complete(self.websocket.disconnect())
        self.async_task and self.async_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def resume_test_callback(self):
        self.resume_test_event.set()

    async def _iter_message(self):
        async for _ in self.websocket.iter_messages():
            self.resume_test_callback()
            self.async_task.cancel()

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_connect_raises_exception(self, _, ws_connect_mock):
        ws_connect_mock.side_effect = Exception("TEST ERROR")

        self.websocket = CryptoComWebsocket()

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(self.websocket.connect())

        self.assertTrue(
            self._is_logged("ERROR", "Websocket error: 'TEST ERROR'"))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_connect_authenticate_is_called(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.websocket.connect())

        sent_payloads = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)
        self.assertEqual(1, len(sent_payloads))
        self.assertEqual(CryptoComWebsocket.AUTH_REQUEST,
                         sent_payloads[0]["method"])

    def test_disconnect(self):
        ws = AsyncMock()
        self.websocket._websocket = ws

        self.async_run_with_timeout(self.websocket.disconnect())

        self.assertEqual(1, ws.close.await_count)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_iter_messages_handle_ping(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.websocket.connect())

        mock_ping = {
            "id": 1587523073344,
            "method": "public/heartbeat",
            "code": 0
        }
        expected_pong_payload = {
            'id': 1587523073344,
            'method': 'public/respond-heartbeat'
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(mock_ping))

        self.async_task = self.ev_loop.create_task(self._iter_message())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_payloads = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)
        self.assertEqual(2, len(sent_payloads))
        self.assertEqual(expected_pong_payload, sent_payloads[-1])

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_subscribe_to_order_book_streams_raises_cancelled_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.async_run_with_timeout(self.websocket.connect())

        ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.websocket.subscribe_to_order_book_streams(
                    self.trading_pairs))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_subscribe_to_order_book_streams_logs_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.async_run_with_timeout(self.websocket.connect())

        ws_connect_mock.return_value.send_json.side_effect = Exception(
            "TEST ERROR")

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(
                self.websocket.subscribe_to_order_book_streams(
                    self.trading_pairs))

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred subscribing to order book trading and delta streams..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_subscribe_to_user_streams_raises_cancelled_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.async_run_with_timeout(self.websocket.connect())

        ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.websocket.subscribe_to_user_streams())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_subscribe_to_user_streams_logs_exception(self, _,
                                                      ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.async_run_with_timeout(self.websocket.connect())

        ws_connect_mock.return_value.send_json.side_effect = Exception(
            "TEST ERROR")

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(
                self.websocket.subscribe_to_user_streams())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred subscribing to user streams..."))
class BinanceUserStreamDataSourceUnitTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = cls.base_asset + cls.quote_asset
        cls.domain = "com"

        cls.listen_key = "TEST_LISTEN_KEY"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.mock_time_provider = MagicMock()
        self.mock_time_provider.time.return_value = 1000
        self.auth = BinanceAuth(api_key="TEST_API_KEY", secret_key="TEST_SECRET", time_provider=self.mock_time_provider)
        self.time_synchronizer = TimeSynchronizer()
        self.time_synchronizer.add_time_offset_ms_sample(0)

        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = BinanceExchange(
            client_config_map=client_config_map,
            binance_api_key="",
            binance_api_secret="",
            trading_pairs=[],
            trading_required=False,
            domain=self.domain)
        self.connector._web_assistants_factory._auth = self.auth

        self.data_source = BinanceAPIUserStreamDataSource(
            auth=self.auth,
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory,
            domain=self.domain
        )

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.resume_test_event = asyncio.Event()

        self.connector._set_trading_pair_symbol_map(bidict({self.ex_trading_pair: self.trading_pair}))

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(record.levelname == log_level and record.getMessage() == message
                   for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _create_return_value_and_unlock_test_with_event(self, value):
        self.resume_test_event.set()
        return value

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout))
        return ret

    def _error_response(self) -> Dict[str, Any]:
        resp = {
            "code": "ERROR CODE",
            "msg": "ERROR MESSAGE"
        }

        return resp

    def _user_update_event(self):
        # Balance Update
        resp = {
            "e": "balanceUpdate",
            "E": 1573200697110,
            "a": "BTC",
            "d": "100.00000000",
            "T": 1573200697068
        }
        return json.dumps(resp)

    def _successfully_subscribed_event(self):
        resp = {
            "result": None,
            "id": 1
        }
        return resp

    @aioresponses()
    def test_get_listen_key_log_exception(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_api.post(regex_url, status=400, body=json.dumps(self._error_response()))

        with self.assertRaises(IOError):
            self.async_run_with_timeout(self.data_source._get_listen_key())

    @aioresponses()
    def test_get_listen_key_successful(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = {
            "listenKey": self.listen_key
        }
        mock_api.post(regex_url, body=json.dumps(mock_response))

        result: str = self.async_run_with_timeout(self.data_source._get_listen_key())

        self.assertEqual(self.listen_key, result)

    @aioresponses()
    def test_ping_listen_key_log_exception(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_api.put(regex_url, status=400, body=json.dumps(self._error_response()))

        self.data_source._current_listen_key = self.listen_key
        result: bool = self.async_run_with_timeout(self.data_source._ping_listen_key())

        self.assertTrue(self._is_logged("WARNING", f"Failed to refresh the listen key {self.listen_key}: "
                                                   f"{self._error_response()}"))
        self.assertFalse(result)

    @aioresponses()
    def test_ping_listen_key_successful(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))
        mock_api.put(regex_url, body=json.dumps({}))

        self.data_source._current_listen_key = self.listen_key
        result: bool = self.async_run_with_timeout(self.data_source._ping_listen_key())
        self.assertTrue(result)

    @patch("hummingbot.connector.exchange.binance.binance_api_user_stream_data_source.BinanceAPIUserStreamDataSource"
           "._ping_listen_key",
           new_callable=AsyncMock)
    def test_manage_listen_key_task_loop_keep_alive_failed(self, mock_ping_listen_key):
        mock_ping_listen_key.side_effect = (lambda *args, **kwargs:
                                            self._create_return_value_and_unlock_test_with_event(False))

        self.data_source._current_listen_key = self.listen_key

        # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached
        self.data_source._last_listen_key_ping_ts = 0

        self.listening_task = self.ev_loop.create_task(self.data_source._manage_listen_key_task_loop())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(self._is_logged("ERROR", "Error occurred renewing listen key ..."))
        self.assertIsNone(self.data_source._current_listen_key)
        self.assertFalse(self.data_source._listen_key_initialized_event.is_set())

    @patch("hummingbot.connector.exchange.binance.binance_api_user_stream_data_source.BinanceAPIUserStreamDataSource."
           "_ping_listen_key",
           new_callable=AsyncMock)
    def test_manage_listen_key_task_loop_keep_alive_successful(self, mock_ping_listen_key):
        mock_ping_listen_key.side_effect = (lambda *args, **kwargs:
                                            self._create_return_value_and_unlock_test_with_event(True))

        # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached
        self.data_source._current_listen_key = self.listen_key
        self.data_source._listen_key_initialized_event.set()
        self.data_source._last_listen_key_ping_ts = 0

        self.listening_task = self.ev_loop.create_task(self.data_source._manage_listen_key_task_loop())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(self._is_logged("INFO", f"Refreshed listen key {self.listen_key}."))
        self.assertGreater(self.data_source._last_listen_key_ping_ts, 0)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_get_listen_key_successful_with_user_update_event(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = {
            "listenKey": self.listen_key
        }
        mock_api.post(regex_url, body=json.dumps(mock_response))

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, self._user_update_event())

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue)
        )

        msg = self.async_run_with_timeout(msg_queue.get())
        self.assertEqual(json.loads(self._user_update_event()), msg)
        mock_ws.return_value.ping.assert_called()

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_empty_payload(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = {
            "listenKey": self.listen_key
        }
        mock_api.post(regex_url, body=json.dumps(mock_response))

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, "")

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue)
        )

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_connection_failed(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = {
            "listenKey": self.listen_key
        }
        mock_api.post(regex_url, body=json.dumps(mock_response))

        mock_ws.side_effect = lambda *arg, **kwars: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR."))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue)
        )

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("ERROR",
                            "Unexpected error while listening to user stream. Retrying after 5 seconds..."))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_iter_message_throws_exception(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.BINANCE_USER_STREAM_PATH_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = {
            "listenKey": self.listen_key
        }
        mock_api.post(regex_url, body=json.dumps(mock_response))

        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = (lambda *args, **kwargs:
                                                    self._create_exception_and_unlock_test_with_event(
                                                        Exception("TEST ERROR")))
        mock_ws.close.return_value = None

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue)
        )

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."))
class TestGateIoAPIUserStreamDataSource(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}"
        cls.api_key = "someKey"
        cls.api_secret_key = "someSecretKey"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.mock_time_provider = MagicMock()
        self.mock_time_provider.time.return_value = 1000
        self.auth = GateIoAuth(api_key=self.api_key,
                               secret_key=self.api_secret_key,
                               time_provider=self.mock_time_provider)
        self.time_synchronizer = TimeSynchronizer()
        self.time_synchronizer.add_time_offset_ms_sample(0)

        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = GateIoExchange(client_config_map=client_config_map,
                                        gate_io_api_key="",
                                        gate_io_secret_key="",
                                        trading_pairs=[],
                                        trading_required=False)
        self.connector._web_assistants_factory._auth = self.auth

        self.data_source = GateIoAPIUserStreamDataSource(
            self.auth,
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.connector._set_trading_pair_symbol_map(
            bidict({self.ex_trading_pair: self.trading_pair}))

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_api_user_stream_data_source.GateIoAPIUserStreamDataSource"
        "._time")
    def test_listen_for_user_stream_subscribes_to_orders_and_balances_events(
            self, time_mock, ws_connect_mock):
        time_mock.return_value = 1000
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_orders = {
            "time": 1611541000,
            "channel": CONSTANTS.USER_ORDERS_ENDPOINT_NAME,
            "event": "subscribe",
            "error": None,
            "result": {
                "status": "success"
            }
        }
        result_subscribe_trades = {
            "time": 1611541000,
            "channel": CONSTANTS.USER_TRADES_ENDPOINT_NAME,
            "event": "subscribe",
            "error": None,
            "result": {
                "status": "success"
            }
        }
        result_subscribe_balance = {
            "time": 1611541000,
            "channel": CONSTANTS.USER_BALANCE_ENDPOINT_NAME,
            "event": "subscribe",
            "error": None,
            "result": {
                "status": "success"
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_orders))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_balance))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(3, len(sent_subscription_messages))
        expected_orders_subscription = {
            "time": int(self.mock_time_provider.time()),
            "channel": CONSTANTS.USER_ORDERS_ENDPOINT_NAME,
            "event": "subscribe",
            "payload": [self.ex_trading_pair],
            "auth": {
                "KEY":
                self.api_key,
                "SIGN":
                '005d2e6996fa7783459453d36ff871d8d5cfe225a098f37ac234543811c79e3c'  # noqa: mock
                'db8f41684f3ad9491f65c15ed880ce7baee81f402eb1df56b1bba188c0e7838c',  # noqa: mock
                "method":
                "api_key"
            },
        }
        self.assertEqual(expected_orders_subscription,
                         sent_subscription_messages[0])
        expected_trades_subscription = {
            "time": int(self.mock_time_provider.time()),
            "channel": CONSTANTS.USER_TRADES_ENDPOINT_NAME,
            "event": "subscribe",
            "payload": [self.ex_trading_pair],
            "auth": {
                "KEY":
                self.api_key,
                "SIGN":
                '0f34bf79558905d2b5bc7790febf1099d38ff1aa39525a077db32bcbf9135268'  # noqa: mock
                'caf23cdf2d62315841500962f788f7c5f4c3f4b8a057b2184366687b1f74af69',  # noqa: mock
                "method":
                "api_key"
            }
        }
        self.assertEqual(expected_trades_subscription,
                         sent_subscription_messages[1])
        expected_balances_subscription = {
            "time": int(self.mock_time_provider.time()),
            "channel": CONSTANTS.USER_BALANCE_ENDPOINT_NAME,
            "event": "subscribe",
            "auth": {
                "KEY":
                self.api_key,
                "SIGN":
                '90f5e732fc586d09c4a1b7de13f65b668c7ce90678b30da87aa137364bac0b97'  # noqa: mock
                '16b34219b689fb754e821872933a0e12b1d415867b9fbb8ec441bc86e77fb79c',  # noqa: mock
                "method":
                "api_key"
            }
        }
        self.assertEqual(expected_balances_subscription,
                         sent_subscription_messages[2])

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Subscribed to private order changes and balance updates channels..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_api_user_stream_data_source.GateIoAPIUserStreamDataSource"
        "._time")
    def test_listen_for_user_stream_skips_subscribe_unsubscribe_messages(
            self, time_mock, ws_connect_mock):
        time_mock.return_value = 1000
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_orders = {
            "time": 1611541000,
            "channel": CONSTANTS.USER_ORDERS_ENDPOINT_NAME,
            "event": "subscribe",
            "error": None,
            "result": {
                "status": "success"
            }
        }
        result_subscribe_trades = {
            "time": 1611541000,
            "channel": CONSTANTS.USER_TRADES_ENDPOINT_NAME,
            "event": "subscribe",
            "error": None,
            "result": {
                "status": "success"
            }
        }
        result_subscribe_balance = {
            "time": 1611541000,
            "channel": CONSTANTS.USER_BALANCE_ENDPOINT_NAME,
            "event": "subscribe",
            "error": None,
            "result": {
                "status": "success"
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_orders))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_balance))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_pong_payload(self, mock_ws):
        mock_pong = {
            "time": 1545404023,
            "channel": CONSTANTS.PONG_CHANNEL_NAME,
            "event": "",
            "error": None,
            "result": None
        }

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(mock_pong))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listen_for_user_stream_connection_failed(self, sleep_mock,
                                                      mock_ws):
        mock_ws.side_effect = Exception("TEST ERROR.")
        sleep_mock.side_effect = asyncio.CancelledError  # to finish the task execution

        msg_queue = asyncio.Queue()
        try:
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listen_for_user_stream_iter_message_throws_exception(
            self, sleep_mock, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = Exception("TEST ERROR")
        sleep_mock.side_effect = asyncio.CancelledError  # to finish the task execution

        try:
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))
class HuobiAPIUserStreamDataSourceTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.auth = HuobiAuth("somKey", "someSecretKey")
        cls.ev_loop = asyncio.get_event_loop()
        for task in asyncio.all_tasks(loop=cls.ev_loop):
            task.cancel()

    @classmethod
    def tearDownClass(cls) -> None:
        for task in asyncio.all_tasks(loop=cls.ev_loop):
            task.cancel()

    def setUp(self) -> None:
        super().setUp()

        self.log_records = []
        self.async_tasks: List[asyncio.Task] = []

        self.api_factory = build_api_factory()
        self.data_source = HuobiAPIUserStreamDataSource(
            huobi_auth=self.auth, api_factory=self.api_factory)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        for task in self.async_tasks:
            task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def test_get_ws_assistant(self):

        data_source = HuobiAPIUserStreamDataSource(self.auth)

        self.assertIsNone(data_source._ws_assistant)

        initial_ws_assistant = self.async_run_with_timeout(
            data_source._get_ws_assistant())
        self.assertIsNotNone(data_source._ws_assistant)
        self.assertIsInstance(initial_ws_assistant, WSAssistant)

        subsequent_ws_assistant = self.async_run_with_timeout(
            data_source._get_ws_assistant())
        self.assertEqual(initial_ws_assistant, subsequent_ws_assistant)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_authenticate_client_raises_cancelled(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source._authenticate_client())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_authenticate_client_logs_exception(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)

        ws_connect_mock.return_value.send_json.side_effect = Exception(
            "TEST ERROR")

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(
                self.data_source._authenticate_client())

        self._is_logged(
            "ERROR",
            "Error occurred authenticating websocket connection... Error: TEST ERROR"
        )

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_authenticate_client_failed(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)

        error_auth_response = {
            "action": "req",
            "code": 0,
            "TEST_ERROR": "ERROR WITH AUTHENTICATION"
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(error_auth_response))

        with self.assertRaisesRegex(ValueError,
                                    "User Stream Authentication Fail!"):
            self.async_run_with_timeout(
                self.data_source._authenticate_client())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_authenticate_client_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)

        successful_auth_response = {
            "action": "req",
            "code": 200,
            "ch": "auth",
            "data": {}
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_auth_response))

        result = self.async_run_with_timeout(
            self.data_source._authenticate_client())

        self.assertIsNone(result)
        self._is_logged("INFO", "Successfully authenticated to user...")

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_subscribe_channels_raises_cancelled(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source._subscribe_channels(
                    websocket_assistant=self.data_source._ws_assistant))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_subscribe_channels_subscribe_topic_fail(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)

        error_sub_response = {
            "action": "sub",
            "code": 0,
            "TEST_ERROR": "ERROR SUBSCRIBING TO USER STREAM TOPIC"
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(error_sub_response))

        with self.assertRaisesRegex(ValueError,
                                    "Error subscribing to topic: "):
            self.async_run_with_timeout(
                self.data_source._subscribe_channels(
                    websocket_assistant=self.data_source._ws_assistant))

        self._is_logged(
            "ERROR",
            f"Cannot subscribe to user stream topic: {CONSTANTS.HUOBI_ORDER_UPDATE_TOPIC}"
        )

        self._is_logged(
            "ERROR",
            "Unexpected error occurred subscribing to private user streams...")

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_subscribe_channels_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        # Initialise WSAssistant and assume connected to websocket server
        self.async_run_with_timeout(self.data_source._get_ws_assistant())
        self.async_run_with_timeout(
            self.data_source._ws_assistant.connect(CONSTANTS.WS_PRIVATE_URL))

        self.assertIsNotNone(self.data_source._ws_assistant)
        successful_sub_trades_response = {
            "action": "sub",
            "code": 200,
            "ch": "trade.clearing#*",
            "data": {}
        }
        successful_sub_order_response = {
            "action": "sub",
            "code": 200,
            "ch": "orders#*",
            "data": {}
        }
        successful_sub_account_response = {
            "action": "sub",
            "code": 200,
            "ch": "accounts.update#2",
            "data": {}
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_trades_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_order_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_account_response))

        result = self.async_run_with_timeout(
            self.data_source._subscribe_channels(
                websocket_assistant=self.data_source._ws_assistant))

        self.assertIsNone(result)

        subscription_requests_sent = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)

        expected_orders_channel_subscription = {
            "action": "sub",
            "ch": "orders#*"
        }
        self.assertIn(expected_orders_channel_subscription,
                      subscription_requests_sent)
        expected_accounts_channel_subscription = {
            "action": "sub",
            "ch": "accounts.update#2"
        }
        self.assertIn(expected_accounts_channel_subscription,
                      subscription_requests_sent)
        expected_trades_channel_subscription = {
            "action": "sub",
            "ch": "trade.clearing#*"
        }
        self.assertIn(expected_trades_channel_subscription,
                      subscription_requests_sent)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep"
    )
    def test_listen_for_user_stream_raises_cancelled_error(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.side_effect = asyncio.CancelledError

        msg_queue = asyncio.Queue()
        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(msg_queue))

        self.assertEqual(0, msg_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep"
    )
    def test_listen_for_user_stream_logs_exception(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        successful_auth_response = {
            "action": "req",
            "code": 200,
            "ch": "auth",
            "data": {}
        }
        successful_sub_trades_response = {
            "action": "sub",
            "code": 200,
            "ch": "trade.clearing#*",
            "data": {}
        }
        successful_sub_order_response = {
            "action": "sub",
            "code": 200,
            "ch": "orders#*",
            "data": {}
        }
        successful_sub_account_response = {
            "action": "sub",
            "code": 200,
            "ch": "accounts.update#2",
            "data": {}
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_auth_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_trades_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_order_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_account_response))

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.CLOSE)
        msg_queue = asyncio.Queue()

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(msg_queue)))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(0, msg_queue.qsize())
        self._is_logged(
            "ERROR",
            "Unexpected error with Huobi WebSocket connection. Retrying after 30 seconds..."
        )

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep"
    )
    def test_listen_for_user_stream_handle_ping(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        successful_auth_response = {
            "action": "req",
            "code": 200,
            "ch": "auth",
            "data": {}
        }
        successful_sub_trades_response = {
            "action": "sub",
            "code": 200,
            "ch": "trade.clearing#*",
            "data": {}
        }
        successful_sub_order_response = {
            "action": "sub",
            "code": 200,
            "ch": "orders#*",
            "data": {}
        }
        successful_sub_account_response = {
            "action": "sub",
            "code": 200,
            "ch": "accounts.update#2",
            "data": {}
        }

        ping_response = {"action": "ping", "data": {"ts": 1637553193021}}

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_auth_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_trades_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_order_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_account_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message=json.dumps(ping_response))

        msg_queue = asyncio.Queue()

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(msg_queue)))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(0, msg_queue.qsize())
        sent_json = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)

        self.assertTrue(any(["pong" in str(payload) for payload in sent_json]))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.huobi.huobi_api_user_stream_data_source.HuobiAPIUserStreamDataSource._sleep"
    )
    def test_listen_for_user_stream_enqueues_updates(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        successful_auth_response = {
            "action": "req",
            "code": 200,
            "ch": "auth",
            "data": {}
        }
        successful_sub_trades_response = {
            "action": "sub",
            "code": 200,
            "ch": "trade.clearing#*",
            "data": {}
        }
        successful_sub_order_response = {
            "action": "sub",
            "code": 200,
            "ch": "orders#*",
            "data": {}
        }
        successful_sub_account_response = {
            "action": "sub",
            "code": 200,
            "ch": "accounts.update#2",
            "data": {}
        }

        ping_response = {"action": "ping", "data": {"ts": 1637553193021}}

        order_update_response = {
            "action": "push",
            "ch": "orders#",
            "data": {
                "execAmt": "0",
                "lastActTime": 1637553210074,
                "orderSource": "spot-api",
                "remainAmt": "0.005",
                "orderPrice": "4122.62",
                "orderSize": "0.005",
                "symbol": "ethusdt",
                "orderId": 414497810678464,
                "orderStatus": "canceled",
                "eventType": "cancellation",
                "clientOrderId": "AAc484720a-buy-ETH-USDT-1637553180003697",
                "type": "buy-limit-maker",
            },
        }

        account_update_response = {
            "action": "push",
            "ch": "accounts.update#2",
            "data": {
                "currency": "usdt",
                "accountId": 15026496,
                "balance": "100",
                "available": "100",
                "changeType": "order.cancel",
                "accountType": "trade",
                "seqNum": 117,
                "changeTime": 1637553210076,
            },
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_auth_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_trades_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_order_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(successful_sub_account_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message=json.dumps(ping_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(order_update_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=json.dumps(account_update_response))

        msg_queue = asyncio.Queue()

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(msg_queue)))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(2, msg_queue.qsize())
class TestKucoinAPIUserStreamDataSource(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.api_key = "someKey"
        cls.api_passphrase = "somePassPhrase"
        cls.api_secret_key = "someSecretKey"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.mock_time_provider = MagicMock()
        self.mock_time_provider.time.return_value = 1000
        self.auth = KucoinAuth(
            self.api_key,
            self.api_passphrase,
            self.api_secret_key,
            time_provider=self.mock_time_provider)

        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = KucoinExchange(
            client_config_map=client_config_map,
            kucoin_api_key="",
            kucoin_passphrase="",
            kucoin_secret_key="",
            trading_pairs=[],
            trading_required=False)

        self.data_source = KucoinAPIUserStreamDataSource(
            auth=self.auth,
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(record.levelname == log_level and record.getMessage() == message
                   for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout))
        return ret

    @staticmethod
    def get_listen_key_mock():
        listen_key = {
            "code": "200000",
            "data": {
                "token": "someToken",
                "instanceServers": [
                    {
                        "endpoint": "wss://someEndpoint",
                        "encrypt": True,
                        "protocol": "websocket",
                        "pingInterval": 18000,
                        "pingTimeout": 10000,
                    }
                ]
            }
        }
        return listen_key

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch("hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id")
    def test_listen_for_user_stream_subscribes_to_orders_and_balances_events(self, mock_api, id_mock, ws_connect_mock):
        id_mock.side_effect = [1, 2]
        url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL)

        resp = {
            "code": "200000",
            "data": {
                "instanceServers": [
                    {
                        "endpoint": "wss://test.url/endpoint",
                        "protocol": "websocket",
                        "encrypt": True,
                        "pingInterval": 50000,
                        "pingTimeout": 10000
                    }
                ],
                "token": "testToken"
            }
        }
        mock_api.post(url, body=json.dumps(resp))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()

        result_subscribe_trades = {
            "type": "ack",
            "id": 1
        }
        result_subscribe_diffs = {
            "type": "ack",
            "id": 2
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(2, len(sent_subscription_messages))
        expected_orders_subscription = {
            "id": 1,
            "type": "subscribe",
            "topic": "/spotMarket/tradeOrders",
            "privateChannel": True,
            "response": False
        }
        self.assertEqual(expected_orders_subscription, sent_subscription_messages[0])
        expected_balances_subscription = {
            "id": 2,
            "type": "subscribe",
            "topic": "/account/balance",
            "privateChannel": True,
            "response": False
        }
        self.assertEqual(expected_balances_subscription, sent_subscription_messages[1])

        self.assertTrue(self._is_logged(
            "INFO",
            "Subscribed to private order changes and balance updates channels..."
        ))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_get_listen_key_successful_with_user_update_event(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = self.get_listen_key_mock()
        mock_api.post(regex_url, body=json.dumps(mock_response))

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        order_event = {
            "type": "message",
            "topic": "/spotMarket/tradeOrders",
            "subject": "orderChange",
            "channelType": "private",
            "data": {

                "symbol": "KCS-USDT",
                "orderType": "limit",
                "side": "buy",
                "orderId": "5efab07953bdea00089965d2",
                "type": "open",
                "orderTime": 1593487481683297666,
                "size": "0.1",
                "filledSize": "0",
                "price": "0.937",
                "clientOid": "1593487481000906",
                "remainSize": "0.1",
                "status": "open",
                "ts": 1593487481683297666
            }
        }
        self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, json.dumps(order_event))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue)
        )

        msg = self.async_run_with_timeout(msg_queue.get())
        self.assertEqual(order_event, msg)
        mock_ws.return_value.ping.assert_called()

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_pong_payload(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = self.get_listen_key_mock()

        mock_pong = {
            "id": "1545910590801",
            "type": "pong"
        }
        mock_api.post(regex_url, body=json.dumps(mock_response))

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, json.dumps(mock_pong))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue)
        )

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch("hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep")
    def test_listen_for_user_stream_connection_failed(self, mock_api, sleep_mock, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = self.get_listen_key_mock()
        mock_api.post(regex_url, body=json.dumps(mock_response))

        mock_ws.side_effect = Exception("TEST ERROR.")
        sleep_mock.side_effect = asyncio.CancelledError  # to finish the task execution

        msg_queue = asyncio.Queue()
        try:
            self.async_run_with_timeout(self.data_source.listen_for_user_stream(msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged("ERROR",
                            "Unexpected error while listening to user stream. Retrying after 5 seconds..."))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch("hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep")
    def test_listen_for_user_stream_iter_message_throws_exception(self, mock_api, sleep_mock, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))

        mock_response = self.get_listen_key_mock()
        mock_api.post(regex_url, body=json.dumps(mock_response))

        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = Exception("TEST ERROR")
        sleep_mock.side_effect = asyncio.CancelledError  # to finish the task execution

        try:
            self.async_run_with_timeout(self.data_source.listen_for_user_stream(msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch("hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id")
    @patch("hummingbot.connector.exchange.kucoin.kucoin_api_user_stream_data_source.KucoinAPIUserStreamDataSource"
           "._time")
    def test_listen_for_user_stream_sends_ping_message_before_ping_interval_finishes(
            self,
            mock_api,
            time_mock,
            id_mock,
            ws_connect_mock):

        id_mock.side_effect = [1, 2, 3, 4]
        time_mock.side_effect = [1000, 1100, 1101, 1102]  # Simulate first ping interval is already due
        url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL)

        resp = {
            "code": "200000",
            "data": {
                "instanceServers": [
                    {
                        "endpoint": "wss://test.url/endpoint",
                        "protocol": "websocket",
                        "encrypt": True,
                        "pingInterval": 20000,
                        "pingTimeout": 10000
                    }
                ],
                "token": "testToken"
            }
        }
        mock_api.post(url, body=json.dumps(resp))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()

        result_subscribe_trades = {
            "type": "ack",
            "id": 1
        }
        result_subscribe_diffs = {
            "type": "ack",
            "id": 2
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        expected_ping_message = {
            "id": 3,
            "type": "ping",
        }
        self.assertEqual(expected_ping_message, sent_messages[-1])
class BinancePerpetualAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}"
        cls.domain = "binance_perpetual_testnet"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task = None
        self.async_tasks: List[asyncio.Task] = []

        self.time_synchronizer = TimeSynchronizer()
        self.time_synchronizer.add_time_offset_ms_sample(0)
        self.data_source = BinancePerpetualAPIOrderBookDataSource(
            time_synchronizer=self.time_synchronizer,
            trading_pairs=[self.trading_pair],
            domain=self.domain,
        )

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()
        BinancePerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {
            self.domain: bidict({self.ex_trading_pair: self.trading_pair})
        }

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        for task in self.async_tasks:
            task.cancel()
        BinancePerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def resume_test_callback(self, *_, **__):
        self.resume_test_event.set()
        return None

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _raise_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _orderbook_update_event(self):
        resp = {
            "stream": f"{self.ex_trading_pair.lower()}@depth",
            "data": {
                "e": "depthUpdate",
                "E": 1631591424198,
                "T": 1631591424189,
                "s": self.ex_trading_pair,
                "U": 752409354963,
                "u": 752409360466,
                "pu": 752409354901,
                "b": [
                    ["43614.31", "0.000"],
                ],
                "a": [
                    ["45277.14", "0.257"],
                ],
            },
        }
        return resp

    def _orderbook_trade_event(self):
        resp = {
            "stream": f"{self.ex_trading_pair.lower()}@aggTrade",
            "data": {
                "e": "aggTrade",
                "E": 1631594403486,
                "a": 817295132,
                "s": self.ex_trading_pair,
                "p": "45266.16",
                "q": "2.206",
                "f": 1437689393,
                "l": 1437689407,
                "T": 1631594403330,
                "m": False,
            },
        }
        return resp

    def _funding_info_event(self):
        resp = {
            "stream": f"{self.ex_trading_pair.lower()}@markPrice",
            "data": {
                "e": "markPriceUpdate",
                "E": 1641288864000,
                "s": self.ex_trading_pair,
                "p": "46353.99600757",
                "P": "46507.47845460",
                "i": "46358.63622407",
                "r": "0.00010000",
                "T": 1641312000000,
            },
        }
        return resp

    @aioresponses()
    def test_get_last_traded_prices(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SERVER_TIME_PATH_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        response = {"serverTime": 1640000003000}

        mock_api.get(regex_url, body=json.dumps(response))

        url = web_utils.rest_url(path_url=CONSTANTS.TICKER_PRICE_CHANGE_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response: Dict[str, Any] = {
            # Truncated responses
            "lastPrice": "10.0",
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result: Dict[str, Any] = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices(
                trading_pairs=[self.trading_pair], domain=self.domain))
        self.assertTrue(self.trading_pair in result)
        self.assertEqual(10.0, result[self.trading_pair])

    @aioresponses()
    def test_init_trading_pair_symbols_failure(self, mock_api):
        BinancePerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"]))

        map = self.async_run_with_timeout(
            self.data_source.trading_pair_symbol_map(
                domain=self.domain,
                time_synchronizer=self.data_source._time_synchronizer))
        self.assertEqual(0, len(map))

    @aioresponses()
    def test_init_trading_pair_symbols_successful(self, mock_api):
        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response: Dict[str, Any] = {
            # Truncated Responses
            "symbols": [
                {
                    "symbol": self.ex_trading_pair,
                    "pair": self.ex_trading_pair,
                    "baseAsset": self.base_asset,
                    "quoteAsset": self.quote_asset,
                    "status": "TRADING",
                },
                {
                    "symbol": "INACTIVEMARKET",
                    "status": "INACTIVE"
                },
            ],
        }
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        self.async_run_with_timeout(
            self.data_source.init_trading_pair_symbols(
                domain=self.domain,
                time_synchronizer=self.data_source._time_synchronizer))
        self.assertEqual(1, len(self.data_source._trading_pair_symbol_map))

    @aioresponses()
    def test_trading_pair_symbol_map_dictionary_not_initialized(
            self, mock_api):
        BinancePerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response: Dict[str, Any] = {
            # Truncated Responses
            "symbols": [
                {
                    "symbol": self.ex_trading_pair,
                    "pair": self.ex_trading_pair,
                    "baseAsset": self.base_asset,
                    "quoteAsset": self.quote_asset,
                    "status": "TRADING",
                },
            ]
        }
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        self.async_run_with_timeout(
            self.data_source.trading_pair_symbol_map(
                domain=self.domain,
                time_synchronizer=self.data_source._time_synchronizer))
        self.assertEqual(1, len(self.data_source._trading_pair_symbol_map))

    def test_trading_pair_symbol_map_dictionary_initialized(self):
        result = self.async_run_with_timeout(
            self.data_source.trading_pair_symbol_map(
                domain=self.domain,
                time_synchronizer=self.data_source._time_synchronizer))
        self.assertEqual(1, len(result))

    def test_convert_from_exchange_trading_pair_not_found(self):
        unknown_pair = "UNKNOWN-PAIR"
        with self.assertRaisesRegex(
                ValueError,
                f"There is no symbol mapping for exchange trading pair {unknown_pair}"
        ):
            self.async_run_with_timeout(
                self.data_source.convert_from_exchange_trading_pair(
                    unknown_pair, domain=self.domain))

    def test_convert_from_exchange_trading_pair_successful(self):
        result = self.async_run_with_timeout(
            self.data_source.convert_from_exchange_trading_pair(
                self.ex_trading_pair, domain=self.domain))
        self.assertEqual(result, self.trading_pair)

    def test_convert_to_exchange_trading_pair_not_found(self):
        unknown_pair = "UNKNOWN-PAIR"
        with self.assertRaisesRegex(
                ValueError,
                f"There is no symbol mapping for trading pair {unknown_pair}"):
            self.async_run_with_timeout(
                self.data_source.convert_to_exchange_trading_pair(
                    unknown_pair, domain=self.domain))

    def test_convert_to_exchange_trading_pair_successful(self):
        result = self.async_run_with_timeout(
            self.data_source.convert_to_exchange_trading_pair(
                self.trading_pair, domain=self.domain))
        self.assertEqual(result, self.ex_trading_pair)

    @aioresponses()
    def test_get_snapshot_exception_raised(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"]))

        with self.assertRaises(IOError) as context:
            self.async_run_with_timeout(
                self.data_source.get_snapshot(
                    trading_pair=self.trading_pair,
                    domain=self.domain,
                    time_synchronizer=self.data_source._time_synchronizer))

        self.assertEqual(
            "Error executing request GET /depth. HTTP status is 400. Error: [\"ERROR\"]",
            str(context.exception))

    @aioresponses()
    def test_get_snapshot_successful(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {
            "lastUpdateId": 1027024,
            "E": 1589436922972,
            "T": 1589436922959,
            "bids": [["10", "1"]],
            "asks": [["11", "1"]],
        }
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))

        result: Dict[str, Any] = self.async_run_with_timeout(
            self.data_source.get_snapshot(
                trading_pair=self.trading_pair,
                domain=self.domain,
                time_synchronizer=self.data_source._time_synchronizer))
        self.assertEqual(mock_response, result)

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {
            "lastUpdateId": 1027024,
            "E": 1589436922972,
            "T": 1589436922959,
            "bids": [["10", "1"]],
            "asks": [["11", "1"]],
        }
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        result = self.async_run_with_timeout(
            self.data_source.get_new_order_book(
                trading_pair=self.trading_pair))
        self.assertIsInstance(result, OrderBook)
        self.assertEqual(1027024, result.snapshot_uid)

    @aioresponses()
    def test_get_funding_info_from_exchange_error_response(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.MARK_PRICE_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400)

        result = self.async_run_with_timeout(
            self.data_source._get_funding_info_from_exchange(
                self.trading_pair))
        self.assertIsNone(result)
        self._is_logged(
            "ERROR",
            f"Unable to fetch FundingInfo for {self.trading_pair}. Error: None"
        )

    @aioresponses()
    def test_get_funding_info_from_exchange_successful(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.MARK_PRICE_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "symbol": self.ex_trading_pair,
            "markPrice": "46382.32704603",
            "indexPrice": "46385.80064948",
            "estimatedSettlePrice": "46510.13598963",
            "lastFundingRate": "0.00010000",
            "interestRate": "0.00010000",
            "nextFundingTime": 1641312000000,
            "time": 1641288825000,
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source._get_funding_info_from_exchange(
                self.trading_pair))

        self.assertIsInstance(result, FundingInfo)
        self.assertEqual(result.trading_pair, self.trading_pair)
        self.assertEqual(result.index_price,
                         Decimal(mock_response["indexPrice"]))
        self.assertEqual(result.mark_price,
                         Decimal(mock_response["markPrice"]))
        self.assertEqual(result.next_funding_utc_timestamp,
                         mock_response["nextFundingTime"])
        self.assertEqual(result.rate,
                         Decimal(mock_response["lastFundingRate"]))

    @aioresponses()
    def test_get_funding_info(self, mock_api):
        self.assertNotIn(self.trading_pair, self.data_source._funding_info)

        url = web_utils.rest_url(CONSTANTS.MARK_PRICE_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "symbol": self.ex_trading_pair,
            "markPrice": "46382.32704603",
            "indexPrice": "46385.80064948",
            "estimatedSettlePrice": "46510.13598963",
            "lastFundingRate": "0.00010000",
            "interestRate": "0.00010000",
            "nextFundingTime": 1641312000000,
            "time": 1641288825000,
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_funding_info(trading_pair=self.trading_pair))

        self.assertIsInstance(result, FundingInfo)
        self.assertEqual(result.trading_pair, self.trading_pair)
        self.assertEqual(result.index_price,
                         Decimal(mock_response["indexPrice"]))
        self.assertEqual(result.mark_price,
                         Decimal(mock_response["markPrice"]))
        self.assertEqual(result.next_funding_utc_timestamp,
                         mock_response["nextFundingTime"])
        self.assertEqual(result.rate,
                         Decimal(mock_response["lastFundingRate"]))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_subscriptions_cancelled_when_connecting(
            self, _, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)
        self.assertEqual(msg_queue.qsize(), 0)

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_logs_exception(self, mock_ws, *_):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.close.return_value = None
        incomplete_resp = {
            "m": 1,
            "i": 2,
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(incomplete_resp))
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._orderbook_update_event()))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.exceptions.TimeoutError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with Websocket connection. Retrying after 30 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_successful(self, mock_ws):
        msg_queue_diffs: asyncio.Queue = asyncio.Queue()
        msg_queue_trades: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.close.return_value = None

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._orderbook_update_event()))
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._orderbook_trade_event()))
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(self._funding_info_event()))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())
        self.listening_task_diffs = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue_diffs))
        self.listening_task_trades = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue_trades))
        self.listening_task_funding_info = self.ev_loop.create_task(
            self.data_source.listen_for_funding_info())

        result: OrderBookMessage = self.async_run_with_timeout(
            msg_queue_diffs.get())
        self.assertIsInstance(result, OrderBookMessage)
        self.assertEqual(OrderBookMessageType.DIFF, result.type)
        self.assertTrue(result.has_update_id)
        self.assertEqual(result.update_id, 752409360466)
        self.assertEqual(self.trading_pair, result.content["trading_pair"])
        self.assertEqual(1, len(result.content["bids"]))
        self.assertEqual(1, len(result.content["asks"]))

        result: OrderBookMessage = self.async_run_with_timeout(
            msg_queue_trades.get())
        self.assertIsInstance(result, OrderBookMessage)
        self.assertEqual(OrderBookMessageType.TRADE, result.type)
        self.assertTrue(result.has_trade_id)
        self.assertEqual(result.trade_id, 817295132)
        self.assertEqual(self.trading_pair, result.content["trading_pair"])

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertIn(self.trading_pair, self.data_source.funding_info)

        funding_info: FundingInfo = self.data_source.funding_info[
            self.trading_pair]
        self.assertTrue(self.data_source.is_funding_info_initialized)
        self.assertEqual(funding_info.trading_pair, self.trading_pair)
        self.assertEqual(funding_info.index_price,
                         Decimal(self._funding_info_event()["data"]["i"]))
        self.assertEqual(funding_info.mark_price,
                         Decimal(self._funding_info_event()["data"]["p"]))
        self.assertEqual(funding_info.next_funding_utc_timestamp,
                         int(self._funding_info_event()["data"]["T"]))
        self.assertEqual(funding_info.rate,
                         Decimal(self._funding_info_event()["data"]["r"]))

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_error_raised(
            self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=asyncio.CancelledError)

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

        self.assertEqual(0, msg_queue.qsize())

    @aioresponses()
    def test_listen_for_order_book_snapshots_logs_exception_error_with_response(
            self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "m": 1,
            "i": 2,
        }
        mock_api.get(regex_url,
                     body=json.dumps(mock_response),
                     callback=self.resume_test_callback)

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred fetching orderbook snapshots. Retrying in 5 seconds..."
            ))

    @aioresponses()
    def test_listen_for_order_book_snapshots_successful(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "lastUpdateId": 1027024,
            "E": 1589436922972,
            "T": 1589436922959,
            "bids": [["10", "1"]],
            "asks": [["11", "1"]],
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        msg_queue: asyncio.Queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        result = self.async_run_with_timeout(msg_queue.get())

        self.assertIsInstance(result, OrderBookMessage)
        self.assertEqual(OrderBookMessageType.SNAPSHOT, result.type)
        self.assertTrue(result.has_update_id)
        self.assertEqual(result.update_id, 1027024)
        self.assertEqual(self.trading_pair, result.content["trading_pair"])

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_funding_info_invalid_trading_pair(self, mock_ws):

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.close.return_value = None

        mock_response = {
            "stream": "unknown_pair@markPrice",
            "data": {
                "e": "markPriceUpdate",
                "E": 1641288864000,
                "s": "unknown_pair",
                "p": "46353.99600757",
                "P": "46507.47845460",
                "i": "46358.63622407",
                "r": "0.00010000",
                "T": 1641312000000,
            },
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(mock_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.listening_task_funding_info = self.ev_loop.create_task(
            self.data_source.listen_for_funding_info())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertNotIn(self.trading_pair, self.data_source.funding_info)

    def test_listen_for_funding_info_cancelled_error_raised(self):

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError
        self.data_source._message_queue[
            CONSTANTS.FUNDING_INFO_STREAM_ID] = mock_queue

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_funding_info())

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_funding_info_logs_exception(self, mock_sleep):

        mock_sleep.side_effect = lambda _: (self.ev_loop.run_until_complete(
            asyncio.sleep(0.5)))

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = lambda: (
            self._raise_exception_and_unlock_test_with_event(
                Exception("TEST ERROR")))
        self.data_source._message_queue[
            CONSTANTS.FUNDING_INFO_STREAM_ID] = mock_queue

        self.listening_task_funding_info = self.ev_loop.create_task(
            self.data_source.listen_for_funding_info())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self._is_logged(
            "ERROR",
            "Unexpected error occured updating funding information. Retrying in 5 seconds... Error: TEST ERROR"
        )
Beispiel #17
0
class CoinflexAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.domain = CONSTANTS.DEFAULT_DOMAIN

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.data_source = CoinflexAPIOrderBookDataSource(trading_pairs=[self.trading_pair],
                                                          throttler=self.throttler,
                                                          domain=self.domain)
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.resume_test_event = asyncio.Event()

        CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {
            "coinflex": bidict(
                {f"{self.ex_trading_pair}": self.trading_pair})
        }

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(record.levelname == log_level and record.getMessage() == message
                   for record in self.log_records)

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout))
        return ret

    def _successfully_subscribed_event(self):
        resp = {
            "result": None,
            "id": 1
        }
        return resp

    def _login_message(self):
        resp = {
            "tag": "1234567890",
            "event": "login",
            "success": True,
            "timestamp": "1234567890"
        }
        return resp

    def _trade_update_event(self):
        resp = {
            "table": "trade",
            "data": [{
                "timestamp": 123456789,
                "marketCode": self.ex_trading_pair,
                "tradeId": 12345,
                "side": "BUY",
                "price": "0.001",
                "quantity": "100",
            }]
        }
        return resp

    def _order_diff_event(self):
        resp = {
            "table": "depth",
            "data": [{
                "timestamp": 123456789,
                "instrumentId": self.ex_trading_pair,
                "seqNum": 157,
                "bids": [["0.0024", "10"]],
                "asks": [["0.0026", "100"]]
            }]
        }
        return resp

    def _snapshot_response(self,
                           update_id=1027024):
        resp = {
            "event": "depthL1000",
            "timestamp": update_id,
            "data": [{
                "bids": [
                    [
                        "4.00000000",
                        "431.00000000"
                    ]
                ],
                "asks": [
                    [
                        "4.00000200",
                        "12.00000000"
                    ]
                ],
                "marketCode": self.ex_trading_pair,
                "timestamp": update_id,
            }]
        }
        return resp

    def _get_regex_url(self,
                       endpoint,
                       return_url=False,
                       endpoint_api_version=None,
                       public=True):
        prv_or_pub = web_utils.public_rest_url if public else web_utils.private_rest_url
        url = prv_or_pub(endpoint, domain=self.domain, endpoint_api_version=endpoint_api_version)
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))
        if return_url:
            return url, regex_url
        return regex_url

    @aioresponses()
    def test_get_last_trade_prices(self, mock_api):
        url, regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, return_url=True)

        mock_response = [{
            "last": "100.0",
            "open24h": "38719",
            "high24h": "38840",
            "low24h": "36377",
            "volume24h": "3622970.9407847790",
            "currencyVolume24h": "96.986",
            "openInterest": "0",
            "marketCode": "COINALPHA-HBOT",
            "timestamp": "1645546950025",
            "lastQty": "0.086",
            "markPrice": "37645",
            "lastMarkPrice": "37628",
        }]

        mock_api.get(regex_url, body=json.dumps(mock_response))

        result: Dict[str, float] = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices(trading_pairs=[self.trading_pair],
                                                    throttler=self.throttler)
        )

        self.assertEqual(1, len(result))
        self.assertEqual(100, result[self.trading_pair])

    @aioresponses()
    def test_get_last_trade_prices_exception_raised(self, mock_api):
        url, regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, return_url=True)

        mock_api.get(regex_url, body=json.dumps([{"marketCode": "COINALPHA-HBOT"}]))

        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_last_traded_prices(trading_pairs=[self.trading_pair],
                                                        throttler=self.throttler)
            )

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {}
        url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain)

        mock_response: Dict[str, Any] = {
            "event": "markets",
            "timestamp": "1639598493658",
            "data": [
                {
                    "marketId": "2001000000000",
                    "marketCode": "BTC-USD",
                    "name": "BTC/USD",
                    "referencePair": "BTC/USD",
                    "base": "BTC",
                    "counter": "USD",
                    "type": "MARGIN",
                    "tickSize": "1",
                    "qtyIncrement": "0.001",
                    "marginCurrency": "USD",
                    "contractValCurrency": "BTC",
                    "upperPriceBound": "39203",
                    "lowerPriceBound": "36187",
                    "marketPrice": "37695",
                    "markPrice": None,
                    "listingDate": 1593316800000,
                    "endDate": 0,
                    "marketPriceLastUpdated": 1645547473153,
                    "markPriceLastUpdated": 0
                },
                {
                    "marketId": "34001000000000",
                    "marketCode": "LTC-USD",
                    "name": "LTC/USD",
                    "referencePair": "LTC/USD",
                    "base": "LTC",
                    "counter": "USD",
                    "type": "SPOT",
                    "tickSize": "0.1",
                    "qtyIncrement": "0.01",
                    "marginCurrency": "USD",
                    "contractValCurrency": "LTC",
                    "upperPriceBound": "114.2",
                    "lowerPriceBound": "97.2",
                    "marketPrice": "105.7",
                    "markPrice": None,
                    "listingDate": 1609765200000,
                    "endDate": 0,
                    "marketPriceLastUpdated": 1645547512308,
                    "markPriceLastUpdated": 0
                },
                {
                    "marketId": "4001000000000",
                    "marketCode": "ETH-USD",
                    "name": "ETH/USD",
                    "referencePair": "ETH/USD",
                    "base": "ETH",
                    "counter": "USD",
                    "type": "SPOT",
                    "tickSize": "0.1",
                    "qtyIncrement": "0.01",
                    "marginCurrency": "USD",
                    "contractValCurrency": "ETH",
                    "upperPriceBound": "2704.3",
                    "lowerPriceBound": "2496.1",
                    "marketPrice": "2600.2",
                    "markPrice": None,
                    "listingDate": 0,
                    "endDate": 0,
                    "marketPriceLastUpdated": 1645547505166,
                    "markPriceLastUpdated": 0
                },
            ]
        }

        mock_api.get(url, body=json.dumps(mock_response))

        result: Dict[str] = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs()
        )

        self.assertEqual(2, len(result))
        self.assertIn("ETH-USD", result)
        self.assertIn("LTC-USD", result)
        self.assertNotIn("BTC-USD", result)

    @aioresponses()
    @patch("hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time")
    def test_fetch_trading_pairs_exception_raised(self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {}

        url, regex_url = self._get_regex_url(CONSTANTS.EXCHANGE_INFO_PATH_URL, return_url=True)

        mock_api.get(regex_url, exception=Exception)

        result: Dict[str] = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs()
        )

        self.assertEqual(0, len(result))

    @aioresponses()
    def test_get_snapshot_successful(self, mock_api):
        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, body=json.dumps(self._snapshot_response()))

        result: Dict[str, Any] = self.async_run_with_timeout(
            self.data_source.get_snapshot(self.trading_pair)
        )

        self.assertEqual(self._snapshot_response()["data"][0], result)

    @aioresponses()
    @patch("hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time")
    def test_get_snapshot_catch_exception(self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, status=400)
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_snapshot(self.trading_pair)
            )

        mock_api.get(regex_url, body=json.dumps({}))
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_snapshot(self.trading_pair)
            )

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, body=json.dumps(self._snapshot_response(update_id=1)))

        result: OrderBook = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair)
        )

        self.assertEqual(1, result.snapshot_uid)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(self._login_message()))

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(1, len(sent_subscription_messages))
        expected_subscription = {
            "op": "subscribe",
            "args": [
                f"trade:{self.ex_trading_pair}",
                f"depth:{self.ex_trading_pair}",
            ],
        }
        self.assertEqual(expected_subscription, sent_subscription_messages[0])

        self.assertTrue(self._is_logged(
            "INFO",
            "Subscribed to public order book and trade channels..."
        ))

    @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep")
    @patch("aiohttp.ClientSession.ws_connect")
    def test_listen_for_subscriptions_raises_cancel_exception(self, mock_ws, _: AsyncMock):
        mock_ws.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

    @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep")
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_logs_exception_details(self, mock_ws, sleep_mock):
        mock_ws.side_effect = Exception("TEST ERROR.")
        sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError())

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."))

    def test_subscribe_channels_raises_cancel_exception(self):
        mock_ws = MagicMock()
        mock_ws.send.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(self.data_source._subscribe_channels(mock_ws))
            self.async_run_with_timeout(self.listening_task)

    def test_subscribe_channels_raises_exception_and_logs_error(self):
        mock_ws = MagicMock()
        mock_ws.send.side_effect = Exception("Test Error")

        with self.assertRaises(Exception):
            self.listening_task = self.ev_loop.create_task(self.data_source._subscribe_channels(mock_ws))
            self.async_run_with_timeout(self.listening_task)

        self.assertTrue(
            self._is_logged("ERROR", "Unexpected error occurred subscribing to order book trading and delta streams...")
        )

    def test_listen_for_trades_cancelled_when_listening(self):
        mock_queue = MagicMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, msg_queue)
            )
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_trades_logs_exception(self):
        incomplete_resp = {
            "data": [{
                "m": 1,
                "i": 2,
            }],
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()]
        self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue)
        )

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged("ERROR", "Unexpected error when processing public trade updates from exchange"))

    def test_listen_for_trades_successful(self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [self._login_message(), self._trade_update_event(), asyncio.CancelledError()]
        self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, msg_queue)
            )
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(-1, msg.update_id)

    def test_listen_for_order_book_diffs_cancelled(self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue)
            )
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_order_book_diffs_logs_exception(self):
        incomplete_resp = {
            "data": [{
                "m": 1,
                "i": 2,
            }],
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()]
        self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue)
        )

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged("ERROR", "Unexpected error when processing public order book updates from exchange"))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(self._login_message()))

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(self._order_diff_event()))

        self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [self._login_message(), self._order_diff_event(), asyncio.CancelledError()]
        self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue)
            )
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(123456789, msg.update_id)

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(self, mock_api):
        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, exception=asyncio.CancelledError)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_order_book_snapshots(self.ev_loop, asyncio.Queue())
            )

    @aioresponses()
    @patch("hummingbot.connector.exchange.coinflex.coinflex_api_order_book_data_source"
           ".CoinflexAPIOrderBookDataSource._sleep")
    @patch("hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time")
    def test_listen_for_order_book_snapshots_log_exception(self, mock_api, retry_sleep_time_mock, sleep_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        msg_queue: asyncio.Queue = asyncio.Queue()
        sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError())

        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, exception=Exception)

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
        )
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}."))

    @aioresponses()
    @patch("hummingbot.connector.exchange.coinflex.coinflex_api_order_book_data_source"
           ".CoinflexAPIOrderBookDataSource._sleep")
    def test_listen_for_order_book_snapshots_log_outer_exception(self, mock_api, sleep_mock):
        msg_queue: asyncio.Queue = asyncio.Queue()
        sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(Exception("Dummy"))

        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, body=json.dumps(self._snapshot_response()))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
        )
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("ERROR", "Unexpected error."))

    @aioresponses()
    def test_listen_for_order_book_snapshots_successful(self, mock_api, ):
        msg_queue: asyncio.Queue = asyncio.Queue()
        url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True)

        mock_api.get(regex_url, body=json.dumps(self._snapshot_response()))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)
        )

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(1027024, msg.update_id)
class ProbitAPIOrderBookDataSourceTest(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        cls.base_asset = "BTC"
        cls.quote_asset = "USDT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.domain = "com"

    def setUp(self) -> None:
        super().setUp()

        self.ev_loop = asyncio.get_event_loop()

        self.api_key = "someKey"
        self.api_secret = "someSecret"
        self.data_source = ProbitAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair], domain=self.domain)
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.log_records = []
        self.mocking_assistant = NetworkMockingAssistant()

        self.async_tasks: List[asyncio.Task] = []

    def tearDown(self) -> None:
        for task in self.async_tasks:
            task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def check_is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def get_ticker_resp_mock(
            self, last_price: float) -> Dict[str, List[Dict[str, str]]]:
        ticker_resp_mock = {
            "data": [{
                "last": str(last_price),
                "low": "3235",
                "high": "3273.8",
                "change": "-23.4",
                "base_volume": "0.16690818",
                "quote_volume": "543.142095541",
                "market_id": self.trading_pair,
                "time": "2018-12-17T06:49:08.000Z"
            }]
        }
        return ticker_resp_mock

    @staticmethod
    def get_market_resp_mock(
            trading_pairs: List[str]) -> Dict[str, List[Dict[str, str]]]:
        market_resp_mock = {
            "data": [{
                "id": trading_pair,
                "base_currency_id": trading_pair.split("-")[0],
                "quote_currency_id": trading_pair.split("-")[1],
                "closed": False,
            } for trading_pair in trading_pairs]
        }
        return market_resp_mock

    def get_order_book_resp_mock(
        self,
        ask_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
        bid_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
    ) -> Dict[str, List[Dict[str, str]]]:
        data = self.get_order_book_resp_mock_data(ask_price_quantity_tuples,
                                                  bid_price_quantity_tuples)
        order_book_resp_mock = {"data": data}
        return order_book_resp_mock

    def get_marketdata_recent_trades_msg_mock(
        self,
        price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
        snapshot: bool = False,
    ) -> Dict:
        msg_mock = self.get_base_marketdata_mock_msg(snapshot)
        data = self.get_recent_trades_resp_mock_data(price_quantity_tuples)
        msg_mock["recent_trades"] = data
        return msg_mock

    def get_marketdata_order_books_msg_mock(
        self,
        ask_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
        bid_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
        snapshot: bool = False,
    ):
        msg_mock = self.get_base_marketdata_mock_msg(snapshot)
        data = self.get_order_book_resp_mock_data(ask_price_quantity_tuples,
                                                  bid_price_quantity_tuples)
        msg_mock["order_books"] = data
        return msg_mock

    def get_base_marketdata_mock_msg(self, snapshot: bool = False) -> Dict:
        msg_mock = {
            "channel": "marketdata",
            "market_id": self.trading_pair,
            "status": "ok",
            "lag": 0,
            "ticker": {
                "time": "2018-08-17T03:00:43.000Z",
                "last": "0.00004221",
                "low": "0.00003953",
                "high": "0.00004233",
                "change": "0.00000195",
                "base_volume": "119304953.57728445",
                "quote_volume": "4914.391934022046355"
            },
            "reset": snapshot,
        }
        return msg_mock

    @staticmethod
    def get_recent_trades_resp_mock_data(
        price_quantity_tuples: Optional[List[Tuple[float, float]]] = None
    ) -> List[Dict[str, str]]:
        price_quantity_tuples = price_quantity_tuples or []
        trades_data = [{
            "price": str(price),
            "quantity": str(quantity),
            "time": "2018-08-17T02:56:17.249Z",
            "side": "buy",
            "tick_direction": "zeroup",
        } for price, quantity in price_quantity_tuples]
        return trades_data

    @staticmethod
    def get_order_book_resp_mock_data(
        ask_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
        bid_price_quantity_tuples: Optional[List[Tuple[float, float]]] = None,
    ) -> List[Dict[str, str]]:
        ask_price_quantity_tuples = ask_price_quantity_tuples or []
        bid_price_quantity_tuples = bid_price_quantity_tuples or []
        ask_data = [{
            "side": "sell",
            "price": str(price),
            "quantity": str(quantity)
        } for price, quantity in ask_price_quantity_tuples]
        bid_data = [{
            "side": "buy",
            "price": str(price),
            "quantity": str(quantity)
        } for price, quantity in bid_price_quantity_tuples]
        data = ask_data + bid_data
        return data

    @aioresponses()
    def test_get_last_traded_prices(self, mocked_api):
        last_price = 3252.4

        url = f"{CONSTANTS.TICKER_URL.format(self.domain)}"
        resp = self.get_ticker_resp_mock(last_price)
        mocked_api.get(url, body=json.dumps(resp))

        res = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices([self.trading_pair],
                                                    self.domain))

        self.assertIn(self.trading_pair, res)
        self.assertEqual(last_price, res[self.trading_pair])

    @aioresponses()
    def test_fetch_trading_pairs(self, mocked_api):
        other_pair = "BTC-USDT"

        url = f"{CONSTANTS.MARKETS_URL.format(self.domain)}"
        resp = self.get_market_resp_mock(
            trading_pairs=[self.trading_pair, other_pair])
        mocked_api.get(url, body=json.dumps(resp))

        res = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs(self.domain))

        self.assertEqual(2, len(res))
        self.assertIn(self.trading_pair, res)
        self.assertIn(other_pair, res)

    @aioresponses()
    def test_get_order_book_data(self, mocked_api):
        url = f"{CONSTANTS.ORDER_BOOK_URL.format(self.domain)}"
        regex_url = re.compile(f"^{url}")
        resp = self.get_order_book_resp_mock(
            ask_price_quantity_tuples=[(1, 2), (3, 4)])
        mocked_api.get(regex_url, body=json.dumps(resp))

        res = self.async_run_with_timeout(
            self.data_source.get_order_book_data(self.trading_pair,
                                                 self.domain))

        self.assertEqual(res, resp)

        res_data = res["data"]

        for d in resp["data"]:
            self.assertIn(d, res_data)

    @aioresponses()
    def test_get_new_order_book(self, mocked_api):
        first_ask_price = 1

        url = f"{CONSTANTS.ORDER_BOOK_URL.format(self.domain)}"
        regex_url = re.compile(f"^{url}")
        resp = self.get_order_book_resp_mock(
            ask_price_quantity_tuples=[(first_ask_price, 2), (3, 4)])
        mocked_api.get(regex_url, body=json.dumps(resp))

        res = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertIsInstance(res, OrderBook)

        asks = list(res.ask_entries())
        bids = list(res.bid_entries())

        self.assertEqual(0, len(bids))
        self.assertEqual(2, len(asks))
        self.assertEqual(first_ask_price, asks[0].price)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.probit.probit_api_order_book_data_source.ProbitAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock,
    )
    def test_listen_for_subscriptions_logs_error_on_closed(
            self, sleep_mock, ws_connect_mock):
        called_event = asyncio.Event()
        continue_event = asyncio.Event()

        async def close_():
            called_event.set()
            await continue_event.wait()

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.close.side_effect = close_
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=WSMsgType.CLOSED)
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        self.async_run_with_timeout(called_event.wait())

        log_target = (  # from _iter_messages
            "Unexpected error occurred iterating through websocket messages.")

        self.assertTrue(
            self.check_is_logged(log_level="ERROR", message=log_target))

        called_event.clear()
        continue_event.set()

        self.async_run_with_timeout(called_event.wait())

        log_target = (  # from listen_for_subscriptions
            "Unexpected error occurred when listening to order book streams. "
            "Retrying in 5 seconds...")

        self.assertTrue(
            self.check_is_logged(log_level="ERROR", message=log_target))
        sleep_mock.assert_called_with(5.0)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_subscribes_to_order_book_streams(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message="")
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_msgs = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)

        self.assertGreaterEqual(len(sent_msgs), 1)

        msg_filters = sent_msgs[0]["filter"]

        self.assertIn(self.data_source.TRADE_FILTER_ID, msg_filters)
        self.assertIn(self.data_source.DIFF_FILTER_ID, msg_filters)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        trade_price = 1

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        message = self.get_marketdata_recent_trades_msg_mock(
            price_quantity_tuples=[(trade_price, 2)])
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(message))

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        outputs = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, outputs)))

        res = self.async_run_with_timeout(outputs.get())

        self.assertIsInstance(res, OrderBookMessage)
        self.assertEqual(str(trade_price), res.content["price"])

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades_ignores_snapshot_msg(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        message = self.get_marketdata_recent_trades_msg_mock(
            price_quantity_tuples=[(1, 2)],
            snapshot=True  # should be ignored
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(message))
        trade_price = 2
        message = self.get_marketdata_recent_trades_msg_mock(
            price_quantity_tuples=[(trade_price, 2)], snapshot=False)
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(message))

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        outputs = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, outputs)))
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        res = self.async_run_with_timeout(outputs.get())

        self.assertEqual(str(trade_price), res.content["price"])

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_generates_snapshot_msg(
            self, ws_connect_mock):
        first_ask_price = 1

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        message = self.get_marketdata_order_books_msg_mock(
            ask_price_quantity_tuples=[(first_ask_price, 2), (3, 4)],
            snapshot=True)
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(message))

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        outputs = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(
                    self.ev_loop, outputs)))

        res = self.async_run_with_timeout(outputs.get())

        self.assertIsInstance(res, OrderBookMessage)
        self.assertEqual(2, len(res.asks))
        self.assertEqual(0, len(res.bids))
        self.assertEqual(first_ask_price, res.asks[0].price)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_generates_diff_msg(
            self, ws_connect_mock):
        ask_price = 1

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        message = self.get_marketdata_order_books_msg_mock(
            ask_price_quantity_tuples=[(ask_price, 2)], snapshot=False)
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(message))

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        outputs = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(
                    self.ev_loop, outputs)))

        res = self.async_run_with_timeout(outputs.get())

        self.assertIsInstance(res, OrderBookMessage)
        self.assertEqual(1, len(res.asks))
        self.assertEqual(0, len(res.bids))
        self.assertEqual(ask_price, res.asks[0].price)
Beispiel #19
0
class OkxAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = cls.base_asset + cls.quote_asset

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task = None
        self.mocking_assistant = NetworkMockingAssistant()

        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = OkxExchange(
            client_config_map=client_config_map,
            okx_api_key="",
            okx_secret_key="",
            okx_passphrase="",
            trading_pairs=[self.trading_pair],
            trading_required=False,
        )
        self.data_source = OkxAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.resume_test_event = asyncio.Event()

        self.connector._set_trading_pair_symbol_map(
            bidict(
                {f"{self.base_asset}-{self.quote_asset}": self.trading_pair}))

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        OkxAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @aioresponses()
    def test_get_new_order_book_successful(self, mock_api):
        url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        resp = {
            "code":
            "0",
            "msg":
            "",
            "data": [{
                "asks": [["41006.8", "0.60038921", "0", "1"]],
                "bids": [["41006.3", "0.30178218", "0", "2"]],
                "ts": "1629966436396"
            }]
        }

        mock_api.get(regex_url, body=json.dumps(resp))

        order_book: OrderBook = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        expected_update_id = int(int(resp["data"][0]["ts"]) * 1e-3)

        self.assertEqual(expected_update_id, order_book.snapshot_uid)
        bids = list(order_book.bid_entries())
        asks = list(order_book.ask_entries())
        self.assertEqual(1, len(bids))
        self.assertEqual(41006.3, bids[0].price)
        self.assertEqual(2, bids[0].amount)
        self.assertEqual(expected_update_id, bids[0].update_id)
        self.assertEqual(1, len(asks))
        self.assertEqual(41006.8, asks[0].price)
        self.assertEqual(1, asks[0].amount)
        self.assertEqual(expected_update_id, asks[0].update_id)

    @aioresponses()
    def test_get_new_order_book_raises_exception(self, mock_api):
        url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400)
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_new_order_book(self.trading_pair))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {
            "event": "subscribe",
            "args": {
                "channel": "trades",
                "instId": self.trading_pair
            }
        }
        result_subscribe_diffs = {
            "event": "subscribe",
            "arg": {
                "channel": "books",
                "instId": self.trading_pair
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(2, len(sent_subscription_messages))
        expected_trade_subscription = {
            "op": "subscribe",
            "args": [{
                "channel": "trades",
                "instId": self.trading_pair
            }]
        }
        self.assertEqual(expected_trade_subscription,
                         sent_subscription_messages[0])
        expected_diff_subscription = {
            "op": "subscribe",
            "args": [{
                "channel": "books",
                "instId": self.trading_pair
            }]
        }
        self.assertEqual(expected_diff_subscription,
                         sent_subscription_messages[1])

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Subscribed to public order book and trade channels..."))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_sends_ping_message_before_ping_interval_finishes(
            self, ws_connect_mock):

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = [
            asyncio.TimeoutError("Test timeiout"), asyncio.CancelledError
        ]

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        sent_messages = self.mocking_assistant.text_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        expected_ping_message = "ping"
        self.assertEqual(expected_ping_message, sent_messages[0])

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect")
    def test_listen_for_subscriptions_raises_cancel_exception(
            self, mock_ws, _: AsyncMock):
        mock_ws.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_logs_exception_details(
            self, mock_ws, sleep_mock):
        mock_ws.side_effect = Exception("TEST ERROR.")
        sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(
            asyncio.CancelledError())

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    def test_subscribe_channels_raises_cancel_exception(self):
        mock_ws = MagicMock()
        mock_ws.send.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source._subscribe_channels(mock_ws))
            self.async_run_with_timeout(self.listening_task)

    def test_subscribe_channels_raises_exception_and_logs_error(self):
        mock_ws = MagicMock()
        mock_ws.send.side_effect = Exception("Test Error")

        with self.assertRaises(Exception):
            self.listening_task = self.ev_loop.create_task(
                self.data_source._subscribe_channels(mock_ws))
            self.async_run_with_timeout(self.listening_task)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred subscribing to order book trading and delta streams..."
            ))

    def test_listen_for_trades_cancelled_when_listening(self):
        mock_queue = MagicMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.data_source._message_queue[
            self.data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_trades_logs_exception(self):
        incomplete_resp = {
            "arg": {
                "channel": "trades",
                "instId": "BTC-USDT"
            },
            "data": [{
                "instId": "BTC-USDT",
            }]
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.data_source._message_queue[
            self.data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public trade updates from exchange"
            ))

    def test_listen_for_trades_successful(self):
        mock_queue = AsyncMock()
        trade_event = {
            "arg": {
                "channel": "trades",
                "instId": self.trading_pair
            },
            "data": [{
                "instId": self.trading_pair,
                "tradeId": "130639474",
                "px": "42219.9",
                "sz": "0.12060306",
                "side": "buy",
                "ts": "1630048897897"
            }]
        }
        mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()]
        self.data_source._message_queue[
            self.data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(OrderBookMessageType.TRADE, msg.type)
        self.assertEqual(trade_event["data"][0]["tradeId"], msg.trade_id)
        self.assertEqual(
            int(trade_event["data"][0]["ts"]) * 1e-3, msg.timestamp)

    def test_listen_for_order_book_diffs_cancelled(self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.data_source._message_queue[
            self.data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_order_book_diffs_logs_exception(self):
        incomplete_resp = {
            "arg": {
                "channel": "books",
                "instId": self.trading_pair
            },
            "action": "update",
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.data_source._message_queue[
            self.data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public order book updates from exchange"
            ))

    def test_listen_for_order_book_diffs_successful(self):
        mock_queue = AsyncMock()
        diff_event = {
            "arg": {
                "channel": "books",
                "instId": self.trading_pair
            },
            "action":
            "update",
            "data": [{
                "asks": [
                    ["8476.98", "415", "0", "13"],
                    ["8477", "7", "0", "2"],
                    ["8477.34", "85", "0", "1"],
                ],
                "bids": [
                    ["8476.97", "256", "0", "12"],
                    ["8475.55", "101", "0", "1"],
                ],
                "ts":
                "1597026383085",
                "checksum":
                -855196043
            }]
        }
        mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()]
        self.data_source._message_queue[
            self.data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(OrderBookMessageType.DIFF, msg.type)
        self.assertEqual(-1, msg.trade_id)
        self.assertEqual(
            int(diff_event["data"][0]["ts"]) * 1e-3, msg.timestamp)
        expected_update_id = int(int(diff_event["data"][0]["ts"]) * 1e-3)
        self.assertEqual(expected_update_id, msg.update_id)

        bids = msg.bids
        asks = msg.asks
        self.assertEqual(2, len(bids))
        self.assertEqual(8476.97, bids[0].price)
        self.assertEqual(12, bids[0].amount)
        self.assertEqual(expected_update_id, bids[0].update_id)
        self.assertEqual(3, len(asks))
        self.assertEqual(8476.98, asks[0].price)
        self.assertEqual(13, asks[0].amount)
        self.assertEqual(expected_update_id, asks[0].update_id)

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(
            self, mock_api):
        url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=asyncio.CancelledError)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_order_book_snapshots(
                    self.ev_loop, asyncio.Queue()))

    @aioresponses()
    @patch("hummingbot.connector.exchange.okx.okx_api_order_book_data_source"
           ".OkxAPIOrderBookDataSource._sleep")
    def test_listen_for_order_book_snapshots_log_exception(
            self, mock_api, sleep_mock):
        msg_queue: asyncio.Queue = asyncio.Queue()
        sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(
            asyncio.CancelledError())

        url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=Exception)

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                f"Unexpected error fetching order book snapshot for {self.trading_pair}."
            ))

    @aioresponses()
    def test_listen_for_order_book_snapshots_successful(
        self,
        mock_api,
    ):
        msg_queue: asyncio.Queue = asyncio.Queue()
        url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        resp = {
            "code":
            "0",
            "msg":
            "",
            "data": [{
                "asks": [["41006.8", "0.60038921", "0", "1"]],
                "bids": [["41006.3", "0.30178218", "0", "2"]],
                "ts": "1629966436396"
            }]
        }

        mock_api.get(regex_url, body=json.dumps(resp))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(OrderBookMessageType.SNAPSHOT, msg.type)
        self.assertEqual(-1, msg.trade_id)
        self.assertEqual(int(resp["data"][0]["ts"]) * 1e-3, msg.timestamp)
        expected_update_id = int(int(resp["data"][0]["ts"]) * 1e-3)
        self.assertEqual(expected_update_id, msg.update_id)

        bids = msg.bids
        asks = msg.asks
        self.assertEqual(1, len(bids))
        self.assertEqual(41006.3, bids[0].price)
        self.assertEqual(2, bids[0].amount)
        self.assertEqual(expected_update_id, bids[0].update_id)
        self.assertEqual(1, len(asks))
        self.assertEqual(41006.8, asks[0].price)
        self.assertEqual(1, asks[0].amount)
        self.assertEqual(expected_update_id, asks[0].update_id)
class WSConnectionTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.ws_url = "ws://some/url"

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        self.client_session = aiohttp.ClientSession()
        self.ws_connection = WSConnection(self.client_session)
        self.async_tasks: List[asyncio.Task] = []

    def tearDown(self) -> None:
        self.ws_connection.disconnect()
        self.client_session.close()
        for task in self.async_tasks:
            task.cancel()
        super().tearDown()

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_connect_and_disconnect(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.assertFalse(self.ws_connection.connected)

        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))

        self.assertTrue(self.ws_connection.connected)

        self.async_run_with_timeout(self.ws_connection.disconnect())

        self.assertFalse(self.ws_connection.connected)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_attempt_to_connect_second_time_raises(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))

        with self.assertRaises(RuntimeError) as e:
            self.async_run_with_timeout(self.ws_connection.connect(
                self.ws_url))

        self.assertEqual("WS is connected.", str(e.exception))

    def test_send_when_disconnected_raises(self):
        request = WSJSONRequest(payload={"one": 1})

        with self.assertRaises(RuntimeError) as e:
            self.async_run_with_timeout(self.ws_connection.send(request))

        self.assertEqual("WS is not connected.", str(e.exception))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_send(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        request = WSJSONRequest(payload={"one": 1})

        self.async_run_with_timeout(self.ws_connection.send(request))

        json_msgs = self.mocking_assistant.json_messages_sent_through_websocket(
            ws_connect_mock.return_value)

        self.assertEqual(1, len(json_msgs))
        self.assertEqual(request.payload, json_msgs[0])

    def test_receive_when_disconnected_raises(self):
        with self.assertRaises(RuntimeError) as e:
            self.async_run_with_timeout(self.ws_connection.receive())

        self.assertEqual("WS is not connected.", str(e.exception))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_raises_on_timeout(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        def raise_timeout(*_, **__):
            raise asyncio.TimeoutError

        ws_connect_mock.return_value.receive.side_effect = raise_timeout
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))

        with self.assertRaises(asyncio.TimeoutError) as e:
            self.async_run_with_timeout(self.ws_connection.receive())

        self.assertEqual("Message receive timed out.", str(e.exception))

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        data = {"one": 1}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message=json.dumps(data))

        self.assertEqual(0, self.ws_connection.last_recv_time)

        response = self.async_run_with_timeout(self.ws_connection.receive())

        self.assertIsInstance(response, WSResponse)
        self.assertEqual(data, response.data)
        self.assertNotEqual(0, self.ws_connection.last_recv_time)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_disconnects_and_raises_on_aiohttp_closed(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.close_code = 1111
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.CLOSED)

        with self.assertRaises(ConnectionError) as e:
            self.async_run_with_timeout(self.ws_connection.receive())

        self.assertEqual(
            "The WS connection was closed unexpectedly. Close code = 1111 msg data: ",
            str(e.exception))
        self.assertFalse(self.ws_connection.connected)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_disconnects_and_raises_on_aiohttp_close(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.close_code = 1111
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.CLOSE)

        with self.assertRaises(ConnectionError) as e:
            self.async_run_with_timeout(self.ws_connection.receive())

        self.assertEqual(
            "The WS connection was closed unexpectedly. Close code = 1111 msg data: ",
            str(e.exception))
        self.assertFalse(self.ws_connection.connected)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_ignores_aiohttp_close_msg_if_disconnect_called(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.CLOSED)
        prev_side_effect = ws_connect_mock.return_value.receive.side_effect

        async def disconnect_on_side_effect(*args, **kwargs):
            await self.ws_connection.disconnect()
            return await prev_side_effect(*args, **kwargs)

        ws_connect_mock.return_value.receive.side_effect = disconnect_on_side_effect

        response = self.async_run_with_timeout(self.ws_connection.receive())

        self.assertFalse(self.ws_connection.connected)
        self.assertIsNone(response)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_ignores_ping(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.PING)
        data = {"one": 1}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message=json.dumps(data))

        response = self.async_run_with_timeout(self.ws_connection.receive())

        self.assertEqual(data, response.data)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_sends_pong_on_ping(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.PING)
        receive_task = self.ev_loop.create_task(self.ws_connection.receive())
        self.async_tasks.append(receive_task)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        ws_connect_mock.return_value.pong.assert_called()

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_ping_updates_last_recv_time(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.PING)
        receive_task = self.ev_loop.create_task(self.ws_connection.receive())
        self.async_tasks.append(receive_task)

        self.assertEqual(0, self.ws_connection.last_recv_time)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertNotEqual(0, self.ws_connection.last_recv_time)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_ignores_pong(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.PONG)
        data = {"one": 1}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message=json.dumps(data))

        response = self.async_run_with_timeout(self.ws_connection.receive())

        self.assertEqual(data, response.data)

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_receive_pong_updates_last_recv_time(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.async_run_with_timeout(self.ws_connection.connect(self.ws_url))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message="",
            message_type=aiohttp.WSMsgType.PONG)
        receive_task = self.ev_loop.create_task(self.ws_connection.receive())
        self.async_tasks.append(receive_task)

        self.assertEqual(0, self.ws_connection.last_recv_time)

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertNotEqual(0, self.ws_connection.last_recv_time)
Beispiel #21
0
class HuobiAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()

        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}".lower()

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.async_tasks: List[asyncio.Task] = []

        self.data_source = HuobiAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair])

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        for task in self.async_tasks:
            task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _compress(self, message: Dict[str, Any]) -> bytes:
        return gzip.compress(json.dumps(message).encode())

    @aioresponses()
    def test_last_traded_prices(self, mock_api):
        url = CONSTANTS.REST_URL + CONSTANTS.TICKER_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response: Dict[str, Any] = {
            "data": [
                {
                    "symbol": self.ex_trading_pair,
                    "open": 1.1,
                    "high": 2.0,
                    "low": 0.8,
                    "close": 1.5,
                    "amount": 100,
                    "vol": 100,
                    "count": 100,
                    "bid": 1.3,
                    "bidSize": 10,
                    "ask": 1.4,
                    "askSize": 10,
                },
            ],
            "status":
            "ok",
            "ts":
            1637229769083,
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices(
                trading_pairs=[self.trading_pair]))

        self.assertEqual(1, len(result))
        self.assertIn(self.trading_pair, result)
        self.assertEqual(1.5, result[self.trading_pair])

    @aioresponses()
    def test_fetch_trading_pairs_failed(self, mock_api):
        url = CONSTANTS.REST_URL + CONSTANTS.API_VERSION + CONSTANTS.SYMBOLS_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400, body=ujson.dumps({}))

        result = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())

        self.assertEqual(0, len(result))

    @aioresponses()
    def test_fetch_trading_pairs_successful(self, mock_api):
        url = CONSTANTS.REST_URL + CONSTANTS.API_VERSION + CONSTANTS.SYMBOLS_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "status":
            "ok",
            "data": [{
                "base-currency": self.base_asset.lower(),
                "quote-currency": self.quote_asset.lower(),
                "price-precision": 4,
                "amount-precision": 2,
                "symbol-partition": "innovation",
                "symbol": self.ex_trading_pair,
                "state": "online",
                "value-precision": 8,
                "min-order-amt": 1,
                "max-order-amt": 10000000,
                "min-order-value": 0.1,
                "limit-order-min-order-amt": 1,
                "limit-order-max-order-amt": 10000000,
                "limit-order-max-buy-amt": 10000000,
                "limit-order-max-sell-amt": 10000000,
                "sell-market-min-order-amt": 1,
                "sell-market-max-order-amt": 1000000,
                "buy-market-max-order-value": 17000,
                "api-trading": "enabled",
                "tags": "abnormalmarket",
            }],
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())

        self.assertEqual(1, len(result))

    @aioresponses()
    def test_get_snapshot_raises_error(self, mock_api):
        url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400, body=ujson.dumps({}))

        expected_error_msg = f"Error fetching Huobi market snapshot for {self.trading_pair}. HTTP status is 400"

        with self.assertRaisesRegex(IOError, expected_error_msg):
            self.async_run_with_timeout(
                self.data_source.get_snapshot(self.trading_pair))

    @aioresponses()
    def test_get_snapshot_successful(self, mock_api):
        url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "ch": f"market.{self.ex_trading_pair}.depth.step0",
            "status": "ok",
            "ts": 1637255180894,
            "tick": {
                "bids": [
                    [57069.57, 0.05],
                ],
                "asks": [
                    [57057.73, 0.007019],
                ],
                "version": 141982962388,
                "ts": 1637255180700,
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_snapshot(self.trading_pair))

        self.assertEqual(mock_response["ch"], result["ch"])
        self.assertEqual(mock_response["status"], result["status"])
        self.assertEqual(1, len(result["tick"]["bids"]))
        self.assertEqual(1, len(result["tick"]["asks"]))

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "ch": f"market.{self.ex_trading_pair}.depth.step0",
            "status": "ok",
            "ts": 1637255180894,
            "tick": {
                "bids": [
                    [57069.57, 0.05],
                ],
                "asks": [
                    [57057.73, 0.007019],
                ],
                "version": 141982962388,
                "ts": 1637255180700,
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertIsInstance(result, OrderBook)
        self.assertEqual(1637255180700, result.snapshot_uid)
        self.assertEqual(1, len(list(result.bid_entries())))
        self.assertEqual(1, len(list(result.ask_entries())))
        self.assertEqual(57069.57, list(result.bid_entries())[0].price)
        self.assertEqual(0.05, list(result.bid_entries())[0].amount)
        self.assertEqual(57057.73, list(result.ask_entries())[0].price)
        self.assertEqual(0.007019, list(result.ask_entries())[0].amount)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_when_subscribing_raised_cancelled(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.huobi.huobi_api_order_book_data_source.HuobiAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_subscriptions_raises_logs_exception(
            self, sleep_mock, ws_connect_mock):
        sleep_mock.side_effect = lambda *_: (
            # Allows listen_for_subscriptions to yield control over thread
            self.ev_loop.run_until_complete(asyncio.sleep(0.0)))
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = lambda *_: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR"))
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_successful_subbed(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        subbed_message = {
            "id": self.ex_trading_pair,
            "status": "ok",
            "subbed": f"market.{self.ex_trading_pair}.depth.step0",
            "ts": 1637333566824,
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(subbed_message),
            message_type=aiohttp.WSMsgType.BINARY)

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(
            0, self.data_source._message_queue[
                self.data_source.TRADE_CHANNEL_SUFFIX].qsize())
        self.assertEqual(
            0, self.data_source._message_queue[
                self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_handle_ping_successful(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        ping_message = {"ping": 1637333569837}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(ping_message),
            message_type=aiohttp.WSMsgType.BINARY)

        # Adds a dummy message to ensure ping message is being handle before breaking from listening task.
        dummy_message = {"msg": "DUMMY MESSAGE"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(dummy_message),
            message_type=aiohttp.WSMsgType.BINARY)

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(
            0, self.data_source._message_queue[
                self.data_source.TRADE_CHANNEL_SUFFIX].qsize())
        self.assertEqual(
            0, self.data_source._message_queue[
                self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize())
        sent_json: List[Dict[
            str,
            Any]] = self.mocking_assistant.json_messages_sent_through_websocket(
                ws_connect_mock.return_value)

        self.assertTrue(any(["pong" in str(payload) for payload in sent_json]))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_successfully_append_trade_and_orderbook_messages(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        trade_message = {
            "ch": f"market.{self.ex_trading_pair}.trade.detail",
            "ts": 1630994963175,
            "tick": {
                "id":
                137005445109,
                "ts":
                1630994963173,
                "data": [{
                    "id": 137005445109359286410323766,
                    "ts": 1630994963173,
                    "tradeId": 102523573486,
                    "amount": 0.006754,
                    "price": 52648.62,
                    "direction": "buy",
                }],
            },
        }
        orderbook_message = {
            "ch": f"market.{self.ex_trading_pair}.depth.step0",
            "ts": 1630983549503,
            "tick": {
                "bids": [[52690.69, 0.36281], [52690.68, 0.2]],
                "asks": [[52690.7, 0.372591], [52691.26, 0.13]],
                "version": 136998124622,
                "ts": 1630983549500,
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(trade_message),
            message_type=aiohttp.WSMsgType.BINARY)
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(orderbook_message),
            message_type=aiohttp.WSMsgType.BINARY,
        )

        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(
            1, self.data_source._message_queue[
                self.data_source.TRADE_CHANNEL_SUFFIX].qsize())
        self.assertEqual(
            1, self.data_source._message_queue[
                self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades_logs_exception(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        trade_message = {
            "ch": f"market.{self.ex_trading_pair}.trade.detail",
            "err": "INCOMPLETE MESSAGE"
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(trade_message),
            message_type=aiohttp.WSMsgType.BINARY)
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        msg_queue = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, msg_queue)))
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(0, msg_queue.qsize())
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with WebSocket connection. Retrying after 30 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        trade_message = {
            "ch": f"market.{self.ex_trading_pair}.trade.detail",
            "ts": 1630994963175,
            "tick": {
                "id":
                137005445109,
                "ts":
                1630994963173,
                "data": [{
                    "id": 137005445109359286410323766,
                    "ts": 1630994963173,
                    "tradeId": 102523573486,
                    "amount": 0.006754,
                    "price": 52648.62,
                    "direction": "buy",
                }],
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(trade_message),
            message_type=aiohttp.WSMsgType.BINARY)
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        msg_queue = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, msg_queue)))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(1, msg_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_logs_exception(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        orderbook_message = {
            "ch": f"market.{self.ex_trading_pair}.depth.step0",
            "err": "INCOMPLETE MESSAGE"
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(orderbook_message),
            message_type=aiohttp.WSMsgType.BINARY,
        )
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))
        msg_queue = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue)))
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(0, msg_queue.qsize())
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with WebSocket connection. Retrying after 30 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_successful(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        orderbook_message = {
            "ch": f"market.{self.ex_trading_pair}.depth.step0",
            "ts": 1630983549503,
            "tick": {
                "bids": [[52690.69, 0.36281], [52690.68, 0.2]],
                "asks": [[52690.7, 0.372591], [52691.26, 0.13]],
                "version": 136998124622,
                "ts": 1630983549500,
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            message=self._compress(orderbook_message),
            message_type=aiohttp.WSMsgType.BINARY,
        )
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions()))

        msg_queue = asyncio.Queue()
        self.async_tasks.append(
            self.ev_loop.create_task(
                self.data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue)))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(1, msg_queue.qsize())

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.huobi.huobi_api_order_book_data_source.HuobiAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_order_book_snapshots_successful(self, mock_api, _):
        url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "ch": f"market.{self.ex_trading_pair}.depth.step0",
            "status": "ok",
            "ts": 1637255180894,
            "tick": {
                "bids": [
                    [57069.57, 0.05],
                ],
                "asks": [
                    [57057.73, 0.007019],
                ],
                "version": 141982962388,
                "ts": 1637255180700,
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        mock_api.get(regex_url, exception=asyncio.CancelledError)

        msg_queue = asyncio.Queue()

        # Purposefully raised error to exit task loop
        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))

        result = self.async_run_with_timeout(coroutine=msg_queue.get())

        self.assertIsInstance(result, OrderBookMessage)
class CryptoComAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = crypto_com_utils.convert_to_exchange_trading_pair(
            cls.trading_pair)

    def setUp(self) -> None:
        super().setUp()

        self.log_records = []
        self.async_task: Optional[asyncio.Task] = None

        self.data_source = CryptoComAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair])

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def test_get_throttler_instance(self):
        self.assertIsInstance(self.data_source._get_throttler_instance(),
                              AsyncThrottler)

    @aioresponses()
    def test_get_last_trade_prices(self, mock_api):
        url = crypto_com_utils.get_rest_url(
            path_url=CONSTANTS.GET_TICKER_PATH_URL)
        regex_url = re.compile(f"^{url}")

        expected_last_traded_price = 1.0

        mock_responses = {
            "code": 0,
            "method": "public/get-ticker",
            "result": {
                "data": [
                    {  # Truncated Response
                        "i": self.ex_trading_pair,
                        "a": expected_last_traded_price,
                    }
                ]
            },
        }
        mock_api.get(regex_url, body=ujson.dumps(mock_responses))

        result = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices(
                trading_pairs=[self.trading_pair]))

        self.assertEqual(result[self.trading_pair], expected_last_traded_price)

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = crypto_com_utils.get_rest_url(
            path_url=CONSTANTS.GET_TICKER_PATH_URL)
        regex_url = re.compile(f"^{url}")

        mock_response = {
            "code": 0,
            "method": "public/get-ticker",
            "result": {
                "data": [
                    {  # Truncated Response
                        "i": self.ex_trading_pair,
                        "a": 1.0,
                    }
                ]
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())

        self.assertTrue(self.trading_pair in result)

    @aioresponses()
    def test_get_order_book_data(self, mock_api):
        url = crypto_com_utils.get_rest_url(
            path_url=CONSTANTS.GET_ORDER_BOOK_PATH_URL)
        regex_url = re.compile(f"^{url}")

        mock_response = {
            "code": 0,
            "method": "public/get-book",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "depth":
                150,
                "data": [{
                    "bids": [
                        [999.00, 1.0, 1],
                    ],
                    "asks": [
                        [1000.00, 1.0, 1],
                    ],
                    "t": 1634731570152,
                }],
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_order_book_data(self.trading_pair))

        self.assertIsInstance(result, dict)

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = crypto_com_utils.get_rest_url(
            path_url=CONSTANTS.GET_ORDER_BOOK_PATH_URL)
        regex_url = re.compile(f"^{url}")

        snapshot_timestamp = 1634731570152
        mock_response = {
            "code": 0,
            "method": "public/get-book",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "depth":
                150,
                "data": [{
                    "bids": [
                        [999.00, 1.0, 1],
                    ],
                    "asks": [
                        [1000.00, 1.0, 1],
                    ],
                    "t": snapshot_timestamp,
                }],
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertIsInstance(result, OrderBook)
        self.assertEqual(snapshot_timestamp, result.snapshot_uid)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_create_websocket_connection_raised_cancelled(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source._create_websocket_connection())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_create_websocket_connection_logs_exception(self, ws_connect_mock):
        ws_connect_mock.side_effect = Exception("TEST ERROR")

        with self.assertRaisesRegex(Exception, "TEST ERROR"):
            self.async_run_with_timeout(
                self.data_source._create_websocket_connection())

        self.assertTrue(
            self._is_logged(
                "NETWORK",
                "Unexpected error occured connecting to crypto_com WebSocket API. (TEST ERROR)"
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_exception_raised_cancelled_when_connecting(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_subscriptions_exception_raised_cancelled_when_subscribing(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_subscriptions_exception_raised_cancelled_when_listening(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_subscription_logs_exception(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR"))
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_subscriptions_enqueues_diff_and_trade_messages(
            self, _, ws_connect_mock):
        diffs_queue = self.data_source._message_queue[
            CryptoComWebsocket.DIFF_CHANNEL_ID]
        trade_queue = self.data_source._message_queue[
            CryptoComWebsocket.TRADE_CHANNEL_ID]

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add diff event message be processed
        diff_response = {
            "method": "subscribe",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "subscription":
                f"book.{self.ex_trading_pair}.150",
                "channel":
                "book",
                "depth":
                150,
                "data": [{
                    "bids": [[11746.488, 128, 8]],
                    "asks": [[11747.488, 201, 12]],
                    "t": 1587523078844
                }],
            },
        }

        # Add trade event message be processed
        trade_response = {
            "method": "subscribe",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "subscription":
                f"trade.{self.ex_trading_pair}",
                "channel":
                "trade",
                "data": [{
                    "p": 162.12,
                    "q": 11.085,
                    "s": "buy",
                    "d": 1210447366,
                    "t": 1587523078844,
                    "dataTime": 0,
                    "i": f"{self.ex_trading_pair}",
                }],
            },
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, ujson.dumps(diff_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, ujson.dumps(trade_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(1, diffs_queue.qsize())
        self.assertEqual(1, trade_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_order_book_diff_raises_cancel_exceptions(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop,
                                                         output=queue))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task.cancel()
            self.ev_loop.run_until_complete(self.listening_task)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_api_order_book_data_source.CryptoComAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_order_book_diff_logs_exception_parsing_message(
            self, _, __, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add incomplete diff event message be processed
        incomplete_diff_response = {
            "method": "subscribe",
            "result": {
                "channel": "book",
                "INCOMPLETE": "PAYLOAD"
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            ujson.dumps(incomplete_diff_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop,
                                                         output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)
        self.assertTrue(
            self._is_logged(
                "ERROR",
                f"Unexpected error parsing order book diff payload. Payload: {incomplete_diff_response['result']}",
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_order_book_diff_successful(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add diff event message be processed
        diff_response = {
            "method": "subscribe",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "subscription":
                f"book.{self.ex_trading_pair}.150",
                "channel":
                "book",
                "depth":
                150,
                "data": [{
                    "bids": [[11746.488, 128, 8]],
                    "asks": [[11747.488, 201, 12]],
                    "t": 1587523078844
                }],
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, ujson.dumps(diff_response))

        diffs_queue = self.data_source._message_queue[
            CryptoComWebsocket.DIFF_CHANNEL_ID]
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop,
                                                         output=output_queue))

        order_book_message = self.async_run_with_timeout(output_queue.get())

        self.assertTrue(diffs_queue.empty())
        self.assertEqual(1587523078844, order_book_message.update_id)
        self.assertEqual(1587523078844, order_book_message.timestamp)
        self.assertEqual(11746.488, order_book_message.bids[0].price)
        self.assertEqual(11747.488, order_book_message.asks[0].price)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_trades_raises_cancel_exceptions(
            self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=queue))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task.cancel()
            self.ev_loop.run_until_complete(self.listening_task)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_api_order_book_data_source.CryptoComAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_trades_logs_exception_parsing_message(
            self, _, __, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add incomplete trade event message be processed
        incomplete_trade_response = {
            "method": "subscribe",
            "result": {
                "channel": "trade",
                "INCOMPLETE": "PAYLOAD"
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value,
            ujson.dumps(incomplete_trade_response))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)
        self.assertTrue(
            self._is_logged(
                "ERROR",
                f"Unexpected error parsing order book trade payload. Payload: {incomplete_trade_response['result']}",
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.crypto_com.crypto_com_websocket.CryptoComWebsocket._sleep"
    )
    def test_listen_for_trades_successful(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Add trade event message be processed
        trade_response = {
            "method": "subscribe",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "subscription":
                f"trade.{self.ex_trading_pair}",
                "channel":
                "trade",
                "data": [{
                    "p": 162.12,
                    "q": 11.085,
                    "s": "buy",
                    "d": 1210447366,
                    "t": 1587523078844,
                    "dataTime": 0,
                    "i": f"{self.ex_trading_pair}",
                }],
            },
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, ujson.dumps(trade_response))

        trades_queue = self.data_source._message_queue[
            CryptoComWebsocket.TRADE_CHANNEL_ID]
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        output_queue = asyncio.Queue()
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=output_queue))

        first_trade_message = self.async_run_with_timeout(output_queue.get())

        self.assertTrue(trades_queue.empty())
        self.assertEqual(1587523078844, first_trade_message.timestamp)

    @aioresponses()
    def test_listen_for_order_book_snapshots_successful(self, mock_api):
        url = crypto_com_utils.get_rest_url(
            path_url=CONSTANTS.GET_ORDER_BOOK_PATH_URL)
        regex_url = re.compile(f"^{url}")

        mock_response = {
            "code": 0,
            "method": "public/get-book",
            "result": {
                "instrument_name":
                self.ex_trading_pair,
                "depth":
                150,
                "data": [{
                    "bids": [
                        [999.00, 1.0, 1],
                    ],
                    "asks": [
                        [1000.00, 1.0, 1],
                    ],
                    "t": 1634731570152,
                }],
            },
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        order_book_messages = asyncio.Queue()

        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                ev_loop=self.ev_loop, output=order_book_messages))

        order_book_message = self.async_run_with_timeout(
            order_book_messages.get())

        self.assertTrue(order_book_messages.empty())
        self.assertEqual(1634731570152, order_book_message.update_id)
        self.assertEqual(1634731570152, order_book_message.timestamp)
        self.assertEqual(999.00, order_book_message.bids[0].price)
        self.assertEqual(1000.00, order_book_message.asks[0].price)
class BitmartAPIUserStreamDataSourceTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()
        self.client_config_map = ClientConfigAdapter(ClientConfigMap())

        self.time_synchronizer = MagicMock()
        self.time_synchronizer.time.return_value = 1640001112.223

        self.auth = BitmartAuth(api_key="test_api_key",
                                secret_key="test_secret_key",
                                memo="test_memo",
                                time_provider=self.time_synchronizer)

        self.connector = BitmartExchange(
            client_config_map=self.client_config_map,
            bitmart_api_key="test_api_key",
            bitmart_secret_key="test_secret_key",
            bitmart_memo="test_memo",
            trading_pairs=[self.trading_pair],
            trading_required=False,
        )
        self.connector._web_assistants_factory._auth = self.auth

        self.data_source = BitmartAPIUserStreamDataSource(
            auth=self.auth,
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.connector._set_trading_pair_symbol_map(
            bidict({self.ex_trading_pair: self.trading_pair}))

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_subscribes_to_orders_events(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        successful_login_response = {"event": "login"}
        result_subscribe_orders = {
            "event": "subscribe",
            "topic": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME,
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(successful_login_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_orders))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(2, len(sent_messages))
        expected_login = {
            "op":
            "login",
            "args": [
                "test_api_key",
                str(int(self.time_synchronizer.time() * 1e3)),
                "f0f176c799346a7730c9c237a09d14742971f3ab59848dde75ef1ac95b04c4e5"
            ]  # noqa: mock
        }
        self.assertEqual(expected_login, sent_messages[0])
        expected_orders_subscription = {
            "op":
            "subscribe",
            "args": [
                f"{CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME}:{self.ex_trading_pair}"
            ]
        }
        self.assertEqual(expected_orders_subscription, sent_messages[1])

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Subscribed to private account and orders channels..."))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_logs_error_when_login_fails(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        erroneous_login_response = {"event": "login", "errorCode": "4001"}

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(erroneous_login_response))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Error authenticating the private websocket connection"))

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_invalid_payload(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        successful_login_response = {"event": "login"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(successful_login_response))

        event_without_data = {
            "table": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(event_without_data))

        event_without_table = {
            "data": [{
                "symbol": self.ex_trading_pair,
                "side": "buy",
                "type": "market",
                "notional": "",
                "size": "1.0000000000",
                "ms_t": "1609926028000",
                "price": "46100.0000000000",
                "filled_notional": "46100.0000000000",
                "filled_size": "1.0000000000",
                "margin_trading": "0",
                "state": "4",
                "order_id": "2147857398",
                "order_type": "0",
                "last_fill_time": "1609926039226",
                "last_fill_price": "46100.00000",
                "last_fill_count": "1.00000",
                "exec_type": "M",
                "detail_id": "256348632",
                "client_order_id": "order4872191"
            }],
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(event_without_table))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listen_for_user_stream_connection_failed(self, sleep_mock,
                                                      mock_ws):
        mock_ws.side_effect = Exception("TEST ERROR")
        sleep_mock.side_effect = asyncio.CancelledError

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_initialization(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(messages))
            self.ev_loop.run_until_complete(self.listening_task)

    @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_authentication(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(messages))
            self.ev_loop.run_until_complete(self.listening_task)

    def test_subscribe_channels_raises_cancel_exception(self):
        ws_assistant = AsyncMock()
        ws_assistant.send.side_effect = asyncio.CancelledError
        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source._subscribe_channels(ws_assistant))
            self.ev_loop.run_until_complete(self.listening_task)

    @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listening_process_logs_exception_during_events_subscription(
            self, sleep_mock, mock_ws):
        self.connector._set_trading_pair_symbol_map({})

        messages = asyncio.Queue()
        sleep_mock.side_effect = asyncio.CancelledError
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps({"event": "login"}))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(messages))

        try:
            self.async_run_with_timeout(self.listening_task, timeout=3)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred subscribing to order book trading and delta streams..."
            ))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_processes_order_event(self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        successful_login_response = {"event": "login"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(successful_login_response))

        order_event = {
            "data": [{
                "symbol": self.ex_trading_pair,
                "side": "buy",
                "type": "market",
                "notional": "",
                "size": "1.0000000000",
                "ms_t": "1609926028000",
                "price": "46100.0000000000",
                "filled_notional": "46100.0000000000",
                "filled_size": "1.0000000000",
                "margin_trading": "0",
                "state": "4",
                "order_id": "2147857398",
                "order_type": "0",
                "last_fill_time": "1609926039226",
                "last_fill_price": "46100.00000",
                "last_fill_count": "1.00000",
                "exec_type": "M",
                "detail_id": "256348632",
                "client_order_id": "order4872191"
            }],
            "table":
            CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(order_event))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(1, msg_queue.qsize())
        order_event_message = msg_queue.get_nowait()
        self.assertEqual(order_event, order_event_message)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_processes_compressed_order_event(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        successful_login_response = {"event": "login"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(successful_login_response))

        order_event = {
            "data": [{
                "symbol": self.ex_trading_pair,
                "side": "buy",
                "type": "market",
                "notional": "",
                "size": "1.0000000000",
                "ms_t": "1609926028000",
                "price": "46100.0000000000",
                "filled_notional": "46100.0000000000",
                "filled_size": "1.0000000000",
                "margin_trading": "0",
                "state": "4",
                "order_id": "2147857398",
                "order_type": "0",
                "last_fill_time": "1609926039226",
                "last_fill_price": "46100.00000",
                "last_fill_count": "1.00000",
                "exec_type": "M",
                "detail_id": "256348632",
                "client_order_id": "order4872191"
            }],
            "table":
            CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=bitmart_utils.compress_ws_message(json.dumps(order_event)),
            message_type=WSMsgType.BINARY)

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(1, msg_queue.qsize())
        order_event_message = msg_queue.get_nowait()
        self.assertEqual(order_event, order_event_message)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_logs_details_for_order_event_with_errors(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        successful_login_response = {"event": "login"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(successful_login_response))

        order_event = {
            "errorCode":
            "4001",
            "errorMessage":
            "Error",
            "data": [{
                "symbol": self.ex_trading_pair,
                "side": "buy",
                "type": "market",
                "notional": "",
                "size": "1.0000000000",
                "ms_t": "1609926028000",
                "price": "46100.0000000000",
                "filled_notional": "46100.0000000000",
                "filled_size": "1.0000000000",
                "margin_trading": "0",
                "state": "4",
                "order_id": "2147857398",
                "order_type": "0",
                "last_fill_time": "1609926039226",
                "last_fill_price": "46100.00000",
                "last_fill_count": "1.00000",
                "exec_type": "M",
                "detail_id": "256348632",
                "client_order_id": "order4872191"
            }],
            "table":
            CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(order_event))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_logs_details_for_invalid_event_message(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        successful_login_response = {"event": "login"}
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message=json.dumps(successful_login_response))

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=mock_ws.return_value,
            message="invalid message content")

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

        self.assertTrue(
            self._is_logged(
                "WARNING",
                "Invalid event message received through the order book data source connection (invalid message content)"
            ))
class DydxPerpetualAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()

        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()

        self.log_records = []
        self.async_task: Optional[asyncio.Task] = None

        self.data_source = DydxPerpetualAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair])

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()
        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        self.async_task and self.async_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @aioresponses()
    def test_get_last_trade_prices(self, mock_api):
        url = CONSTANTS.DYDX_REST_URL + CONSTANTS.TICKER_URL + "/" + self.trading_pair
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "markets": {
                self.trading_pair: {
                    "market": self.trading_pair,
                    "open": "65603",
                    "high": "66350",
                    "low": "60342",
                    "close": "60711",
                    "baseVolume": "27933.3033",
                    "quoteVolume": "1758807943.4273",
                    "type": "PERPETUAL",
                    "fees": "1057036.553334",
                }
            }
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_last_traded_prices([self.trading_pair]))

        self.assertEqual(1, len(result))
        self.assertEqual(float("60711"), result[self.trading_pair])

    @aioresponses()
    def test_fetch_trading_pairs_failed(self, mock_api):
        url = f"{CONSTANTS.DYDX_REST_URL}{CONSTANTS.MARKETS_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400, body=ujson.dumps({}))

        result = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())

        self.assertEqual(0, len(result))
        self.assertNotIn(self.trading_pair, result)

    @aioresponses()
    def test_fetch_trading_pairs_successful(self, mock_api):
        url = f"{CONSTANTS.DYDX_REST_URL}{CONSTANTS.MARKETS_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "markets": {
                self.trading_pair: {
                    "market": self.trading_pair,
                    "status": "ONLINE",
                    "baseAsset": "BTC",
                    "quoteAsset": "USD",
                    "stepSize": "0.0001",
                    "tickSize": "1",
                    "indexPrice": "61001.4995",
                    "oraclePrice": "60971.6290",
                    "priceChange24H": "-4559.950500",
                    "nextFundingRate": "0.0000046999",
                    "nextFundingAt": "2021-11-16T09:00:00.000Z",
                    "minOrderSize": "0.001",
                    "type": "PERPETUAL",
                    "initialMarginFraction": "0.04",
                    "maintenanceMarginFraction": "0.03",
                    "volume24H": "1799563001.940300",
                    "trades24H": "142324",
                    "openInterest": "6108.6751",
                    "incrementalInitialMarginFraction": "0.01",
                    "incrementalPositionSize": "1.5",
                    "maxPositionSize": "170",
                    "baselinePositionSize": "9",
                    "assetResolution": "10000000000",
                },
            }
        }

        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())

        self.assertEqual(1, len(result))
        self.assertIn(self.trading_pair, result)

    @aioresponses()
    def test_get_snapshot_raise_io_error(self, mock_api):
        url = CONSTANTS.DYDX_REST_URL + CONSTANTS.SNAPSHOT_URL + "/" + self.trading_pair
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, status=400, body=ujson.dumps({}))

        with self.assertRaisesRegex(
                IOError,
                f"Error fetching dydx market snapshot for {self.trading_pair}. "
                f"HTTP status is 400."):
            self.async_run_with_timeout(
                self.data_source.get_snapshot(self.trading_pair))

    @aioresponses()
    def test_get_snapshot_successful(self, mock_api):
        url = CONSTANTS.DYDX_REST_URL + CONSTANTS.SNAPSHOT_URL + "/" + self.trading_pair
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "asks": [{
                "size": "2.0",
                "price": "20.0"
            }],
            "bids": [{
                "size": "1.0",
                "price": "10.0"
            }],
        }
        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_snapshot(self.trading_pair))

        self.assertEqual(mock_response["asks"][0]["size"],
                         result["asks"][0]["size"])
        self.assertEqual(mock_response["asks"][0]["price"],
                         result["asks"][0]["price"])
        self.assertEqual(mock_response["bids"][0]["size"],
                         result["bids"][0]["size"])
        self.assertEqual(mock_response["bids"][0]["price"],
                         result["bids"][0]["price"])
        self.assertEqual(self.trading_pair, result["trading_pair"])

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = CONSTANTS.DYDX_REST_URL + CONSTANTS.SNAPSHOT_URL + "/" + self.trading_pair
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            "asks": [{
                "size": "2.0",
                "price": "20.0"
            }],
            "bids": [{
                "size": "1.0",
                "price": "10.0"
            }],
        }
        mock_api.get(regex_url, body=ujson.dumps(mock_response))

        result = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))
        self.assertIsInstance(result, OrderBook)
        self.assertEqual(1, len(list(result.bid_entries())))
        self.assertEqual(1, len(list(result.ask_entries())))
        self.assertEqual(float(mock_response["bids"][0]["price"]),
                         list(result.bid_entries())[0].price)
        self.assertEqual(float(mock_response["bids"][0]["size"]),
                         list(result.bid_entries())[0].amount)
        self.assertEqual(float(mock_response["asks"][0]["price"]),
                         list(result.ask_entries())[0].price)
        self.assertEqual(float(mock_response["asks"][0]["size"]),
                         list(result.ask_entries())[0].amount)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_api_order_book_data_source.DydxPerpetualAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_subcriptions_raises_cancelled_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.data_source.listen_for_subscriptions())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_api_order_book_data_source.DydxPerpetualAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_subcriptions_raises_logs_exception(
            self, mock_sleep, ws_connect_mock):
        mock_sleep.side_effect = lambda: (self.ev_loop.run_until_complete(
            asyncio.sleep(0.5)))
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = lambda *_: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR"))
        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.async_run_with_timeout(self.resume_test_event.wait(), 1.0)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_api_order_book_data_source.DydxPerpetualAPIOrderBookDataSource._sleep"
    )
    def test_listen_for_subcriptions_successful(self, mock_sleep,
                                                ws_connect_mock):
        mock_sleep.side_effect = lambda: (self.ev_loop.run_until_complete(
            asyncio.sleep(0.5)))
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        mock_response = {
            "type": "channel_data",
            "connection_id": "d600a0d2-8039-4cd9-a010-2d6f5c336473",
            "message_id": 2,
            "id": "LINK-USD",
            "channel": "v3_orderbook",
            "contents": {
                "offset": "3218381978",
                "bids": [],
                "asks": [["36.152", "304.8"]]
            },
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, message=ujson.dumps(mock_response))

        self.async_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(
            1, self.data_source._message_queue[
                self.data_source.ORDERBOOK_CHANNEL].qsize())
Beispiel #25
0
class BinancePerpetualUserStreamDataSourceUnitTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = cls.base_asset + cls.quote_asset
        cls.domain = CONSTANTS.TESTNET_DOMAIN

        cls.api_key = "TEST_API_KEY"
        cls.secret_key = "TEST_SECRET_KEY"
        cls.listen_key = "TEST_LISTEN_KEY"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.emulated_time = 1640001112.223
        self.auth = BinancePerpetualAuth(api_key=self.api_key,
                                         api_secret=self.secret_key,
                                         time_provider=self)
        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.time_synchronizer = TimeSynchronizer()
        self.time_synchronizer.add_time_offset_ms_sample(0)
        self.data_source = BinancePerpetualUserStreamDataSource(
            auth=self.auth,
            domain=self.domain,
            throttler=self.throttler,
            time_synchronizer=self.time_synchronizer)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mock_done_event = asyncio.Event()
        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _mock_responses_done_callback(self, *_, **__):
        self.mock_done_event.set()

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _successful_get_listen_key_response(self) -> str:
        resp = {"listenKey": self.listen_key}
        return ujson.dumps(resp)

    def _error_response(self) -> Dict[str, Any]:
        resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"}

        return resp

    def _simulate_user_update_event(self):
        # Order Trade Update
        resp = {
            "e": "ORDER_TRADE_UPDATE",
            "E": 1591274595442,
            "T": 1591274595453,
            "i": "SfsR",
            "o": {
                "s": "BTCUSD_200925",
                "c": "TEST",
                "S": "SELL",
                "o": "TRAILING_STOP_MARKET",
                "f": "GTC",
                "q": "2",
                "p": "0",
                "ap": "0",
                "sp": "9103.1",
                "x": "NEW",
                "X": "NEW",
                "i": 8888888,
                "l": "0",
                "z": "0",
                "L": "0",
                "ma": "BTC",
                "N": "BTC",
                "n": "0",
                "T": 1591274595442,
                "t": 0,
                "rp": "0",
                "b": "0",
                "a": "0",
                "m": False,
                "R": False,
                "wt": "CONTRACT_PRICE",
                "ot": "TRAILING_STOP_MARKET",
                "ps": "LONG",
                "cp": False,
                "AP": "9476.8",
                "cr": "5.0",
                "pP": False,
            },
        }
        return ujson.dumps(resp)

    def time(self):
        # Implemented to emulate a TimeSynchronizer
        return self.emulated_time

    def test_last_recv_time(self):
        # Initial last_recv_time
        self.assertEqual(0, self.data_source.last_recv_time)

    @aioresponses()
    def test_get_listen_key_exception_raised(self, mock_api):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.post(regex_url,
                      status=400,
                      body=ujson.dumps(self._error_response))

        with self.assertRaises(IOError):
            self.async_run_with_timeout(self.data_source.get_listen_key())

    @aioresponses()
    def test_get_listen_key_successful(self, mock_api):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.post(regex_url,
                      body=self._successful_get_listen_key_response())

        result: str = self.async_run_with_timeout(
            self.data_source.get_listen_key())

        self.assertEqual(self.listen_key, result)

    @aioresponses()
    def test_ping_listen_key_failed_log_warning(self, mock_api):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.put(regex_url,
                     status=400,
                     body=ujson.dumps(self._error_response()))

        self.data_source._current_listen_key = self.listen_key
        result: bool = self.async_run_with_timeout(
            self.data_source.ping_listen_key())

        self.assertTrue(
            self._is_logged(
                "WARNING",
                f"Failed to refresh the listen key {self.listen_key}: {self._error_response()}"
            ))
        self.assertFalse(result)

    @aioresponses()
    def test_ping_listen_key_successful(self, mock_api):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.put(regex_url, body=ujson.dumps({}))

        self.data_source._current_listen_key = self.listen_key
        result: bool = self.async_run_with_timeout(
            self.data_source.ping_listen_key())
        self.assertTrue(result)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_create_websocket_connection_log_exception(self, mock_api,
                                                       mock_ws):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.post(regex_url,
                      body=self._successful_get_listen_key_response())

        mock_ws.side_effect = Exception("TEST ERROR.")

        msg_queue = asyncio.Queue()
        try:
            self.async_run_with_timeout(
                self.data_source.listen_for_user_stream(msg_queue))
        except asyncio.exceptions.TimeoutError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.",
            ))

    @aioresponses()
    def test_manage_listen_key_task_loop_keep_alive_failed(self, mock_api):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.put(regex_url,
                     status=400,
                     body=ujson.dumps(self._error_response()),
                     callback=self._mock_responses_done_callback)

        self.data_source._current_listen_key = self.listen_key

        # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached
        self.data_source._last_listen_key_ping_ts = 0

        self.listening_task = self.ev_loop.create_task(
            self.data_source._manage_listen_key_task_loop())

        self.async_run_with_timeout(self.mock_done_event.wait())

        self.assertTrue(
            self._is_logged("ERROR", "Error occurred renewing listen key... "))
        self.assertIsNone(self.data_source._current_listen_key)
        self.assertFalse(
            self.data_source._listen_key_initialized_event.is_set())

    @aioresponses()
    def test_manage_listen_key_task_loop_keep_alive_successful(self, mock_api):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.put(regex_url,
                     body=ujson.dumps({}),
                     callback=self._mock_responses_done_callback)

        self.data_source._current_listen_key = self.listen_key

        # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached
        self.data_source._last_listen_key_ping_ts = 0

        self.listening_task = self.ev_loop.create_task(
            self.data_source._manage_listen_key_task_loop())

        self.async_run_with_timeout(self.mock_done_event.wait())

        self.assertTrue(
            self._is_logged("INFO",
                            f"Refreshed listen key {self.listen_key}."))
        self.assertGreater(self.data_source._last_listen_key_ping_ts, 0)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_create_websocket_connection_failed(
            self, mock_api, mock_ws):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.post(regex_url,
                      body=self._successful_get_listen_key_response())

        mock_ws.side_effect = Exception("TEST ERROR.")

        msg_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        try:
            self.async_run_with_timeout(msg_queue.get())
        except asyncio.exceptions.TimeoutError:
            pass

        self.assertTrue(
            self._is_logged(
                "INFO", f"Successfully obtained listen key {self.listen_key}"))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.",
            ))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep"
    )
    def test_listen_for_user_stream_iter_message_throws_exception(
            self, mock_api, _, mock_ws):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {"listenKey": self.listen_key}
        mock_api.post(regex_url, body=ujson.dumps(mock_response))

        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = Exception("TEST ERROR")
        mock_ws.return_value.closed = False
        mock_ws.return_value.close.side_effect = Exception

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        try:
            self.async_run_with_timeout(msg_queue.get())
        except Exception:
            pass

        self.assertTrue(
            self._is_logged(
                "INFO", f"Successfully obtained listen key {self.listen_key}"))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR",
            ))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_successful(self, mock_api, mock_ws):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.post(regex_url,
                      body=self._successful_get_listen_key_response())

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._simulate_user_update_event())

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        msg = self.async_run_with_timeout(msg_queue.get())
        self.assertTrue(msg, self._simulate_user_update_event)
        mock_ws.return_value.ping.assert_called()

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_empty_payload(
            self, mock_api, mock_ws):
        url = web_utils.rest_url(
            path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
            domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.post(regex_url,
                      body=self._successful_get_listen_key_response())

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, "")

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())
Beispiel #26
0
class TestBybitAPIOrderBookDataSource(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = cls.base_asset + cls.quote_asset
        cls.domain = CONSTANTS.DEFAULT_DOMAIN

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.async_task = None

        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.time_synchronnizer = TimeSynchronizer()
        self.time_synchronnizer.add_time_offset_ms_sample(1000)
        self.ob_data_source = BybitAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            throttler=self.throttler,
            time_synchronizer=self.time_synchronnizer)

        self.ob_data_source.logger().setLevel(1)
        self.ob_data_source.logger().addHandler(self)

        BybitAPIOrderBookDataSource._trading_pair_symbol_map = {
            self.domain: bidict({self.ex_trading_pair: self.trading_pair})
        }

    def tearDown(self) -> None:
        self.async_task and self.async_task.cancel()
        BybitAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    # TRADING PAIRS
    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        BybitAPIOrderBookDataSource._trading_pair_symbol_map = {}
        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL)

        resp = {
            "ret_code":
            0,
            "ret_msg":
            "",
            "ext_code":
            None,
            "ext_info":
            None,
            "result": [{
                "name": "BTCUSDT",
                "alias": "BTCUSDT",
                "baseCurrency": "BTC",
                "quoteCurrency": "USDT",
                "basePrecision": "0.000001",
                "quotePrecision": "0.01",
                "minTradeQuantity": "0.0001",
                "minTradeAmount": "10",
                "minPricePrecision": "0.01",
                "maxTradeQuantity": "2",
                "maxTradeAmount": "200",
                "category": 1
            }, {
                "name": "ETHUSDT",
                "alias": "ETHUSDT",
                "baseCurrency": "ETH",
                "quoteCurrency": "USDT",
                "basePrecision": "0.0001",
                "quotePrecision": "0.01",
                "minTradeQuantity": "0.0001",
                "minTradeAmount": "10",
                "minPricePrecision": "0.01",
                "maxTradeQuantity": "2",
                "maxTradeAmount": "200",
                "category": 1
            }]
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=BybitAPIOrderBookDataSource.fetch_trading_pairs(
                domain=self.domain,
                throttler=self.throttler,
                time_synchronizer=self.time_synchronnizer,
            ))
        self.assertEqual(2, len(ret))
        self.assertEqual("BTC-USDT", ret[0])
        self.assertEqual("ETH-USDT", ret[1])

    @aioresponses()
    def test_fetch_trading_pairs_exception_raised(self, mock_api):
        BybitAPIOrderBookDataSource._trading_pair_symbol_map = {}

        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=Exception)

        result: Dict[str] = self.async_run_with_timeout(
            self.ob_data_source.fetch_trading_pairs())

        self.assertEqual(0, len(result))

    # LAST TRADED PRICES
    @aioresponses()
    def test_get_last_traded_prices(self, mock_api):
        BybitAPIOrderBookDataSource._trading_pair_symbol_map[
            CONSTANTS.DEFAULT_DOMAIN]["TKN1TKN2"] = "TKN1-TKN2"

        url1 = web_utils.rest_url(path_url=CONSTANTS.LAST_TRADED_PRICE_PATH,
                                  domain=CONSTANTS.DEFAULT_DOMAIN)
        url1 = f"{url1}?symbol={self.ex_trading_pair}"
        regex_url = re.compile(f"^{url1}".replace(".",
                                                  r"\.").replace("?", r"\?"))
        resp = {
            "ret_code": 0,
            "ret_msg": None,
            "result": {
                "symbol": self.ex_trading_pair,
                "price": "50008"
            },
            "ext_code": None,
            "ext_info": None
        }
        mock_api.get(regex_url, body=json.dumps(resp))

        url2 = web_utils.rest_url(path_url=CONSTANTS.LAST_TRADED_PRICE_PATH,
                                  domain=CONSTANTS.DEFAULT_DOMAIN)
        url2 = f"{url2}?symbol=TKN1TKN2"
        regex_url = re.compile(f"^{url2}".replace(".",
                                                  r"\.").replace("?", r"\?"))
        resp = {
            "ret_code": 0,
            "ret_msg": None,
            "result": {
                "symbol": "TKN1TKN2",
                "price": "2050"
            },
            "ext_code": None,
            "ext_info": None
        }
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=BybitAPIOrderBookDataSource.get_last_traded_prices(
                [self.trading_pair, "TKN1-TKN2"]))

        ticker_requests = [(key, value)
                           for key, value in mock_api.requests.items()
                           if key[1].human_repr().startswith(url1)
                           or key[1].human_repr().startswith(url2)]
        request_params = ticker_requests[0][1][0].kwargs["params"]
        self.assertEqual(self.ex_trading_pair, request_params["symbol"])
        request_params = ticker_requests[1][1][0].kwargs["params"]
        self.assertEqual("TKN1TKN2", request_params["symbol"])

        self.assertEqual(ret[self.trading_pair], 50008)
        self.assertEqual(ret["TKN1-TKN2"], 2050)

    # ORDER BOOK SNAPSHOT
    @staticmethod
    def _snapshot_response() -> Dict:
        snapshot = {
            "ret_code": 0,
            "ret_msg": None,
            "result": {
                "time": 1620886105740,
                "bids": [["50005.12", "403.0416"]],
                "asks": [["50006.34", "0.2297"]]
            },
            "ext_code": None,
            "ext_info": None
        }
        return snapshot

    @staticmethod
    def _snapshot_response_processed() -> Dict:
        snapshot_processed = {
            "time": 1620886105740,
            "bids": [["50005.12", "403.0416"]],
            "asks": [["50006.34", "0.2297"]]
        }
        return snapshot_processed

    @aioresponses()
    def test_get_snapshot(self, mock_api):
        url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        snapshot_data = self._snapshot_response()
        mock_api.get(regex_url, body=json.dumps(snapshot_data))

        ret = self.async_run_with_timeout(
            coroutine=self.ob_data_source.get_snapshot(self.trading_pair))

        self.assertEqual(
            ret, self._snapshot_response_processed())  # shallow comparison ok

    @aioresponses()
    def test_get_snapshot_raises(self, mock_api):
        url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, status=500)

        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                coroutine=self.ob_data_source.get_snapshot(self.trading_pair))

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self._snapshot_response()
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=self.ob_data_source.get_new_order_book(
                self.trading_pair))
        bid_entries = list(ret.bid_entries())
        ask_entries = list(ret.ask_entries())
        self.assertEqual(1, len(bid_entries))
        self.assertEqual(50005.12, bid_entries[0].price)
        self.assertEqual(403.0416, bid_entries[0].amount)
        self.assertEqual(int(resp["result"]["time"]), bid_entries[0].update_id)
        self.assertEqual(1, len(ask_entries))
        self.assertEqual(50006.34, ask_entries[0].price)
        self.assertEqual(0.2297, ask_entries[0].amount)
        self.assertEqual(int(resp["result"]["time"]), ask_entries[0].update_id)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_subscribes_to_trades_and_depth(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {
            'topic': 'trade',
            'event': 'sub',
            'symbol': self.ex_trading_pair,
            'params': {
                'binary': 'false',
                'symbolName': self.ex_trading_pair
            },
            'code': '0',
            'msg': 'Success'
        }

        result_subscribe_depth = {
            'topic': 'depth',
            'event': 'sub',
            'symbol': self.ex_trading_pair,
            'params': {
                'binary': 'false',
                'symbolName': self.ex_trading_pair
            },
            'code': '0',
            'msg': 'Success'
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_depth))

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(2, len(sent_subscription_messages))
        expected_trade_subscription = {
            "topic": "trade",
            "event": "sub",
            "symbol": self.ex_trading_pair,
            "params": {
                "binary": False
            }
        }
        self.assertEqual(expected_trade_subscription,
                         sent_subscription_messages[0])
        expected_diff_subscription = {
            "topic": "diffDepth",
            "event": "sub",
            "symbol": self.ex_trading_pair,
            "params": {
                "binary": False
            }
        }
        self.assertEqual(expected_diff_subscription,
                         sent_subscription_messages[1])

        self.assertTrue(
            self._is_logged(
                "INFO",
                f"Subscribed to public order book and trade channels of {self.trading_pair}..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.bybit.bybit_api_order_book_data_source.BybitAPIOrderBookDataSource._time"
    )
    def test_listen_for_subscriptions_sends_ping_message_before_ping_interval_finishes(
            self, time_mock, ws_connect_mock):

        time_mock.side_effect = [
            1000, 1100, 1101, 1102
        ]  # Simulate first ping interval is already due

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {
            'topic': 'trade',
            'event': 'sub',
            'symbol': self.ex_trading_pair,
            'params': {
                'binary': 'false',
                'symbolName': self.ex_trading_pair
            },
            'code': '0',
            'msg': 'Success'
        }

        result_subscribe_depth = {
            'topic': 'depth',
            'event': 'sub',
            'symbol': self.ex_trading_pair,
            'params': {
                'binary': 'false',
                'symbolName': self.ex_trading_pair
            },
            'code': '0',
            'msg': 'Success'
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_depth))

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)
        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        expected_ping_message = {"ping": int(1101 * 1e3)}
        self.assertEqual(expected_ping_message, sent_messages[-1])

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_subscriptions_raises_cancel_exception(
            self, _, ws_connect_mock):
        ws_connect_mock.side_effect = asyncio.CancelledError
        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_subscriptions_logs_exception_details(
            self, sleep_mock, ws_connect_mock):
        sleep_mock.side_effect = asyncio.CancelledError
        ws_connect_mock.side_effect = Exception("TEST ERROR.")

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    def test_listen_for_trades_cancelled_when_listening(self):
        mock_queue = MagicMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.ob_data_source._message_queue[
            CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_trades_logs_exception(self):
        incomplete_resp = {
            "topic": "trade",
            "params": {
                "symbol": self.ex_trading_pair,
                "binary": "false",
                "symbolName": self.ex_trading_pair
            },
            "data": {
                "v": "564265886622695424",
                # "t": 1582001735462,
                "p": "9787.5",
                "q": "0.195009",
                "m": True
            }
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.ob_data_source._message_queue[
            CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public trade updates from exchange"
            ))

    def test_listen_for_trades_successful(self):
        mock_queue = AsyncMock()
        trade_event = {
            "symbol":
            self.ex_trading_pair,
            "symbolName":
            self.ex_trading_pair,
            "topic":
            "trade",
            "params": {
                "realtimeInterval": "24h",
                "binary": "false"
            },
            "data": [{
                "v": "929681067596857345",
                "t": 1625562619577,
                "p": "34924.15",
                "q": "0.00027",
                "m": True
            }],
            "f":
            True,
            "sendTime":
            1626249138535,
            "shared":
            False
        }
        mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()]
        self.ob_data_source._message_queue[
            CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(trade_event["data"][0]["t"], msg.trade_id)

    def test_listen_for_order_book_diffs_cancelled(self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.ob_data_source._message_queue[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_order_book_diffs_logs_exception(self):
        incomplete_resp = {
            # "symbol": self.ex_trading_pair,
            "symbolName":
            self.ex_trading_pair,
            "topic":
            "diffDepth",
            "params": {
                "realtimeInterval": "24h",
                "binary": "false"
            },
            "data": [{
                "e":
                301,
                "s":
                self.ex_trading_pair,
                "t":
                1565600357643,
                "v":
                "112801745_18",
                "b": [["11371.49", "0.0014"], ["11371.12", "0.2"],
                      ["11369.97", "0.3523"], ["11369.96", "0.5"],
                      ["11369.95", "0.0934"], ["11369.94", "1.6809"],
                      ["11369.6", "0.0047"], ["11369.17", "0.3"],
                      ["11369.16", "0.2"], ["11369.04", "1.3203"]],
                "a": [["11375.41", "0.0053"], ["11375.42", "0.0043"],
                      ["11375.48", "0.0052"], ["11375.58", "0.0541"],
                      ["11375.7", "0.0386"], ["11375.71", "2"],
                      ["11377", "2.0691"], ["11377.01", "0.0167"],
                      ["11377.12", "1.5"], ["11377.61", "0.3"]],
                "o":
                0
            }],
            "f":
            False,
            "sendTime":
            1626253839401,
            "shared":
            False
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.ob_data_source._message_queue[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public order book updates from exchange"
            ))

    def test_listen_for_order_book_diffs_successful(self):
        mock_queue = AsyncMock()
        diff_event = {
            "symbol":
            self.ex_trading_pair,
            "symbolName":
            self.ex_trading_pair,
            "topic":
            "diffDepth",
            "params": {
                "realtimeInterval": "24h",
                "binary": "false"
            },
            "data": [{
                "e":
                301,
                "s":
                self.ex_trading_pair,
                "t":
                1565600357643,
                "v":
                "112801745_18",
                "b": [["11371.49", "0.0014"], ["11371.12", "0.2"],
                      ["11369.97", "0.3523"], ["11369.96", "0.5"],
                      ["11369.95", "0.0934"], ["11369.94", "1.6809"],
                      ["11369.6", "0.0047"], ["11369.17", "0.3"],
                      ["11369.16", "0.2"], ["11369.04", "1.3203"]],
                "a": [["11375.41", "0.0053"], ["11375.42", "0.0043"],
                      ["11375.48", "0.0052"], ["11375.58", "0.0541"],
                      ["11375.7", "0.0386"], ["11375.71", "2"],
                      ["11377", "2.0691"], ["11377.01", "0.0167"],
                      ["11377.12", "1.5"], ["11377.61", "0.3"]],
                "o":
                0
            }],
            "f":
            False,
            "sendTime":
            1626253839401,
            "shared":
            False
        }
        mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()]
        self.ob_data_source._message_queue[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(diff_event["data"][0]["t"], msg.update_id)

    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(
            self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.ob_data_source._message_queue[
            CONSTANTS.SNAPSHOT_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.ob_data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))

    @aioresponses()
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_order_book_snapshots_log_exception(
            self, mock_api, sleep_mock):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = ['ERROR', asyncio.CancelledError]
        self.ob_data_source._message_queue[
            CONSTANTS.SNAPSHOT_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()
        sleep_mock.side_effect = [asyncio.CancelledError]
        url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, exception=Exception)

        try:
            self.async_run_with_timeout(
                self.ob_data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public order book updates from exchange"
            ))

    @aioresponses()
    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    def test_listen_for_order_book_snapshots_successful_rest(
            self, mock_api, _):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.TimeoutError
        self.ob_data_source._message_queue[
            CONSTANTS.SNAPSHOT_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()
        url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        snapshot_data = self._snapshot_response()
        mock_api.get(regex_url, body=json.dumps(snapshot_data))

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(int(snapshot_data["result"]["time"]), msg.update_id)

    def test_listen_for_order_book_snapshots_successful_ws(self):
        mock_queue = AsyncMock()
        snapshot_event = {
            "symbol":
            self.ex_trading_pair,
            "symbolName":
            self.ex_trading_pair,
            "topic":
            "diffDepth",
            "params": {
                "realtimeInterval": "24h",
                "binary": "false"
            },
            "data": [{
                "e":
                301,
                "s":
                self.ex_trading_pair,
                "t":
                1565600357643,
                "v":
                "112801745_18",
                "b": [["11371.49", "0.0014"], ["11371.12", "0.2"],
                      ["11369.97", "0.3523"], ["11369.96", "0.5"],
                      ["11369.95", "0.0934"], ["11369.94", "1.6809"],
                      ["11369.6", "0.0047"], ["11369.17", "0.3"],
                      ["11369.16", "0.2"], ["11369.04", "1.3203"]],
                "a": [["11375.41", "0.0053"], ["11375.42", "0.0043"],
                      ["11375.48", "0.0052"], ["11375.58", "0.0541"],
                      ["11375.7", "0.0386"], ["11375.71", "2"],
                      ["11377", "2.0691"], ["11377.01", "0.0167"],
                      ["11377.12", "1.5"], ["11377.61", "0.3"]],
                "o":
                0
            }],
            "f":
            True,
            "sendTime":
            1626253839401,
            "shared":
            False
        }
        mock_queue.get.side_effect = [snapshot_event, asyncio.CancelledError()]
        self.ob_data_source._message_queue[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get(),
                                                            timeout=6)

        self.assertTrue(snapshot_event["data"][0]["t"], msg.update_id)
Beispiel #27
0
class OkxUserStreamDataSourceUnitTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = cls.base_asset + cls.quote_asset
        cls.domain = "com"

        cls.listen_key = "TEST_LISTEN_KEY"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()

        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.mock_time_provider = MagicMock()
        self.mock_time_provider.time.return_value = 1000

        self.time_synchronizer = MagicMock()
        self.time_synchronizer.time.return_value = 1640001112.223

        self.auth = OkxAuth(api_key="TEST_API_KEY",
                            secret_key="TEST_SECRET",
                            passphrase="TEST_PASSPHRASE",
                            time_provider=self.time_synchronizer)

        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = OkxExchange(
            client_config_map=client_config_map,
            okx_api_key="",
            okx_secret_key="",
            okx_passphrase="",
            trading_pairs=[self.trading_pair],
            trading_required=False,
        )
        self.connector._web_assistants_factory._auth = self.auth

        self.data_source = OkxAPIUserStreamDataSource(
            auth=self.auth,
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.resume_test_event = asyncio.Event()

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _create_return_value_and_unlock_test_with_event(self, value):
        self.resume_test_event.set()
        return value

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_subscribes_to_orders_and_balances_events(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        successful_login_response = {"event": "login", "code": "0", "msg": ""}
        result_subscribe_orders = {
            "event": "subscribe",
            "arg": {
                "channel": "account"
            }
        }
        result_subscribe_account = {
            "event": "subscribe",
            "arg": {
                "channel": "orders",
                "instType": "SPOT",
            }
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(successful_login_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_orders))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_account))

        output_queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(3, len(sent_messages))
        expected_login = {
            "op":
            "login",
            "args": [{
                "apiKey": self.auth.api_key,
                "passphrase": self.auth.passphrase,
                'timestamp': '1640001112',
                'sign': 'wEhbGLkjM+fzAclpjd67vGUzbRpxPe4AlLyh6/wVwL4=',
            }]
        }
        self.assertEqual(expected_login, sent_messages[0])
        expected_account_subscription = {
            "op": "subscribe",
            "args": [{
                "channel": "account"
            }]
        }
        self.assertEqual(expected_account_subscription, sent_messages[1])
        expected_orders_subscription = {
            "op": "subscribe",
            "args": [{
                "channel": "orders",
                "instType": "SPOT",
            }]
        }
        self.assertEqual(expected_orders_subscription, sent_messages[2])

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Subscribed to private account and orders channels..."))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_authentication_failure(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        login_response = {
            "event": "error",
            "code": "60009",
            "msg": "Login failed."
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(login_response))

        output_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(output=output_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_empty_payload(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        successful_login_response = {"event": "login", "code": "0", "msg": ""}
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, json.dumps(successful_login_response))
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, "")

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_connection_failed(self, mock_ws):
        mock_ws.side_effect = lambda *arg, **kwars: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR."))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_sends_ping_message_before_ping_interval_finishes(
            self, ws_connect_mock):

        successful_login_response = {"event": "login", "code": "0", "msg": ""}

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.receive.side_effect = [
            WSMessage(type=WSMsgType.TEXT,
                      data=json.dumps(successful_login_response),
                      extra=None),
            asyncio.TimeoutError("Test timeiout"), asyncio.CancelledError
        ]

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        sent_messages = self.mocking_assistant.text_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        expected_ping_message = "ping"
        self.assertEqual(expected_ping_message, sent_messages[0])
class BitmartAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()

        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}"

    @classmethod
    def tearDownClass(cls) -> None:
        for task in asyncio.all_tasks(loop=cls.ev_loop):
            task.cancel()

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task = None
        self.mocking_assistant = NetworkMockingAssistant()
        self.client_config_map = ClientConfigAdapter(ClientConfigMap())

        self.connector = BitmartExchange(
            client_config_map=self.client_config_map,
            bitmart_api_key="",
            bitmart_secret_key="",
            bitmart_memo="",
            trading_pairs=[self.trading_pair],
            trading_required=False,
        )

        self.data_source = BitmartAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory)
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.connector._set_trading_pair_symbol_map(
            bidict({self.ex_trading_pair: self.trading_pair}))

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _order_book_snapshot_example(self):
        return {
            "data": {
                "timestamp":
                1527777538000,
                "buys": [
                    {
                        "amount": "4800.00",
                        "total": "4800.00",
                        "price": "0.000767",
                        "count": "1"
                    },
                    {
                        "amount": "99996475.79",
                        "total": "100001275.79",
                        "price": "0.000201",
                        "count": "1"
                    },
                ],
                "sells": [
                    {
                        "amount": "100.00",
                        "total": "100.00",
                        "price": "0.007000",
                        "count": "1"
                    },
                    {
                        "amount": "6997.00",
                        "total": "7097.00",
                        "price": "1.000000",
                        "count": "1"
                    },
                ]
            }
        }

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    @aioresponses()
    def test_get_last_traded_prices(self, mock_get):
        mock_response: Dict[Any] = {
            "message": "OK",
            "code": 1000,
            "trace": "6e42c7c9-fdc5-461b-8fd1-b4e2e1b9ed57",
            "data": {
                "tickers": [{
                    "symbol":
                    "COINALPHA_HBOT",
                    "last_price":
                    "1.00",
                    "quote_volume_24h":
                    "201477650.88000",
                    "base_volume_24h":
                    "25186.48000",
                    "high_24h":
                    "8800.00",
                    "low_24h":
                    "1.00",
                    "open_24h":
                    "8800.00",
                    "close_24h":
                    "1.00",
                    "best_ask":
                    "0.00",
                    "best_ask_size":
                    "0.00000",
                    "best_bid":
                    "0.00",
                    "best_bid_size":
                    "0.00000",
                    "fluctuation":
                    "-0.9999",
                    "url":
                    "https://www.bitmart.com/trade?symbol=COINALPHA_HBOT"
                }]
            }
        }
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_LAST_TRADING_PRICES_PATH_URL}"
        )
        mock_get.get(regex_url, body=json.dumps(mock_response))

        results = self.ev_loop.run_until_complete(
            asyncio.gather(
                self.data_source.get_last_traded_prices([self.trading_pair])))
        results: Dict[str, Any] = results[0]

        self.assertEqual(results[self.trading_pair], float("1.00"))

    @aioresponses()
    def test_get_new_order_book_successful(self, mock_get):
        mock_response: Dict[str, Any] = self._order_book_snapshot_example()
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps(mock_response))

        results = self.ev_loop.run_until_complete(
            asyncio.gather(
                self.data_source.get_new_order_book(self.trading_pair)))
        order_book: OrderBook = results[0]

        self.assertTrue(type(order_book) == OrderBook)
        self.assertEqual(order_book.snapshot_uid,
                         mock_response["data"]["timestamp"])

        self.assertEqual(mock_response["data"]["timestamp"],
                         order_book.snapshot_uid)
        bids = list(order_book.bid_entries())
        asks = list(order_book.ask_entries())
        self.assertEqual(2, len(bids))
        self.assertEqual(float(mock_response["data"]["buys"][0]["price"]),
                         bids[0].price)
        self.assertEqual(float(mock_response["data"]["buys"][0]["amount"]),
                         bids[0].amount)
        self.assertEqual(mock_response["data"]["timestamp"], bids[0].update_id)
        self.assertEqual(2, len(asks))
        self.assertEqual(float(mock_response["data"]["sells"][0]["price"]),
                         asks[0].price)
        self.assertEqual(float(mock_response["data"]["sells"][0]["amount"]),
                         asks[0].amount)
        self.assertEqual(mock_response["data"]["timestamp"], asks[0].update_id)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(
            self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {
            "event":
            "subscribe",
            "table":
            f"{CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME}:{self.ex_trading_pair}",
        }
        result_subscribe_diffs = {
            "event":
            "subscribe",
            "table":
            f"{CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME}:{self.ex_trading_pair}",
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket(
            websocket_mock=ws_connect_mock.return_value)

        self.assertEqual(2, len(sent_subscription_messages))
        expected_trade_subscription = {
            "op":
            "subscribe",
            "args":
            [f"{CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME}:{self.ex_trading_pair}"]
        }
        self.assertEqual(expected_trade_subscription,
                         sent_subscription_messages[0])
        expected_diff_subscription = {
            "op":
            "subscribe",
            "args":
            [f"{CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME}:{self.ex_trading_pair}"]
        }
        self.assertEqual(expected_diff_subscription,
                         sent_subscription_messages[1])

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Subscribed to public order book and trade channels..."))

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect")
    def test_listen_for_subscriptions_raises_cancel_exception(
            self, mock_ws, _):
        mock_ws.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_subscriptions())
            self.async_run_with_timeout(self.listening_task)

    @patch(
        "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_logs_exception_details(
            self, mock_ws, sleep_mock):
        mock_ws.side_effect = Exception("TEST ERROR.")
        sleep_mock.side_effect = asyncio.CancelledError

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..."
            ))

    def test_subscribe_channels_raises_cancel_exception(self):
        mock_ws = MagicMock()
        mock_ws.send.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source._subscribe_channels(mock_ws))
            self.async_run_with_timeout(self.listening_task)

    def test_subscribe_channels_raises_exception_and_logs_error(self):
        mock_ws = MagicMock()
        mock_ws.send.side_effect = Exception("Test Error")

        with self.assertRaises(Exception):
            self.listening_task = self.ev_loop.create_task(
                self.data_source._subscribe_channels(mock_ws))
            self.async_run_with_timeout(self.listening_task)

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred subscribing to order book trading and delta streams..."
            ))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_compressed_messages_are_correctly_read(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        result_subscribe_trades = {
            "event":
            "subscribe",
            "table":
            f"{CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME}:{self.ex_trading_pair}",
        }
        result_subscribe_diffs = {
            "event":
            "subscribe",
            "table":
            f"{CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME}:{self.ex_trading_pair}",
        }

        trade_event = {
            "table":
            CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME,
            "data": [{
                "symbol": self.ex_trading_pair,
                "price": "162.12",
                "side": "buy",
                "size": "11.085",
                "s_t": 1542337219
            }, {
                "symbol": self.ex_trading_pair,
                "price": "163.12",
                "side": "buy",
                "size": "15",
                "s_t": 1542337238
            }]
        }

        compressed_trade_event = bitmart_utils.compress_ws_message(
            json.dumps(trade_event))

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_trades))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(result_subscribe_diffs))
        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_connect_mock.return_value,
            message=compressed_trade_event,
            message_type=WSMsgType.BINARY)

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_subscriptions())

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        trade_message = self.async_run_with_timeout(
            self.data_source._message_queue[
                self.data_source._trade_messages_queue_key].get())

        self.assertEqual(trade_event, trade_message)

    def test_listen_for_trades(self):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_queue = AsyncMock()

        trade_event = {
            "table":
            CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME,
            "data": [{
                "symbol": self.ex_trading_pair,
                "price": "162.12",
                "side": "buy",
                "size": "11.085",
                "s_t": 1542337219
            }, {
                "symbol": self.ex_trading_pair,
                "price": "163.12",
                "side": "buy",
                "size": "15",
                "s_t": 1542337238
            }]
        }
        mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()]
        self.data_source._message_queue[
            self.data_source._trade_messages_queue_key] = mock_queue

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))

        trade1: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())
        trade2: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(msg_queue.empty())
        self.assertEqual(1542337219, int(trade1.trade_id))
        self.assertEqual(1542337238, int(trade2.trade_id))

    def test_listen_for_trades_raises_cancelled_exception(self):
        mock_queue = MagicMock()
        mock_queue.get.side_effect = asyncio.CancelledError
        self.data_source._message_queue[
            self.data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_trades(self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_trades_logs_exception(self):
        incomplete_resp = {
            "table": CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME,
            "data": [{
                "symbol": self.ex_trading_pair,
            }]
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.data_source._message_queue[
            self.data_source._trade_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public trade updates from exchange"
            ))

    def test_listen_for_order_book_diffs_successful(self):
        mock_queue = AsyncMock()
        snapshot_event = {
            "table":
            CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME,
            "data": [{
                "asks": [["161.96", "7.37567"]],
                "bids": [["161.94", "4.552355"]],
                "symbol": self.ex_trading_pair,
                "ms_t": 1542337219120
            }]
        }
        mock_queue.get.side_effect = [snapshot_event, asyncio.CancelledError]
        self.data_source._message_queue[
            self.data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertEqual(OrderBookMessageType.SNAPSHOT, msg.type)
        self.assertEqual(-1, msg.trade_id)
        self.assertEqual(
            int(snapshot_event["data"][0]["ms_t"]) * 1e-3, msg.timestamp)
        expected_update_id = int(snapshot_event["data"][0]["ms_t"])
        self.assertEqual(expected_update_id, msg.update_id)

        bids = msg.bids
        asks = msg.asks
        self.assertEqual(1, len(bids))
        self.assertEqual(float(snapshot_event["data"][0]["bids"][0][0]),
                         bids[0].price)
        self.assertEqual(float(snapshot_event["data"][0]["bids"][0][1]),
                         bids[0].amount)
        self.assertEqual(expected_update_id, bids[0].update_id)
        self.assertEqual(1, len(asks))
        self.assertEqual(float(snapshot_event["data"][0]["asks"][0][0]),
                         asks[0].price)
        self.assertEqual(float(snapshot_event["data"][0]["asks"][0][1]),
                         asks[0].amount)
        self.assertEqual(expected_update_id, asks[0].update_id)

    def test_listen_for_order_book_snapshots_raises_cancelled_exception(self):
        mock_queue = AsyncMock()
        mock_queue.get.side_effect = asyncio.CancelledError()
        self.data_source._message_queue[
            self.data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_order_book_snapshots_logs_exception(self):
        incomplete_resp = {
            "table": CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME,
            "data": [{
                "symbol": self.ex_trading_pair,
                "ms_t": 1542337219120
            }]
        }

        mock_queue = AsyncMock()
        mock_queue.get.side_effect = [
            incomplete_resp, asyncio.CancelledError()
        ]
        self.data_source._message_queue[
            self.data_source._diff_messages_queue_key] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public order book updates from exchange"
            ))
Beispiel #29
0
class LatokenUserStreamDataSourceUnitTests(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "d8ae67f2-f954-4014-98c8-64b1ac334c64"
        cls.quote_asset = "0c3a106d-bde3-4c13-a26e-3fd2394529e5"
        cls.trading_pair = "ETH-USDT"
        cls.trading_pairs = [cls.trading_pair]
        cls.ex_trading_pair = cls.base_asset + '/' + cls.quote_asset
        cls.domain = "com"

        cls.listen_key = 'ffffffff-ffff-ffff-ffff-ffffffffff'

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task: Optional[asyncio.Task] = None
        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.mock_time_provider = MagicMock()
        self.mock_time_provider.time.return_value = 1000
        self.auth = LatokenAuth(api_key="TEST_API_KEY",
                                secret_key="TEST_SECRET",
                                time_provider=self.mock_time_provider)
        self.time_synchronizer = TimeSynchronizer()
        self.time_synchronizer.add_time_offset_ms_sample(0)
        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = LatokenExchange(client_config_map=client_config_map,
                                         latoken_api_key="",
                                         latoken_api_secret="",
                                         trading_pairs=[],
                                         trading_required=False,
                                         domain=self.domain)
        self.connector._web_assistants_factory._auth = self.auth

        self.data_source = LatokenAPIUserStreamDataSource(
            auth=self.auth,
            trading_pairs=[self.trading_pair],
            connector=self.connector,
            api_factory=self.connector._web_assistants_factory,
            domain=self.domain)

        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.resume_test_event = asyncio.Event()

        self.connector._set_trading_pair_symbol_map(
            bidict({self.ex_trading_pair: self.trading_pair}))

    def tearDown(self) -> None:
        self.listening_task and self.listening_task.cancel()
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def _create_return_value_and_unlock_test_with_event(self, value):
        self.resume_test_event.set()
        return value

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _error_response(self) -> Dict[str, Any]:
        resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"}

        return resp

    def _user_update_event(self):
        # Balance Update, so not the initial balance
        return b'MESSAGE\ndestination:/user/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/v1/account\nmessage-id:9e8188c8-682c-41cd-9a14-722bf6dfd99e\ncontent-length:346\nsubscription:2\n\n{"payload":[{"id":"44d36460-46dc-4828-a17c-63b1a047b054","status":"ACCOUNT_STATUS_ACTIVE","type":"ACCOUNT_TYPE_SPOT","timestamp":1650120265819,"currency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","available":"34.001000000000000000","blocked":"0.999000000000000000","user":"******"}],"nonce":1,"timestamp":1650120265830}\x00'

    def _successfully_subscribed_event(self):
        return b'CONNECTED\nserver:vertx-stomp/3.9.6\nheart-beat:1000,1000\nsession:37a8e962-7fa7-4eab-b163-146eeafdef63\nversion:1.1\n\n\x00 '

    @aioresponses()
    def test_get_listen_key_log_exception(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url,
                     status=400,
                     body=json.dumps(self._error_response()))

        with self.assertRaises(IOError):
            self.async_run_with_timeout(self.data_source._get_listen_key())

    @aioresponses()
    def test_get_listen_key_successful(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            'id': 'ffffffff-ffff-ffff-ffff-ffffffffff',
            'status': 'ACTIVE',
            'role': 'INVESTOR',
            'email': '*****@*****.**',
            'phone': '',
            'authorities': [],
            'forceChangePassword': None,
            'authType': 'API_KEY',
            'socials': []
        }

        mock_api.get(regex_url, body=json.dumps(mock_response))

        result: str = self.async_run_with_timeout(
            self.data_source._get_listen_key())

        self.assertEqual(self.listen_key, result)

    @aioresponses()
    def test_ping_listen_key_log_exception(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url,
                     status=400,
                     body=json.dumps(self._error_response()))

        self.data_source._current_listen_key = self.listen_key
        result: bool = self.async_run_with_timeout(
            self.data_source._ping_listen_key())

        self.assertTrue(
            self._is_logged(
                "WARNING",
                f"Failed to refresh the listen key {self.listen_key}: {self._error_response()}"
            ))
        self.assertFalse(result)

    @aioresponses()
    def test_ping_listen_key_successful(self, mock_api):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {
            'id': 'ffffffff-ffff-ffff-ffff-ffffffffff',
            'status': 'ACTIVE',
            'role': 'INVESTOR',
            'email': '*****@*****.**',
            'phone': '',
            'authorities': [],
            'forceChangePassword': None,
            'authType': 'API_KEY',
            'socials': []
        }

        mock_api.get(regex_url, body=json.dumps(mock_response))

        self.data_source._current_listen_key = self.listen_key
        result: bool = self.async_run_with_timeout(
            self.data_source._ping_listen_key())
        self.assertTrue(result)

    @patch(
        "hummingbot.connector.exchange.latoken.latoken_api_user_stream_data_source.LatokenAPIUserStreamDataSource"
        "._ping_listen_key",
        new_callable=AsyncMock)
    def test_manage_listen_key_task_loop_keep_alive_failed(
            self, mock_ping_listen_key):
        mock_ping_listen_key.side_effect = (
            lambda *args, **kwargs: self.
            _create_return_value_and_unlock_test_with_event(False))

        self.data_source._current_listen_key = self.listen_key

        # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached
        self.data_source._last_listen_key_ping_ts = 0

        self.listening_task = self.ev_loop.create_task(
            self.data_source._manage_listen_key_task_loop())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("ERROR", "Error occurred renewing listen key ..."))
        self.assertIsNone(self.data_source._current_listen_key)
        self.assertFalse(
            self.data_source._listen_key_initialized_event.is_set())

    @patch(
        "hummingbot.connector.exchange.latoken.latoken_api_user_stream_data_source.LatokenAPIUserStreamDataSource."
        "_ping_listen_key",
        new_callable=AsyncMock)
    def test_manage_listen_key_task_loop_keep_alive_successful(
            self, mock_ping_listen_key):
        mock_ping_listen_key.side_effect = (
            lambda *args, **kwargs: self.
            _create_return_value_and_unlock_test_with_event(True))

        # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached
        self.data_source._current_listen_key = self.listen_key
        self.data_source._listen_key_initialized_event.set()
        self.data_source._last_listen_key_ping_ts = 0

        self.listening_task = self.ev_loop.create_task(
            self.data_source._manage_listen_key_task_loop())

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged("INFO",
                            f"Refreshed listen key {self.listen_key}."))
        self.assertGreater(self.data_source._last_listen_key_ping_ts, 0)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_get_listen_key_successful_with_user_update_event(
            self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            'id': 'ffffffff-ffff-ffff-ffff-ffffffffff',
            'status': 'ACTIVE',
            'role': 'INVESTOR',
            'email': '*****@*****.**',
            'phone': '',
            'authorities': [],
            'forceChangePassword': None,
            'authType': 'API_KEY',
            'socials': []
        }

        mock_api.get(regex_url, body=json.dumps(mock_response))

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value,
            self._successfully_subscribed_event(),
            message_type=aiohttp.WSMsgType.BINARY)
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value,
            self._user_update_event(),
            message_type=aiohttp.WSMsgType.BINARY)

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        msg = self.async_run_with_timeout(msg_queue.get())
        self.assertTrue(msg, self._user_update_event)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_does_not_queue_empty_payload(
            self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            'id': 'ffffffff-ffff-ffff-ffff-ffffffffff',
            'status': 'ACTIVE',
            'role': 'INVESTOR',
            'email': '*****@*****.**',
            'phone': '',
            'authorities': [],
            'forceChangePassword': None,
            'authType': 'API_KEY',
            'socials': []
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, "")

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

        self.assertEqual(0, msg_queue.qsize())

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_connection_failed(self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            'id': 'ffffffff-ffff-ffff-ffff-ffffffffff',
            'status': 'ACTIVE',
            'role': 'INVESTOR',
            'email': '*****@*****.**',
            'phone': '',
            'authorities': [],
            'forceChangePassword': None,
            'authType': 'API_KEY',
            'socials': []
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        mock_ws.side_effect = lambda *arg, **kwars: self._create_exception_and_unlock_test_with_event(
            Exception("TEST ERROR."))

        msg_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_iter_message_throws_exception(
            self, mock_api, mock_ws):
        url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL,
                                         domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_response = {
            'id': 'ffffffff-ffff-ffff-ffff-ffffffffff',
            'status': 'ACTIVE',
            'role': 'INVESTOR',
            'email': '*****@*****.**',
            'phone': '',
            'authorities': [],
            'forceChangePassword': None,
            'authType': 'API_KEY',
            'socials': []
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.receive.side_effect = (
            lambda *args, **kwargs: self.
            _create_exception_and_unlock_test_with_event(
                Exception("TEST ERROR")))
        mock_ws.close.return_value = None

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(msg_queue))

        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error while listening to user stream. Retrying after 5 seconds..."
            ))
class BitmexPerpetualDerivativeUnitTest(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp()

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.symbol = f"{cls.base_asset}{cls.quote_asset}"
        cls.domain = CONSTANTS.TESTNET_DOMAIN
        cls.listen_key = "TEST_LISTEN_KEY"
        cls.ev_loop = asyncio.get_event_loop()
        utils.TRADING_PAIR_SIZE_CURRENCY[
            "COINALPHAHBOT"] = utils.TRADING_PAIR_SIZE("COINALPHA", True, 1)
        utils.TRADING_PAIR_SIZE_CURRENCY["XBTUSD"] = utils.TRADING_PAIR_SIZE(
            "USD", False, None)

    def setUp(self) -> None:
        super().setUp()

        self.log_records = []

        self.ws_sent_messages = []
        self.ws_incoming_messages = asyncio.Queue()
        self.resume_test_event = asyncio.Event()
        self.client_config_map = ClientConfigAdapter(ClientConfigMap())

        self.exchange = BitmexPerpetualDerivative(
            client_config_map=self.client_config_map,
            bitmex_perpetual_api_key="testAPIKey",
            bitmex_perpetual_api_secret="testSecret",
            trading_pairs=[self.trading_pair],
            domain=self.domain,
        )
        BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {
            self.symbol: self.trading_pair,
            "XBTUSD": "XBT-USD"
        }
        self.exchange._set_current_timestamp(1640780000)
        self.exchange.logger().setLevel(1)
        self.exchange.logger().addHandler(self)
        self.exchange._client_order_tracker.logger().setLevel(1)
        self.exchange._client_order_tracker.logger().addHandler(self)
        self.exchange._trading_pair_to_size_type[
            "COINALPHA-HBOT"] = utils.TRADING_PAIR_SIZE("COINALPHA", True, 1)
        self.exchange._trading_pair_to_size_type[
            "XBT-USD"] = utils.TRADING_PAIR_SIZE("USD", False, None)
        self.mocking_assistant = NetworkMockingAssistant()
        self.test_task: Optional[asyncio.Task] = None
        self.resume_test_event = asyncio.Event()
        self._initialize_event_loggers()
        BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {
            self.domain:
            bidict({
                self.symbol: self.trading_pair,
                "XBTUSD": "XBT-USD"
            })
        }

    def tearDown(self) -> None:
        self.test_task and self.test_task.cancel()
        BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def _initialize_event_loggers(self):
        self.buy_order_completed_logger = EventLogger()
        self.sell_order_completed_logger = EventLogger()
        self.order_cancelled_logger = EventLogger()
        self.order_filled_logger = EventLogger()
        self.funding_payment_completed_logger = EventLogger()

        events_and_loggers = [
            (MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger),
            (MarketEvent.SellOrderCompleted, self.sell_order_completed_logger),
            (MarketEvent.OrderCancelled, self.order_cancelled_logger),
            (MarketEvent.OrderFilled, self.order_filled_logger),
            (MarketEvent.FundingPaymentCompleted,
             self.funding_payment_completed_logger)
        ]

        for event, logger in events_and_loggers:
            self.exchange.add_listener(event, logger)

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    def _return_calculation_and_set_done_event(self, calculation: Callable,
                                               *args, **kwargs):
        if self.resume_test_event.is_set():
            raise asyncio.CancelledError
        self.resume_test_event.set()
        return calculation(*args, **kwargs)

    def _get_position_risk_api_endpoint_single_position_list(
            self) -> List[Dict[str, Any]]:
        positions = [{
            "symbol": self.symbol,
            "currentQty": 1,
            "avgEntryPrice": 10,
            "unrealisedPnl": 1,
            "leverage": 1,
            "openOrderBuyQty": 1
        }]
        return positions

    def _get_position_risk_api_endpoint_single_position_closed_list(
            self) -> List[Dict[str, Any]]:
        positions = [{
            "symbol": self.symbol,
            "currentQty": 0,
            "avgEntryPrice": 10,
            "unrealisedPnl": 1,
            "leverage": 1,
            "openOrderBuyQty": 1
        }]
        return positions

    def _get_position_update_ws_event_single_position_dict(
            self) -> Dict[str, Any]:
        account_update = {
            "table":
            "position",
            "data": [{
                "symbol": self.symbol,
                "currentQty": 1,
                "avgEntryPrice": 10,
                "unrealisedPnl": 1,
                "leverage": 1,
                "openOrderBuyQty": 1
            }],
        }
        return account_update

    def _get_income_history_dict(self) -> List:
        income_history = [{
            "income": 1,
            "symbol": self.symbol,
            "time": self.start_timestamp,
        }]
        return income_history

    def _get_funding_info_dict(self) -> Dict[str, Any]:
        funding_info = {
            "lastPrice": 100.0,
            "fairPrice": 101.1,
            "fundingTimestamp": "2022-02-11T09:30:30.000Z",
            "fundingRate": 0.05,
        }
        return funding_info

    def _get_trading_pair_symbol_map(self) -> Dict[str, str]:
        trading_pair_symbol_map = {
            self.symbol: f"{self.base_asset}-{self.quote_asset}"
        }
        return trading_pair_symbol_map

    def _get_exchange_info_mock_response(
            self,
            margin_asset: str = "HBOT",
            min_order_size: float = 1,
            min_price_increment: float = 2,
            min_notional_size: float = 4,
            max_order_size: float = 1000) -> Dict[str, Any]:
        mocked_exchange_info = [
            {
                "symbol": self.symbol,
                "typ": "FFWCSX",
                "rootSymbol": self.base_asset,
                "quoteCurrency": self.quote_asset,
                "maxOrderQty": max_order_size,
                "lotSize":
                min_order_size,  # this gets divided by the multiplier, which is set to 1
                "settlCurrency": margin_asset,
                "tickSize": min_price_increment
            },
            {
                "symbol": "XBTUSD",
                "typ": "FFWCSX",
                "rootSymbol": "XBT",
                "quoteCurrency": "USD",
                "lotSize": 100,
                "tickSize": 0.5,
                "settlCurrency": "XBt",
                "maxOrderQty": 10000000
            },
        ]
        return mocked_exchange_info

    @aioresponses()
    def test_existing_account_position_detected_on_positions_update(
            self, req_mock):
        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.trading_pair.replace("-", ""), self.symbol)

    @aioresponses()
    def test_account_position_updated_on_positions_update(self, req_mock):
        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 1)

        positions[0]["currentQty"] = "2"
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 2)

    @aioresponses()
    def test_new_account_position_detected_on_positions_update(self, req_mock):
        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_closed_list(
        )
        req_mock.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        self.assertEqual(len(self.exchange.account_positions), 0)

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        self.assertEqual(len(self.exchange.account_positions), 1)

    @aioresponses()
    def test_closed_account_position_removed_on_positions_update(
            self, req_mock):
        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        self.assertEqual(len(self.exchange.account_positions), 1)

        positions[0]["currentQty"] = "0"
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        self.assertEqual(len(self.exchange.account_positions), 0)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_new_account_position_detected_on_stream_event(
            self, mock_api, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.ev_loop.create_task(self.exchange._user_stream_tracker.start())

        self.assertEqual(len(self.exchange.account_positions), 0)

        account_update = self._get_position_update_ws_event_single_position_dict(
        )
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(account_update))

        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        positions = self._get_position_risk_api_endpoint_single_position_list()
        mock_api.get(regex_url, body=json.dumps(positions))

        self.ev_loop.create_task(self.exchange._user_stream_event_listener())
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(len(self.exchange.account_positions), 1)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_account_position_updated_on_stream_event(self, mock_api,
                                                      ws_connect_mock):
        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        positions = self._get_position_risk_api_endpoint_single_position_list()
        mock_api.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.ev_loop.create_task(self.exchange._user_stream_tracker.start())

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 1)

        account_update = self._get_position_update_ws_event_single_position_dict(
        )
        account_update["data"][0]["currentQty"] = 2
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(account_update))

        self.ev_loop.create_task(self.exchange._user_stream_event_listener())
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 2)

    @aioresponses()
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_closed_account_position_removed_on_stream_event(
            self, mock_api, ws_connect_mock):
        url = web_utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        positions = self._get_position_risk_api_endpoint_single_position_list()
        mock_api.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.async_run_with_timeout(task)

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.ev_loop.create_task(self.exchange._user_stream_tracker.start())

        self.assertEqual(len(self.exchange.account_positions), 1)

        account_update = self._get_position_update_ws_event_single_position_dict(
        )
        account_update["data"][0]["currentQty"] = 0
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(account_update))

        self.ev_loop.create_task(self.exchange._user_stream_event_listener())
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertEqual(len(self.exchange.account_positions), 0)

    @aioresponses()
    def test_set_position_mode_initial_mode_is_none(self, mock_api):
        self.assertIsNone(self.exchange.position_mode)

        self.exchange.set_position_mode(PositionMode.ONEWAY)

        self.assertEqual(PositionMode.ONEWAY, self.exchange.position_mode)

    @aioresponses()
    def test_update_trading_rules(self, mock_api):
        self.exchange._trading_pairs.append("XBT-USD")
        url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response: Dict[str, Any] = [
            {
                "symbol": "COINALPHAHBOT",
                "rootSymbol": "COINALPHA",
                "quoteCurrency": "HBOT",
                "settlCurrency": "HBOT",
                "lotSize": 1.0,
                "tickSize": 0.0001,
                "minProvideSize": 0.001,
                "maxOrderQty": 1000000
            },
            {
                "symbol": "XBTUSD",
                "rootSymbol": "XBT",
                "quoteCurrency": "USD",
                "lotSize": 100,
                "tickSize": 0.5,
                "settlCurrency": "XBt",
                "maxOrderQty": 10000000
            },
        ]
        mock_api.get(regex_url, status=200, body=json.dumps(mock_response))
        url_2 = web_utils.rest_url(CONSTANTS.TICKER_PRICE_URL,
                                   domain=self.domain)
        regex_url_2 = re.compile(f"^{url_2}".replace(".", r"\.").replace(
            "?", r"\?"))
        mock_response_2: List[Dict[str, Any]] = [{
            "symbol": "COINALPHAHBOT",
            "lastPrice": 1000.0
        }]
        mock_api.get(regex_url_2, body=json.dumps(mock_response_2))
        url_3 = web_utils.rest_url(CONSTANTS.TICKER_PRICE_URL,
                                   domain=self.domain)
        regex_url_3 = re.compile(f"^{url_3}".replace(".", r"\.").replace(
            "?", r"\?"))
        mock_response_3: List[Dict[str, Any]] = [{
            "symbol": "XBTUSD",
            "lastPrice": 1000.0
        }]
        mock_api.get(regex_url_3, body=json.dumps(mock_response_3))
        self.async_run_with_timeout(self.exchange._update_trading_rules())
        self.assertTrue(len(self.exchange._trading_rules) > 0)
        quant_amount = self.exchange.quantize_order_amount(
            'XBT-USD', Decimal('0.00001'), Decimal('10000'))
        self.assertEqual(quant_amount, Decimal('0'))
        quant_price = self.exchange.quantize_order_price(
            'COINALPHA-HBOT', Decimal('1'))
        self.assertEqual(quant_price, Decimal('1.0'))
        quant_amount = self.exchange.quantize_order_amount(
            'COINALPHA-HBOT', Decimal('0.00001'))
        self.assertEqual(quant_amount, Decimal('0'))
        self.exchange._trading_pairs.remove("XBT-USD")

    def test_format_trading_rules(self):
        margin_asset = self.quote_asset
        min_order_size = 1
        min_price_increment = 2
        min_base_amount_increment = 1
        mocked_response = self._get_exchange_info_mock_response()

        task = self.ev_loop.create_task(
            self.exchange._format_trading_rules(mocked_response))
        trading_rules = self.async_run_with_timeout(task)

        self.assertEqual(1, len(trading_rules))

        trading_rule = trading_rules[0]

        self.assertEqual(min_order_size, trading_rule.min_order_size)
        self.assertEqual(min_price_increment, trading_rule.min_price_increment)
        self.assertEqual(min_base_amount_increment,
                         trading_rule.min_base_amount_increment)
        self.assertEqual(margin_asset, trading_rule.buy_order_collateral_token)
        self.assertEqual(margin_asset,
                         trading_rule.sell_order_collateral_token)

    def test_get_collateral_token(self):
        margin_asset = self.quote_asset
        mocked_response = self._get_exchange_info_mock_response(margin_asset)
        task = self.ev_loop.create_task(
            self.exchange._format_trading_rules(mocked_response))
        trading_rules = self.async_run_with_timeout(task)
        self.exchange._trading_rules[self.trading_pair] = trading_rules[0]

        self.assertEqual(
            margin_asset,
            self.exchange.get_buy_collateral_token(self.trading_pair))
        self.assertEqual(
            margin_asset,
            self.exchange.get_sell_collateral_token(self.trading_pair))

    def test_buy_order_fill_event_takes_fee_from_update_event(self):
        self.exchange.start_tracking_order(
            order_side=TradeType.BUY,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        partial_fill = {
            "table":
            "order",
            "data": [{
                "clOrdID": "OID1",
                "leavesQty": 0.5,
                "ordStatus": "PartiallyFilled",
                "avgPx": 9999,
            }]
        }
        logger_len = len(self.order_filled_logger.event_log)

        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: partial_fill)

        self.exchange._user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertEqual(logger_len + 1,
                         len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0]
        self.assertEqual(Decimal("0.0002"), fill_event.trade_fee.percent)

        complete_fill = {
            "table":
            "order",
            "data": [{
                "clOrdID": "OID1",
                "leavesQty": 0.0,
                "ordStatus": "Filled",
                "avgPx": 9999,
            }]
        }

        self.resume_test_event = asyncio.Event()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: complete_fill)

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertEqual(logger_len + 2,
                         len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1]
        self.assertEqual(Decimal("0.0002"), fill_event.trade_fee.percent)

        self.assertEqual(1, len(self.buy_order_completed_logger.event_log))

    def test_sell_order_fill_event_takes_fee_from_update_event(self):
        self.exchange.start_tracking_order(
            order_side=TradeType.SELL,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        partial_fill = {
            "table":
            "order",
            "data": [{
                "clOrdID": "OID1",
                "leavesQty": 0.5,
                "ordStatus": "PartiallyFilled",
                "avgPx": 10001,
            }]
        }
        logger_len = len(self.order_filled_logger.event_log)
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: partial_fill)

        self.exchange._user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())
        self.assertEqual(logger_len + 1,
                         len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0]
        self.assertEqual(Decimal("0.0002"), fill_event.trade_fee.percent)

        complete_fill = {
            "table":
            "order",
            "data": [{
                "clOrdID": "OID1",
                "leavesQty": 0.0,
                "ordStatus": "Filled",
                "avgPx": 10001,
            }]
        }

        self.resume_test_event = asyncio.Event()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: complete_fill)

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertEqual(logger_len + 2,
                         len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1]
        self.assertEqual(Decimal("0.0002"), fill_event.trade_fee.percent)

        self.assertEqual(1, len(self.sell_order_completed_logger.event_log))

    def test_order_event_with_cancelled_status_marks_order_as_cancelled(self):
        self.exchange.start_tracking_order(
            order_side=TradeType.SELL,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        order = self.exchange.in_flight_orders.get("OID1")

        partial_fill = {
            "table": "order",
            "data": [{
                "clOrdID": "OID1",
                "ordStatus": "Canceled",
                "leavesQty": 1
            }]
        }

        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: partial_fill)

        self.exchange._user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertEqual(1, len(self.order_cancelled_logger.event_log))

        event = self.order_cancelled_logger.event_log[0]

        self.assertEqual(event.order_id, order.client_order_id)

    def test_user_stream_event_listener_raises_cancelled_error(self):
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = asyncio.CancelledError

        self.exchange._user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.assertRaises(asyncio.CancelledError, self.async_run_with_timeout,
                          self.test_task)

    @aioresponses()
    @patch(
        "hummingbot.connector.derivative.bitmex_perpetual.bitmex_perpetual_derivative."
        "BitmexPerpetualDerivative.current_timestamp")
    def test_update_order_status_successful(self, req_mock, mock_timestamp):
        self.exchange._last_poll_timestamp = 0
        mock_timestamp.return_value = 1

        self.exchange.start_tracking_order(
            order_side=TradeType.SELL,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        order = [{
            "clOrdID": "OID1",
            "leavesQty": 0.5,
            "ordStatus": "PartiallyFilled",
            "avgPx": 10001,
        }]

        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        req_mock.get(regex_url, body=json.dumps(order))

        self.async_run_with_timeout(self.exchange._update_order_status())

        in_flight_orders = self.exchange._in_flight_orders

        self.assertTrue("OID1" in in_flight_orders)

        self.assertEqual("OID1", in_flight_orders["OID1"].client_order_id)
        self.assertEqual(f"{self.base_asset}-{self.quote_asset}",
                         in_flight_orders["OID1"].trading_pair)
        self.assertEqual(OrderType.LIMIT, in_flight_orders["OID1"].order_type)
        self.assertEqual(TradeType.SELL, in_flight_orders["OID1"].trade_type)
        self.assertEqual(10000, in_flight_orders["OID1"].price)
        self.assertEqual(1, in_flight_orders["OID1"].amount)
        self.assertEqual(BitmexPerpetualOrderStatus.PartiallyFilled,
                         in_flight_orders["OID1"].state)
        self.assertEqual(1, in_flight_orders["OID1"].leverage)
        self.assertEqual(PositionAction.OPEN,
                         in_flight_orders["OID1"].position)

        # Processing an order update should not impact trade fill information
        self.assertEqual(Decimal("0.5"),
                         in_flight_orders["OID1"].executed_amount_base)
        self.assertEqual(Decimal("5000.5"),
                         in_flight_orders["OID1"].executed_amount_quote)

    @aioresponses()
    @patch(
        "hummingbot.connector.derivative.bitmex_perpetual.bitmex_perpetual_derivative."
        "BitmexPerpetualDerivative.current_timestamp")
    def test_update_order_status_failure_old_order(self, req_mock,
                                                   mock_timestamp):
        self.exchange._last_poll_timestamp = 0
        mock_timestamp.return_value = 1

        self.exchange.start_tracking_order(
            order_side=TradeType.SELL,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=0,
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        order = [{
            "clOrdID": "OID2",
            "leavesQty": 0.5,
            "ordStatus": "PartiallyFilled",
            "avgPx": 10001,
        }]

        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        req_mock.get(regex_url, body=json.dumps(order))

        self.async_run_with_timeout(self.exchange._update_order_status())
        self.assertEqual(len(self.exchange.in_flight_orders), 0)

    @aioresponses()
    def test_set_leverage_successful(self, req_mock):
        trading_pair = f"{self.base_asset}-{self.quote_asset}"
        leverage = 21

        self.exchange.set_leverage(trading_pair, leverage)

        self.assertEqual(self.exchange._leverage[trading_pair], 21)

    @aioresponses()
    @patch(
        "hummingbot.connector.derivative.bitmex_perpetual.bitmex_perpetual_derivative."
        "LatchingEventResponder.wait_for_completion")
    def test_cancel_all_successful(self, mocked_api, mock_wait):
        mock_wait.return_value = True
        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        cancel_response = {"code": 200, "msg": "success"}
        mocked_api.delete(regex_url, body=json.dumps(cancel_response))

        self.exchange.start_tracking_order(
            order_side=TradeType.BUY,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.CLOSE,
        )

        self.exchange.start_tracking_order(
            order_side=TradeType.SELL,
            client_order_id="OID2",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        self.assertTrue("OID1" in self.exchange._in_flight_orders)
        self.assertTrue("OID2" in self.exchange._in_flight_orders)

        cancellation_results = self.async_run_with_timeout(
            self.exchange.cancel_all(timeout_seconds=1))

        order_cancelled_events = self.order_cancelled_logger.event_log

        self.assertEqual(0, len(order_cancelled_events))
        self.assertEqual(2, len(cancellation_results))
        self.assertEqual("OID1", cancellation_results[0].order_id)
        self.assertEqual("OID2", cancellation_results[1].order_id)

    @aioresponses()
    def test_cancel_unknown_new_order(self, req_mock):
        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        cancel_response = {
            "error": {
                "message": "order not found",
                "name": "Not Found"
            }
        }
        req_mock.delete(regex_url,
                        status=400,
                        body=json.dumps(cancel_response))

        self.exchange.start_tracking_order(
            order_side=TradeType.BUY,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=time.time(),
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        tracked_order = self.exchange.in_flight_orders.get("OID1")
        tracked_order.state = BitmexPerpetualOrderStatus.New

        self.assertTrue("OID1" in self.exchange._in_flight_orders)

        try:
            self.async_run_with_timeout(self.exchange.cancel_order("OID1"))
        except Exception as e:
            self.assertEqual(
                str(e),
                f"order {tracked_order.client_order_id} does not yet exist on the exchange and could not be cancelled."
            )

    @aioresponses()
    def test_cancel_unknown_old_order(self, req_mock):
        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        cancel_response = {
            "error": {
                "message": "order not found",
                "name": "Not Found"
            }
        }
        req_mock.delete(regex_url,
                        status=400,
                        body=json.dumps(cancel_response))

        self.exchange.start_tracking_order(
            order_side=TradeType.BUY,
            client_order_id="OID1",
            order_type=OrderType.LIMIT,
            created_at=0.0,
            hash="8886774",
            trading_pair=self.trading_pair,
            price=Decimal("10000"),
            amount=Decimal("1"),
            leverage=1,
            position=PositionAction.OPEN,
        )

        tracked_order = self.exchange.in_flight_orders.get("OID1")
        tracked_order.state = BitmexPerpetualOrderStatus.New

        self.assertTrue("OID1" in self.exchange._in_flight_orders)
        try:
            cancellation_result = self.async_run_with_timeout(
                self.exchange.cancel_order("OID1"))
        except Exception:
            pass

        self.assertFalse(cancellation_result)

        self.assertTrue("OID1" not in self.exchange._in_flight_orders)

    @aioresponses()
    def test_create_order_successful(self, req_mock):
        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        create_response = {
            "updateTime": int(self.start_timestamp),
            "ordStatus": "New",
            "orderID": "8886774"
        }
        req_mock.post(regex_url, body=json.dumps(create_response))

        margin_asset = self.quote_asset
        mocked_response = self._get_exchange_info_mock_response(margin_asset)
        trading_rules = self.async_run_with_timeout(
            self.exchange._format_trading_rules(mocked_response))
        self.exchange._trading_rules[self.trading_pair] = trading_rules[0]

        self.async_run_with_timeout(
            self.exchange.execute_buy(order_id="OID1",
                                      trading_pair=self.trading_pair,
                                      amount=Decimal("100"),
                                      order_type=OrderType.LIMIT,
                                      position_action=PositionAction.OPEN,
                                      price=Decimal("10000")))

        self.assertTrue("OID1" in self.exchange._in_flight_orders)

    @aioresponses()
    def test_create_order_exception(self, req_mock):
        url = web_utils.rest_url(CONSTANTS.ORDER_URL, domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        create_response = {
            "updateTime": int(self.start_timestamp),
            "ordStatus": "Canceled",
            "orderID": "8886774"
        }

        req_mock.post(regex_url, body=json.dumps(create_response))

        margin_asset = self.quote_asset
        mocked_response = self._get_exchange_info_mock_response(margin_asset)
        trading_rules = self.async_run_with_timeout(
            self.exchange._format_trading_rules(mocked_response))
        self.exchange._trading_rules[self.trading_pair] = trading_rules[0]

        self.async_run_with_timeout(
            self.exchange.execute_sell(order_id="OID1",
                                       trading_pair=self.trading_pair,
                                       amount=Decimal("10000"),
                                       order_type=OrderType.LIMIT,
                                       position_action=PositionAction.OPEN,
                                       price=Decimal("1010")))

        self.assertTrue("OID1" not in self.exchange._in_flight_orders)

    def test_restore_tracking_states_only_registers_open_orders(self):
        orders = []
        orders.append(
            InFlightOrder(
                client_order_id="OID1",
                exchange_order_id="EOID1",
                trading_pair=self.trading_pair,
                order_type=OrderType.LIMIT,
                trade_type=TradeType.BUY,
                amount=Decimal("1000.0"),
                price=Decimal("1.0"),
                creation_timestamp=1640001112.223,
            ))
        orders.append(
            InFlightOrder(client_order_id="OID2",
                          exchange_order_id="EOID2",
                          trading_pair=self.trading_pair,
                          order_type=OrderType.LIMIT,
                          trade_type=TradeType.BUY,
                          amount=Decimal("1000.0"),
                          price=Decimal("1.0"),
                          creation_timestamp=1640001112.223,
                          initial_state=BitmexPerpetualOrderStatus.Canceled))
        orders.append(
            InFlightOrder(client_order_id="OID3",
                          exchange_order_id="EOID3",
                          trading_pair=self.trading_pair,
                          order_type=OrderType.LIMIT,
                          trade_type=TradeType.BUY,
                          amount=Decimal("1000.0"),
                          price=Decimal("1.0"),
                          creation_timestamp=1640001112.223,
                          initial_state=BitmexPerpetualOrderStatus.Filled))
        orders.append(
            InFlightOrder(client_order_id="OID4",
                          exchange_order_id="EOID4",
                          trading_pair=self.trading_pair,
                          order_type=OrderType.LIMIT,
                          trade_type=TradeType.BUY,
                          amount=Decimal("1000.0"),
                          price=Decimal("1.0"),
                          creation_timestamp=1640001112.223,
                          initial_state=BitmexPerpetualOrderStatus.FAILURE))

        tracking_states = {
            order.client_order_id: order.to_json()
            for order in orders
        }

        self.exchange.restore_tracking_states(tracking_states)

        self.assertIn("OID1", self.exchange.in_flight_orders)
        self.assertNotIn("OID2", self.exchange.in_flight_orders)
        self.assertNotIn("OID3", self.exchange.in_flight_orders)
        self.assertNotIn("OID4", self.exchange.in_flight_orders)

    @patch("hummingbot.connector.utils.get_tracking_nonce_low_res")
    def test_client_order_id_on_order(self, mocked_nonce):
        mocked_nonce.return_value = 4

        result = self.exchange.buy(
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("2"),
            position_action="OPEN",
        )
        expected_client_order_id = get_new_client_order_id(
            is_buy=True,
            trading_pair=self.trading_pair,
            hbot_order_id_prefix=CONSTANTS.BROKER_ID,
            max_id_len=CONSTANTS.MAX_ORDER_ID_LEN,
        )

        self.assertEqual(result, expected_client_order_id)

        result = self.exchange.sell(
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("2"),
            position_action="OPEN",
        )
        expected_client_order_id = get_new_client_order_id(
            is_buy=False,
            trading_pair=self.trading_pair,
            hbot_order_id_prefix=CONSTANTS.BROKER_ID,
            max_id_len=CONSTANTS.MAX_ORDER_ID_LEN,
        )

        self.assertEqual(result, expected_client_order_id)

    @aioresponses()
    def test_update_balances(self, mock_api):
        url = web_utils.rest_url(CONSTANTS.ACCOUNT_INFO_URL,
                                 domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        response = [{
            "currency": "USDT",
            "amount": 100000000,
            "pendingCredit": 2000000,
            "pendingDebit": 0
        }, {
            "currency": "XBT",
            "amount": 1000000000,
            "pendingCredit": 20000000,
            "pendingDebit": 0
        }]

        mock_api.get(regex_url, body=json.dumps(response))

        url_2 = web_utils.rest_url(CONSTANTS.TOKEN_INFO_URL,
                                   domain=self.domain)
        regex_url_2 = re.compile(f"^{url_2}".replace(".", r"\.").replace(
            "?", r"\?"))

        response_2 = [{
            "asset": "USDT",
            "scale": 6
        }, {
            "asset": "XBT",
            "scale": 8,
        }]

        mock_api.get(regex_url_2, body=json.dumps(response_2))
        self.async_run_with_timeout(self.exchange._update_balances())

        available_balances = self.exchange.available_balances
        total_balances = self.exchange.get_all_balances()

        self.assertEqual(Decimal("98"), available_balances["USDT"])
        self.assertEqual(Decimal("9.8"), available_balances["XBT"])
        self.assertEqual(Decimal("100"), total_balances["USDT"])
        self.assertEqual(Decimal("10"), total_balances["XBT"])

    def test_adjust_quote_based_amounts(self):
        self.exchange._trading_pairs.append("XBT-USD")
        mocked_response = self._get_exchange_info_mock_response()

        task = self.ev_loop.create_task(
            self.exchange._format_trading_rules(mocked_response))
        trading_rules = self.async_run_with_timeout(task)
        self.exchange._trading_rules["XBT-USD"] = trading_rules[1]
        base, quote = self.exchange.adjust_quote_based_amounts(
            "XBT-USD", Decimal('1000'), Decimal('10'))
        self.exchange._trading_pairs.remove("XBT-USD")