class NdaxAPIOrderBookDataSourceUnitTests(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.instrument_id = 1

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

        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.data_source = NdaxAPIOrderBookDataSource(
            throttler=self.throttler, trading_pairs=[self.trading_pair])
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)
        self.data_source._trading_pair_id_map.clear()

        self.mocking_assistant = NetworkMockingAssistant()

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

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

    def simulate_trading_pair_ids_initialized(self):
        self.data_source._trading_pair_id_map.update(
            {self.trading_pair: self.instrument_id})

    def _raise_exception(self, exception_class):
        raise exception_class

    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 _subscribe_level_2_response(self):
        resp = {
            "m":
            1,
            "i":
            2,
            "n":
            "SubscribeLevel2",
            "o":
            "[[93617617, 1, 1626788175000, 0, 37800.0, 1, 37750.0, 1, 0.015, 0],[93617617, 1, 1626788175000, 0, 37800.0, 1, 37751.0, 1, 0.015, 1]]"
        }
        return ujson.dumps(resp)

    def _orderbook_update_event(self):
        resp = {
            "m":
            3,
            "i":
            3,
            "n":
            "Level2UpdateEvent",
            "o":
            "[[93617618, 1, 1626788175001, 0, 37800.0, 1, 37740.0, 1, 0.015, 0]]"
        }
        return ujson.dumps(resp)

    @patch("aiohttp.ClientSession.get")
    def test_init_trading_pair_ids(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response: List[Any] = [{
            "Product1Symbol": self.base_asset,
            "Product2Symbol": self.quote_asset,
            "InstrumentId": self.instrument_id,
            "SessionStatus": "Running"
        }, {
            "Product1Symbol": "ANOTHER_ACTIVE",
            "Product2Symbol": "MARKET",
            "InstrumentId": 2,
            "SessionStatus": "Running"
        }, {
            "Product1Symbol": "NOT_ACTIVE",
            "Product2Symbol": "MARKET",
            "InstrumentId": 3,
            "SessionStatus": "Stopped"
        }]

        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        self.ev_loop.run_until_complete(
            self.data_source.init_trading_pair_ids())
        self.assertEqual(2, len(self.data_source._trading_pair_id_map))
        self.assertEqual(
            1, self.data_source._trading_pair_id_map[self.trading_pair])
        self.assertEqual(
            2, self.data_source._trading_pair_id_map["ANOTHER_ACTIVE-MARKET"])

    @patch("aiohttp.ClientSession.get")
    def test_get_last_traded_prices(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        self.simulate_trading_pair_ids_initialized()
        mock_response: Dict[Any] = {"LastTradedPx": 1.0}

        self.mocking_assistant.add_http_response(mock_api, 200, 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],
                         mock_response["LastTradedPx"])

    @patch("aiohttp.ClientSession.get")
    def test_fetch_trading_pairs(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        self.simulate_trading_pair_ids_initialized()

        mock_response: List[Any] = [{
            "Product1Symbol": self.base_asset,
            "Product2Symbol": self.quote_asset,
            "InstrumentId": self.instrument_id,
            "SessionStatus": "Running"
        }, {
            "Product1Symbol": "ANOTHER_ACTIVE",
            "Product2Symbol": "MARKET",
            "InstrumentId": 2,
            "SessionStatus": "Running"
        }, {
            "Product1Symbol": "NOT_ACTIVE",
            "Product2Symbol": "MARKET",
            "InstrumentId": 3,
            "SessionStatus": "Stopped"
        }]

        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results: List[str] = self.ev_loop.run_until_complete(
            self.data_source.fetch_trading_pairs())
        self.assertTrue(self.trading_pair in results)
        self.assertTrue("ANOTHER_ACTIVE-MARKET" in results)
        self.assertFalse("NOT_ACTIVE-MARKET" in results)

    @patch("aiohttp.ClientSession.get")
    def test_fetch_trading_pairs_with_error_status_in_response(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        mock_response = {}
        self.mocking_assistant.add_http_response(mock_api, 100, mock_response)

        result = self.ev_loop.run_until_complete(
            self.data_source.fetch_trading_pairs())
        self.assertEqual(0, len(result))

    @patch("aiohttp.ClientSession.get")
    def test_get_order_book_data(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        self.simulate_trading_pair_ids_initialized()
        mock_response: List[List[Any]] = [
            # mdUpdateId, accountId, actionDateTime, actionType, lastTradePrice, orderId, price, productPairCode, quantity, side
            [
                93617617, 1, 1626788175416, 0, 37813.22, 1, 37750.6, 1,
                0.014698, 0
            ]
        ]
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results = self.ev_loop.run_until_complete(
            asyncio.gather(
                self.data_source.get_order_book_data(self.trading_pair)))
        result = results[0]

        self.assertTrue("data" in result)
        self.assertGreaterEqual(len(result["data"]), 0)
        self.assertEqual(NdaxOrderBookEntry(*mock_response[0]),
                         result["data"][0])

    @patch("aiohttp.ClientSession.get")
    def test_get_order_book_data_raises_exception_when_response_has_error_code(
            self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        self.simulate_trading_pair_ids_initialized()
        mock_response = {"Erroneous response"}
        self.mocking_assistant.add_http_response(mock_api, 100, mock_response)

        with self.assertRaises(IOError) as context:
            self.ev_loop.run_until_complete(
                self.data_source.get_order_book_data(self.trading_pair))

        self.assertEqual(
            str(context.exception),
            f"Error fetching OrderBook for {self.trading_pair} "
            f"at {CONSTANTS.ORDER_BOOK_URL}. "
            f"HTTP {100}. Response: {mock_response}")

    @patch("aiohttp.ClientSession.get")
    def test_get_new_order_book(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        self.simulate_trading_pair_ids_initialized()

        mock_response: List[List[Any]] = [
            # mdUpdateId, accountId, actionDateTime, actionType, lastTradePrice, orderId, price, productPairCode, quantity, side
            [93617617, 1, 1626788175416, 0, 37800.0, 1, 37750.0, 1, 0.015, 0],
            [93617617, 1, 1626788175416, 0, 37800.0, 1, 37751.0, 1, 0.015, 1]
        ]
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results = self.ev_loop.run_until_complete(
            asyncio.gather(
                self.data_source.get_new_order_book(self.trading_pair)))
        result: OrderBook = results[0]

        self.assertTrue(type(result) == OrderBook)
        self.assertEqual(result.snapshot_uid, 0)

    @patch("aiohttp.ClientSession.get")
    def test_get_instrument_ids(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response: List[Any] = [{
            "Product1Symbol": self.base_asset,
            "Product2Symbol": self.quote_asset,
            "InstrumentId": self.instrument_id,
            "SessionStatus": "Running",
        }]
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results = self.ev_loop.run_until_complete(
            asyncio.gather(self.data_source.get_instrument_ids()))
        result: Dict[str, Any] = results[0]

        self.assertEqual(
            1, self.data_source._trading_pair_id_map[self.trading_pair])
        self.assertEqual(result[self.trading_pair], self.instrument_id)

    @patch(
        "hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.get")
    def test_listen_for_snapshots_cancelled_when_fetching_snapshot(
            self, mock_api, mock_sleep):
        mock_api.side_effect = asyncio.CancelledError
        self.simulate_trading_pair_ids_initialized()

        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.ev_loop.run_until_complete(self.listening_task)

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

    @patch(
        "hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.get")
    def test_listen_for_snapshots_logs_exception_when_fetching_snapshot(
            self, mock_api, mock_sleep):
        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)

        self.simulate_trading_pair_ids_initialized()

        mock_api.side_effect = Exception
        mock_sleep.side_effect = lambda delay: 1 / 0 if len(
            sync_queue) == 0 else sync_queue.pop()

        msg_queue: asyncio.Queue = asyncio.Queue()
        with self.assertRaises(ZeroDivisionError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))
            self.ev_loop.run_until_complete(self.listening_task)

        self.assertEqual(msg_queue.qsize(), 0)
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occured listening for orderbook snapshots. Retrying in 5 secs..."
            ))

    @patch(
        "hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.get")
    def test_listen_for_snapshots_successful(self, mock_api, mock_sleep):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)

        mock_response: List[List[Any]] = [
            # mdUpdateId, accountId, actionDateTime, actionType, lastTradePrice, orderId, price, productPairCode, quantity, side
            [93617617, 1, 1626788175416, 0, 37800.0, 1, 37750.0, 1, 0.015, 0],
            [93617617, 1, 1626788175416, 0, 37800.0, 1, 37751.0, 1, 0.015, 1],
        ]
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)
        self.simulate_trading_pair_ids_initialized()

        mock_sleep.side_effect = lambda delay: 1 / 0 if len(
            sync_queue) == 0 else sync_queue.pop()

        msg_queue: asyncio.Queue = asyncio.Queue()
        with self.assertRaises(ZeroDivisionError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_order_book_snapshots(
                    self.ev_loop, msg_queue))
            self.ev_loop.run_until_complete(self.listening_task)

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

        snapshot_msg: OrderBookMessage = msg_queue.get_nowait()
        self.assertEqual(snapshot_msg.update_id, 0)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_cancelled_when_subscribing(
            self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.send_json.side_effect = asyncio.CancelledError()

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._subscribe_level_2_response())
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._orderbook_update_event())

        self.simulate_trading_pair_ids_initialized()

        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.ev_loop.run_until_complete(self.listening_task)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_cancelled_when_listening(
            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 = lambda: (
            self._raise_exception(asyncio.CancelledError))

        self.simulate_trading_pair_ids_initialized()

        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.ev_loop.run_until_complete(self.listening_task)

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

    @patch("hummingbot.client.hummingbot_application.HummingbotApplication")
    @patch(
        "hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch("aiohttp.ClientSession.get", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_logs_exception(
            self, mock_api, mock_ws, *_):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.close.return_value = None

        incomplete_resp = {
            "m": 1,
            "i": 2,
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, ujson.dumps(incomplete_resp))
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._orderbook_update_event())

        self.simulate_trading_pair_ids_initialized()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        self.ev_loop.run_until_complete(msg_queue.get())

        self.assertTrue(
            self._is_logged("NETWORK",
                            "Unexpected error with WebSocket connection."))

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_successful(self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.send_json.return_value = None

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._subscribe_level_2_response())
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, self._orderbook_update_event())

        self.simulate_trading_pair_ids_initialized()

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

        first_msg = self.ev_loop.run_until_complete(msg_queue.get())
        second_msg = self.ev_loop.run_until_complete(msg_queue.get())

        self.assertTrue(first_msg.type == OrderBookMessageType.SNAPSHOT)
        self.assertTrue(second_msg.type == OrderBookMessageType.DIFF)

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

        with self.assertRaises(asyncio.CancelledError):
            asyncio.get_event_loop().run_until_complete(
                self.data_source._create_websocket_connection())

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

        with self.assertRaises(Exception):
            asyncio.get_event_loop().run_until_complete(
                self.data_source._create_websocket_connection())

        self.assertTrue(
            self._is_logged(
                "NETWORK",
                "Unexpected error occurred during ndax WebSocket Connection ()"
            ))
Ejemplo n.º 2
0
class MexcExchangeTests(TestCase):
    # the level is required to receive logs from the data source loger
    level = 0

    start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp()

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

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

        self.tracker_task = None
        self.exchange_task = None
        self.log_records = []
        self.resume_test_event = asyncio.Event()
        self._account_name = "hbot"

        self.exchange = MexcExchange(mexc_api_key='testAPIKey',
                                     mexc_secret_key='testSecret',
                                     trading_pairs=[self.trading_pair])

        self.exchange.logger().setLevel(1)
        self.exchange.logger().addHandler(self)
        self.exchange._account_id = 1

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

    def tearDown(self) -> None:
        self.tracker_task and self.tracker_task.cancel()
        self.exchange_task and self.exchange_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 _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 _create_exception_and_unlock_test_with_event(self, exception):
        self.resume_test_event.set()
        raise exception

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

    def _simulate_reset_poll_notifier(self):
        self.exchange._poll_notifier.clear()

    def _simulate_ws_message_received(self, timestamp: float):
        self.exchange._user_stream_tracker._data_source._last_recv_time = timestamp

    def _simulate_trading_rules_initialized(self):
        self.exchange._trading_rules = {
            self.trading_pair:
            TradingRule(trading_pair=self.trading_pair,
                        min_order_size=4,
                        min_price_increment=Decimal(str(0.0001)),
                        min_base_amount_increment=2,
                        min_notional_size=Decimal(str(5)))
        }

    @property
    def order_book_data(self):
        _data = {
            "code": 200,
            "data": {
                "asks": [{
                    "price": "56454.0",
                    "quantity": "0.799072"
                }, {
                    "price": "56455.28",
                    "quantity": "0.008663"
                }],
                "bids": [{
                    "price": "56451.0",
                    "quantity": "0.008663"
                }, {
                    "price": "56449.99",
                    "quantity": "0.173078"
                }],
                "version":
                "547878563"
            }
        }
        return _data

    def _simulate_create_order(self,
                               trade_type: TradeType,
                               order_id: str,
                               trading_pair: str,
                               amount: Decimal,
                               price: Decimal = Decimal("0"),
                               order_type: OrderType = OrderType.MARKET):
        future = safe_ensure_future(
            self.exchange.execute_buy(order_id, trading_pair, amount,
                                      order_type, price))
        self.exchange.start_tracking_order(order_id, None, self.trading_pair,
                                           TradeType.BUY, Decimal(10.0),
                                           Decimal(1.0), OrderType.LIMIT)
        return future

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

        self.exchange_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())

        dummy_user_stream = AsyncMock()
        dummy_user_stream.get.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            Exception("Dummy test error"))
        self.exchange._user_stream_tracker._user_stream = dummy_user_stream

        # Add a dummy message for the websocket to read and include in the "messages" queue
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock, ujson.dumps({'channel': 'push.personal.order'}))
        self.async_run_with_timeout(self.resume_test_event.wait())
        self.resume_test_event.clear()

        try:
            self.exchange_task.cancel()
            self.async_run_with_timeout(self.exchange_task)
        except asyncio.CancelledError:
            pass
        except Exception:
            pass

        self.assertTrue(
            self._is_logged(
                'ERROR',
                "Unknown error. Retrying after 1 second. Dummy test error"))

    def test_user_event_queue_notifies_cancellations(self):
        self.tracker_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())

        dummy_user_stream = AsyncMock()
        dummy_user_stream.get.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            asyncio.CancelledError())
        self.exchange._user_stream_tracker._user_stream = dummy_user_stream

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(self.tracker_task)

    def test_exchange_logs_unknown_event_message(self):
        payload = {'channel': 'test'}
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: payload)

        self.exchange._user_stream_tracker._user_stream = mock_user_stream
        self.exchange_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertTrue(
            self._is_logged(
                'DEBUG',
                f"Unknown event received from the connector ({payload})"))

    @property
    def balances_mock_data(self):
        return {
            "code": 200,
            "data": {
                "MX": {
                    "frozen": "30.9863",
                    "available": "450.0137"
                }
            }
        }

    @property
    def user_stream_data(self):
        return {
            'symbol': 'MX_USDT',
            'data': {
                'price': 3.1504,
                'quantity': 2,
                'amount': 6.3008,
                'remainAmount': 6.3008,
                'remainQuantity': 2,
                'remainQ': 2,
                'id': '40728558ead64032a676e6f0a4afc4ca',
                'status': 4,
                'tradeType': 2,
                'createTime': 1638156451000,
                'symbolDisplay': 'MX_USDT',
                'clientOrderId': 'sell-MX-USDT-1638156451005305'
            },
            'channel': 'push.personal.order',
            'symbol_display': 'MX_USDT'
        }

    @aioresponses()
    def test_order_event_with_cancel_status_cancels_in_flight_order(
            self, mock_api):
        mock_response = self.balances_mock_data
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(
            regex_url,
            body=json.dumps(mock_response),
        )

        self.exchange.start_tracking_order(
            order_id="sell-MX-USDT-1638156451005305",
            exchange_order_id="40728558ead64032a676e6f0a4afc4ca",
            trading_pair="MX-USDT",
            trade_type=TradeType.SELL,
            price=Decimal("3.1504"),
            amount=Decimal("6.3008"),
            order_type=OrderType.LIMIT)

        inflight_order = self.exchange.in_flight_orders[
            "sell-MX-USDT-1638156451005305"]

        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = [
            self.user_stream_data, asyncio.CancelledError
        ]

        self.exchange._user_stream_tracker._user_stream = mock_user_stream

        try:
            self.async_run_with_timeout(
                self.exchange._user_stream_event_listener(), 1000000)
        except asyncio.CancelledError:
            pass

        self.assertEqual("CANCELED", inflight_order.last_state)
        self.assertTrue(inflight_order.is_cancelled)
        self.assertFalse(
            inflight_order.client_order_id in self.exchange.in_flight_orders)
        self.assertTrue(
            self._is_logged(
                "INFO", f"Order {inflight_order.client_order_id} "
                f"has been canceled according to order delta websocket API."))
        self.assertEqual(1, len(self.exchange.event_logs))
        cancel_event = self.exchange.event_logs[0]
        self.assertEqual(OrderCancelledEvent, type(cancel_event))
        self.assertEqual(inflight_order.client_order_id, cancel_event.order_id)

    @aioresponses()
    def test_order_event_with_rejected_status_makes_in_flight_order_fail(
            self, mock_api):
        mock_response = self.balances_mock_data
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(
            regex_url,
            body=json.dumps(mock_response),
        )
        self.exchange.start_tracking_order(
            order_id="sell-MX-USDT-1638156451005305",
            exchange_order_id="40728558ead64032a676e6f0a4afc4ca",
            trading_pair="MX-USDT",
            trade_type=TradeType.SELL,
            price=Decimal("3.1504"),
            amount=Decimal("6.3008"),
            order_type=OrderType.LIMIT)

        inflight_order = self.exchange.in_flight_orders[
            "sell-MX-USDT-1638156451005305"]
        stream_data = self.user_stream_data
        stream_data.get("data")["status"] = 5
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = [
            stream_data, asyncio.CancelledError
        ]
        self.exchange._user_stream_tracker._user_stream = mock_user_stream
        try:
            self.async_run_with_timeout(
                self.exchange._user_stream_event_listener(), 1000000)
        except asyncio.CancelledError:
            pass

        self.assertEqual("PARTIALLY_CANCELED", inflight_order.last_state)
        self.assertTrue(inflight_order.is_failure)
        self.assertFalse(
            inflight_order.client_order_id in self.exchange.in_flight_orders)
        self.assertTrue(
            self._is_logged(
                "INFO", f"Order {inflight_order.client_order_id} "
                f"has been canceled according to order delta websocket API."))
        self.assertEqual(1, len(self.exchange.event_logs))
        failure_event = self.exchange.event_logs[0]
        self.assertEqual(OrderCancelledEvent, type(failure_event))
        self.assertEqual(inflight_order.client_order_id,
                         failure_event.order_id)

    @aioresponses()
    def test_trade_event_fills_and_completes_buy_in_flight_order(
            self, mock_api):
        fee_mock_data = {
            'code':
            200,
            'data': [{
                'id': 'c85b7062f69c4bf1b6c153dca5c0318a',
                'symbol': 'MX_USDT',
                'quantity': '2',
                'price': '3.1265',
                'amount': '6.253',
                'fee': '0.012506',
                'trade_type': 'BID',
                'order_id': '95c4ce45fdd34cf99bfd1e1378eb38ae',
                'is_taker': False,
                'fee_currency': 'USDT',
                'create_time': 1638177115000
            }]
        }
        mock_response = self.balances_mock_data
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(
            regex_url,
            body=json.dumps(mock_response),
        )
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_DEAL_DETAIL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(
            regex_url,
            body=json.dumps(fee_mock_data),
        )
        self.exchange.start_tracking_order(
            order_id="sell-MX-USDT-1638156451005305",
            exchange_order_id="40728558ead64032a676e6f0a4afc4ca",
            trading_pair="MX-USDT",
            trade_type=TradeType.SELL,
            price=Decimal("3.1504"),
            amount=Decimal("6.3008"),
            order_type=OrderType.LIMIT)
        inflight_order = self.exchange.in_flight_orders[
            "sell-MX-USDT-1638156451005305"]
        _user_stream = self.user_stream_data
        _user_stream.get("data")["status"] = 2
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = [
            _user_stream, asyncio.CancelledError
        ]

        self.exchange._user_stream_tracker._user_stream = mock_user_stream
        try:
            self.async_run_with_timeout(
                self.exchange._user_stream_event_listener(), 1000000)
        except asyncio.CancelledError:
            pass

        self.assertEqual("FILLED", inflight_order.last_state)
        self.assertEqual(Decimal(0), inflight_order.executed_amount_base)
        self.assertEqual(Decimal(0), inflight_order.executed_amount_quote)
        self.assertEqual(1, len(self.exchange.event_logs))
        fill_event = self.exchange.event_logs[0]
        self.assertEqual(SellOrderCompletedEvent, type(fill_event))
        self.assertEqual(inflight_order.client_order_id, fill_event.order_id)
        self.assertEqual(inflight_order.trading_pair,
                         f'{fill_event.base_asset}-{fill_event.quote_asset}')

    def test_tick_initial_tick_successful(self):
        start_ts: float = time.time() * 1e3

        self.exchange.tick(start_ts)
        self.assertEqual(start_ts, self.exchange._last_timestamp)
        self.assertTrue(self.exchange._poll_notifier.is_set())

    @patch("time.time")
    def test_tick_subsequent_tick_within_short_poll_interval(self, mock_ts):
        # Assumes user stream tracker has NOT been receiving messages, Hence SHORT_POLL_INTERVAL in use
        start_ts: float = self.start_timestamp
        next_tick: float = start_ts + (self.exchange.SHORT_POLL_INTERVAL - 1)

        mock_ts.return_value = start_ts
        self.exchange.tick(start_ts)
        self.assertEqual(start_ts, self.exchange._last_timestamp)
        self.assertTrue(self.exchange._poll_notifier.is_set())

        self._simulate_reset_poll_notifier()

        mock_ts.return_value = next_tick
        self.exchange.tick(next_tick)
        self.assertEqual(next_tick, self.exchange._last_timestamp)
        self.assertTrue(self.exchange._poll_notifier.is_set())

    @patch("time.time")
    def test_tick_subsequent_tick_exceed_short_poll_interval(self, mock_ts):
        # Assumes user stream tracker has NOT been receiving messages, Hence SHORT_POLL_INTERVAL in use
        start_ts: float = self.start_timestamp
        next_tick: float = start_ts + (self.exchange.SHORT_POLL_INTERVAL + 1)

        mock_ts.return_value = start_ts
        self.exchange.tick(start_ts)
        self.assertEqual(start_ts, self.exchange._last_timestamp)
        self.assertTrue(self.exchange._poll_notifier.is_set())

        self._simulate_reset_poll_notifier()

        mock_ts.return_value = next_tick
        self.exchange.tick(next_tick)
        self.assertEqual(next_tick, self.exchange._last_timestamp)
        self.assertTrue(self.exchange._poll_notifier.is_set())

    @aioresponses()
    def test_update_balances(self, mock_api):
        self.assertEqual(0, len(self.exchange._account_balances))
        self.assertEqual(0, len(self.exchange._account_available_balances))

        mock_response = self.balances_mock_data
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(
            regex_url,
            body=json.dumps(mock_response),
        )

        self.exchange_task = asyncio.get_event_loop().create_task(
            self.exchange._update_balances())
        self.async_run_with_timeout(self.exchange_task)

        self.assertEqual(Decimal(str(481.0)),
                         self.exchange.get_balance(self.base_asset))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp",
        new_callable=PropertyMock)
    def test_update_order_status(self, mock_api, mock_ts):
        # Simulates order being tracked
        order: MexcInFlightOrder = MexcInFlightOrder(
            "0",
            "2628",
            self.trading_pair,
            OrderType.LIMIT,
            TradeType.SELL,
            Decimal(str(41720.83)),
            Decimal("1"),
            1640001112.0,
            "Working",
        )
        self.exchange._in_flight_orders.update({order.client_order_id: order})
        self.exchange._last_poll_timestamp = 10
        ts: float = time.time()
        mock_ts.return_value = ts
        self.exchange._current_timestamp = ts
        self.assertTrue(1, len(self.exchange.in_flight_orders))

        # Add TradeHistory API Response
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_DETAILS_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {
            "code":
            200,
            "data": [{
                "id": "504feca6ba6349e39c82262caf0be3f4",
                "symbol": "MX_USDT",
                "price": "3.001",
                "quantity": "30",
                "state": "CANCELED",
                "type": "BID",
                "deal_quantity": "0",
                "deal_amount": "0",
                "create_time": 1573117266000
            }]
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))

        self.async_run_with_timeout(self.exchange._update_order_status())
        self.assertEqual(0, len(self.exchange.in_flight_orders))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp",
        new_callable=PropertyMock)
    def test_update_order_status_error_response(self, mock_api, mock_ts):

        # Simulates order being tracked
        order: MexcInFlightOrder = MexcInFlightOrder(
            "0",
            "2628",
            self.trading_pair,
            OrderType.LIMIT,
            TradeType.SELL,
            Decimal(str(41720.83)),
            Decimal("1"),
            creation_timestamp=1640001112.0)
        self.exchange._in_flight_orders.update({order.client_order_id: order})
        self.assertTrue(1, len(self.exchange.in_flight_orders))

        ts: float = time.time()
        mock_ts.return_value = ts
        self.exchange._current_timestamp = ts

        # Add TradeHistory API Response
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_DETAILS_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {
            "result": False,
            "errormsg": "Invalid Request",
            "errorcode": 100,
            "detail": None
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))
        self.async_run_with_timeout(self.exchange._update_order_status())
        self.assertEqual(1, len(self.exchange.in_flight_orders))

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_balances",
        new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_order_status",
        new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp",
        new_callable=PropertyMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._reset_poll_notifier"
    )
    def test_status_polling_loop(self, _, mock_ts, mock_update_order_status,
                                 mock_balances):
        mock_balances.return_value = None
        mock_update_order_status.return_value = None

        ts: float = time.time()
        mock_ts.return_value = ts
        self.exchange._current_timestamp = ts

        with self.assertRaises(asyncio.TimeoutError):
            self.exchange_task = asyncio.get_event_loop().create_task(
                self.exchange._status_polling_loop())
            self.exchange._poll_notifier.set()

            self.async_run_with_timeout(
                asyncio.wait_for(self.exchange_task, 2.0))

        self.assertEqual(ts, self.exchange._last_poll_timestamp)

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp",
        new_callable=PropertyMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._reset_poll_notifier"
    )
    @aioresponses()
    def test_status_polling_loop_cancels(self, _, mock_ts, mock_api):
        url = CONSTANTS.MEXC_BASE_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, exception=asyncio.CancelledError)

        ts: float = time.time()
        mock_ts.return_value = ts
        self.exchange._current_timestamp = ts

        with self.assertRaises(asyncio.CancelledError):
            self.exchange_task = asyncio.get_event_loop().create_task(
                self.exchange._status_polling_loop())
            self.exchange._poll_notifier.set()

            self.async_run_with_timeout(self.exchange_task)

        self.assertEqual(0, self.exchange._last_poll_timestamp)

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_balances",
        new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_order_status",
        new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp",
        new_callable=PropertyMock)
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._reset_poll_notifier"
    )
    def test_status_polling_loop_exception_raised(self, _, mock_ts,
                                                  mock_update_order_status,
                                                  mock_balances):
        mock_balances.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            Exception("Dummy test error"))
        mock_update_order_status.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            Exception("Dummy test error"))

        ts: float = time.time()
        mock_ts.return_value = ts
        self.exchange._current_timestamp = ts

        self.exchange_task = asyncio.get_event_loop().create_task(
            self.exchange._status_polling_loop())

        self.exchange._poll_notifier.set()

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

        self.assertEqual(0, self.exchange._last_poll_timestamp)
        self._is_logged(
            "ERROR", "Unexpected error while in status polling loop. Error: ")

    def test_format_trading_rules_success(self):
        instrument_info: List[Dict[str, Any]] = [{
            "symbol": f"{self.base_asset}_{self.quote_asset}",
            "price_scale": 3,
            "quantity_scale": 3,
            "min_amount": "1",
        }]

        result: List[str, TradingRule] = self.exchange._format_trading_rules(
            instrument_info)
        self.assertTrue(self.trading_pair == result[0].trading_pair)

    def test_format_trading_rules_failure(self):
        # Simulate invalid API response
        instrument_info: List[Dict[str, Any]] = [{}]

        result: Dict[str, TradingRule] = self.exchange._format_trading_rules(
            instrument_info)
        self.assertTrue(self.trading_pair not in result)
        self.assertTrue(
            self._is_logged(
                "ERROR", 'Error parsing the trading pair rule {}. Skipping.'))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp",
        new_callable=PropertyMock)
    def test_update_trading_rules(self, mock_api, mock_ts):
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_SYMBOL_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {
            "code":
            200,
            "data": [{
                "symbol": "MX_USDT",
                "state": "ENABLED",
                "price_scale": 4,
                "quantity_scale": 2,
                "min_amount": "5",
                "max_amount": "5000000",
                "maker_fee_rate": "0.002",
                "taker_fee_rate": "0.002",
                "limited": False,
                "etf_mark": 0,
                "symbol_partition": "MAIN"
            }]
        }
        mock_api.get(regex_url, body=json.dumps(mock_response))
        self.exchange._last_poll_timestamp = 10
        ts: float = time.time()
        mock_ts.return_value = ts
        self.exchange._current_timestamp = ts

        task = asyncio.get_event_loop().create_task(
            self.exchange._update_trading_rules())
        self.async_run_with_timeout(task)

        self.assertTrue(self.trading_pair in self.exchange.trading_rules)

        self.exchange.trading_rules[self.trading_pair]

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_trading_rules",
        new_callable=AsyncMock)
    def test_trading_rules_polling_loop(self, mock_update):
        # No Side Effects expected
        mock_update.return_value = None
        with self.assertRaises(asyncio.TimeoutError):
            self.exchange_task = asyncio.get_event_loop().create_task(
                self.exchange._trading_rules_polling_loop())

            self.async_run_with_timeout(
                asyncio.wait_for(self.exchange_task, 1.0))

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_trading_rules",
        new_callable=AsyncMock)
    def test_trading_rules_polling_loop_cancels(self, mock_update):
        mock_update.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.exchange_task = asyncio.get_event_loop().create_task(
                self.exchange._trading_rules_polling_loop())

            self.async_run_with_timeout(self.exchange_task)

        self.assertEqual(0, self.exchange._last_poll_timestamp)

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_trading_rules",
        new_callable=AsyncMock)
    def test_trading_rules_polling_loop_exception_raised(self, mock_update):
        mock_update.side_effect = lambda: self._create_exception_and_unlock_test_with_event(
            Exception("Dummy test error"))

        self.exchange_task = asyncio.get_event_loop().create_task(
            self.exchange._trading_rules_polling_loop())

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

        self._is_logged(
            "ERROR", "Unexpected error while fetching trading rules. Error: ")

    @aioresponses()
    def test_check_network_succeeds_when_ping_replies_pong(self, mock_api):
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {"code": 200}
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(self.exchange.check_network())

        self.assertEqual(NetworkStatus.CONNECTED, result)

    @aioresponses()
    def test_check_network_fails_when_ping_does_not_reply_pong(self, mock_api):
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {"code": 100}
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(self.exchange.check_network())
        self.assertEqual(NetworkStatus.NOT_CONNECTED, result)

        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {}
        mock_api.get(regex_url, body=json.dumps(mock_response))

        result = self.async_run_with_timeout(self.exchange.check_network())
        self.assertEqual(NetworkStatus.NOT_CONNECTED, result)

    @aioresponses()
    def test_check_network_fails_when_ping_returns_error_code(self, mock_api):
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_response = {"code": 100}
        mock_api.get(regex_url, body=json.dumps(mock_response), status=404)

        result = self.async_run_with_timeout(self.exchange.check_network())

        self.assertEqual(NetworkStatus.NOT_CONNECTED, result)

    def test_get_order_book_for_valid_trading_pair(self):
        dummy_order_book = MexcOrderBook()
        self.exchange._order_book_tracker.order_books[
            "BTC-USDT"] = dummy_order_book
        self.assertEqual(dummy_order_book,
                         self.exchange.get_order_book("BTC-USDT"))

    def test_get_order_book_for_invalid_trading_pair_raises_error(self):
        self.assertRaisesRegex(ValueError,
                               "No order book exists for 'BTC-USDT'",
                               self.exchange.get_order_book, "BTC-USDT")

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.execute_buy",
        new_callable=AsyncMock)
    def test_buy(self, mock_create):
        mock_create.side_effect = None
        order_details = [
            self.trading_pair,
            Decimal(1.0),
            Decimal(10.0),
            OrderType.LIMIT,
        ]

        # Note: BUY simply returns immediately with the client order id.
        order_id: str = self.exchange.buy(*order_details)

        # Order ID is simply a timestamp. The assertion below checks if it is created within 1 sec
        self.assertTrue(len(order_id) > 0)

    def test_sell(self):
        order_details = [
            self.trading_pair,
            Decimal(1.0),
            Decimal(10.0),
            OrderType.LIMIT,
        ]

        # Note: SELL simply returns immediately with the client order id.
        order_id: str = self.exchange.buy(*order_details)

        # Order ID is simply a timestamp. The assertion below checks if it is created within 1 sec
        self.assertTrue(len(order_id) > 0)

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.quantize_order_amount"
    )
    def test_create_limit_order(self, mock_post, amount_mock):
        amount_mock.return_value = Decimal("1")
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PLACE_ORDER
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        expected_response = {"code": 200, "data": "123"}
        mock_post.post(regex_url, body=json.dumps(expected_response))

        self._simulate_trading_rules_initialized()

        order_details = [
            TradeType.BUY,
            str(1),
            self.trading_pair,
            Decimal(1.0),
            Decimal(10.0),
            OrderType.LIMIT,
        ]

        self.assertEqual(0, len(self.exchange.in_flight_orders))
        future = self._simulate_create_order(*order_details)
        self.async_run_with_timeout(future)

        self.assertEqual(1, len(self.exchange.in_flight_orders))
        self._is_logged(
            "INFO",
            f"Created {OrderType.LIMIT.name} {TradeType.BUY.name} order {123} for {Decimal(1.0)} {self.trading_pair}"
        )

        tracked_order: MexcInFlightOrder = self.exchange.in_flight_orders["1"]
        self.assertEqual(tracked_order.client_order_id, "1")
        self.assertEqual(tracked_order.exchange_order_id, "123")
        self.assertEqual(tracked_order.last_state, "NEW")
        self.assertEqual(tracked_order.trading_pair, self.trading_pair)
        self.assertEqual(tracked_order.price, Decimal(10.0))
        self.assertEqual(tracked_order.amount, Decimal(1.0))
        self.assertEqual(tracked_order.trade_type, TradeType.BUY)

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.quantize_order_amount"
    )
    def test_create_market_order(self, mock_post, amount_mock):
        amount_mock.return_value = Decimal("1")
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PLACE_ORDER
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        expected_response = {"code": 200, "data": "123"}
        mock_post.post(regex_url, body=json.dumps(expected_response))

        self._simulate_trading_rules_initialized()

        order_details = [
            TradeType.BUY,
            str(1),
            self.trading_pair,
            Decimal(1.0),
            Decimal(10.0),
            OrderType.LIMIT_MAKER,
        ]

        self.assertEqual(0, len(self.exchange.in_flight_orders))
        future = self._simulate_create_order(*order_details)
        self.async_run_with_timeout(future)

        self.assertEqual(1, len(self.exchange.in_flight_orders))
        self._is_logged(
            "INFO",
            f"Created {OrderType.LIMIT.name} {TradeType.BUY.name} order {123} for {Decimal(1.0)} {self.trading_pair}"
        )

        tracked_order: MexcInFlightOrder = self.exchange.in_flight_orders["1"]
        self.assertEqual(tracked_order.client_order_id, "1")
        self.assertEqual(tracked_order.exchange_order_id, "123")
        self.assertEqual(tracked_order.last_state, "NEW")
        self.assertEqual(tracked_order.trading_pair, self.trading_pair)
        self.assertEqual(tracked_order.amount, Decimal(1.0))
        self.assertEqual(tracked_order.trade_type, TradeType.BUY)

    @aioresponses()
    def test_detect_created_order_server_acknowledgement(self, mock_api):
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, body=json.dumps(self.balances_mock_data))

        self.exchange.start_tracking_order(
            order_id="sell-MX-USDT-1638156451005305",
            exchange_order_id="40728558ead64032a676e6f0a4afc4ca",
            trading_pair="MX-USDT",
            trade_type=TradeType.SELL,
            price=Decimal("3.1504"),
            amount=Decimal("6.3008"),
            order_type=OrderType.LIMIT)
        _user_data = self.user_stream_data
        _user_data.get("data")["status"] = 2
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: _user_data)
        self.exchange._user_stream_tracker._user_stream = mock_user_stream
        self.exchange_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.exchange.in_flight_orders))
        tracked_order: MexcInFlightOrder = self.exchange.in_flight_orders[
            "sell-MX-USDT-1638156451005305"]
        self.assertEqual(tracked_order.last_state, "NEW")

    @aioresponses()
    def test_execute_cancel_success(self, mock_cancel):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0,
            initial_state="Working",
        )

        self.exchange._in_flight_orders.update({order.client_order_id: order})

        mock_response = {"code": 200, "data": {"123": "success"}}
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_cancel.delete(regex_url, body=json.dumps(mock_response))

        self.mocking_assistant.configure_http_request_mock(mock_cancel)
        self.mocking_assistant.add_http_response(mock_cancel, 200,
                                                 mock_response, "")

        result = self.async_run_with_timeout(
            self.exchange.execute_cancel(self.trading_pair,
                                         order.client_order_id))
        self.assertIsNone(result)

    @aioresponses()
    def test_execute_cancel_all_success(self, mock_post_request):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0)

        self.exchange._in_flight_orders.update({order.client_order_id: order})

        mock_response = {"code": 200, "data": {"0": "success"}}
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_post_request.delete(regex_url, body=json.dumps(mock_response))

        cancellation_results = self.async_run_with_timeout(
            self.exchange.cancel_all(10))

        self.assertEqual(1, len(cancellation_results))
        self.assertEqual("0", cancellation_results[0].order_id)
        self.assertTrue(cancellation_results[0].success)

    @aioresponses()
    @patch("hummingbot.client.hummingbot_application.HummingbotApplication")
    def test_execute_cancel_fail(self, mock_cancel, mock_main_app):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0,
            initial_state="Working",
        )

        self.exchange._in_flight_orders.update({order.client_order_id: order})
        mock_response = {"code": 100, "data": {"123": "success"}}
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_cancel.delete(regex_url, body=json.dumps(mock_response))

        self.async_run_with_timeout(
            self.exchange.execute_cancel(self.trading_pair,
                                         order.client_order_id))

        self._is_logged(
            "NETWORK",
            "Failed to cancel order 0 : MexcAPIError('Order could not be canceled')"
        )

    @aioresponses()
    def test_execute_cancel_cancels(self, mock_cancel):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0,
            initial_state="Working",
        )

        self.exchange._in_flight_orders.update({order.client_order_id: order})
        url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_cancel.delete(regex_url, exception=asyncio.CancelledError)

        with self.assertRaises(asyncio.CancelledError):
            self.async_run_with_timeout(
                self.exchange.execute_cancel(self.trading_pair,
                                             order.client_order_id))

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.execute_cancel",
        new_callable=AsyncMock)
    def test_cancel(self, mock_cancel):
        mock_cancel.return_value = None

        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0)

        self.exchange._in_flight_orders.update({order.client_order_id: order})

        # Note: BUY simply returns immediately with the client order id.
        return_val: str = self.exchange.cancel(self.trading_pair,
                                               order.client_order_id)

        # Order ID is simply a timestamp. The assertion below checks if it is created within 1 sec
        self.assertTrue(order.client_order_id, return_val)

    def test_ready_trading_required_all_ready(self):
        self.exchange._trading_required = True

        # Simulate all components initialized
        self.exchange._account_id = 1
        self.exchange._order_book_tracker._order_books_initialized.set()
        self.exchange._account_balances = {self.base_asset: Decimal(str(10.0))}
        self._simulate_trading_rules_initialized()
        self.exchange._user_stream_tracker.data_source._last_recv_time = 1

        self.assertTrue(self.exchange.ready)

    def test_ready_trading_required_not_ready(self):
        self.exchange._trading_required = True

        # Simulate all components but account_id not initialized
        self.exchange._account_id = None
        self.exchange._order_book_tracker._order_books_initialized.set()
        self.exchange._account_balances = {}
        self._simulate_trading_rules_initialized()
        self.exchange._user_stream_tracker.data_source._last_recv_time = 0

        self.assertFalse(self.exchange.ready)

    def test_ready_trading_not_required_ready(self):
        self.exchange._trading_required = False

        # Simulate all components but account_id not initialized
        self.exchange._account_id = None
        self.exchange._order_book_tracker._order_books_initialized.set()
        self.exchange._account_balances = {}
        self._simulate_trading_rules_initialized()
        self.exchange._user_stream_tracker.data_source._last_recv_time = 0

        self.assertTrue(self.exchange.ready)

    def test_ready_trading_not_required_not_ready(self):
        self.exchange._trading_required = False
        self.assertFalse(self.exchange.ready)

    def test_limit_orders(self):
        self.assertEqual(0, len(self.exchange.limit_orders))

        # Simulate orders being placed and tracked
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0)

        self.exchange._in_flight_orders.update({order.client_order_id: order})

        self.assertEqual(1, len(self.exchange.limit_orders))

    def test_tracking_states_order_not_done(self):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0)

        order_json = order.to_json()

        self.exchange._in_flight_orders.update({order.client_order_id: order})

        self.assertEqual(1, len(self.exchange.tracking_states))
        self.assertEqual(order_json,
                         self.exchange.tracking_states[order.client_order_id])

    def test_tracking_states_order_done(self):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0,
            initial_state="FILLED")

        self.exchange._in_flight_orders.update({order.client_order_id: order})

        self.assertEqual(0, len(self.exchange.tracking_states))

    def test_restore_tracking_states(self):
        order: MexcInFlightOrder = MexcInFlightOrder(
            client_order_id="0",
            exchange_order_id="123",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal(10.0),
            amount=Decimal(1.0),
            creation_timestamp=1640001112.0)

        order_json = order.to_json()

        self.exchange.restore_tracking_states(
            {order.client_order_id: order_json})

        self.assertEqual(1, len(self.exchange.in_flight_orders))
        self.assertEqual(
            str(self.exchange.in_flight_orders[order.client_order_id]),
            str(order))
class MexcAPIOrderBookDataSourceUnitTests(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 = "BTC"
        cls.quote_asset = "USDT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.instrument_id = 1

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

        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.data_source = MexcAPIOrderBookDataSource(
            throttler=self.throttler, trading_pairs=[self.trading_pair])
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)

        self.mocking_assistant = NetworkMockingAssistant()

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

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

    def _raise_exception(self, exception_class):
        raise exception_class

    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

    @patch("aiohttp.ClientSession.get")
    def test_get_last_traded_prices(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        mock_response: Dict[Any] = {
            "code":
            200,
            "data": [{
                "symbol": "BTC_USDT",
                "volume": "1076.002782",
                "high": "59387.98",
                "low": "57009",
                "bid": "57920.98",
                "ask": "57921.03",
                "open": "57735.92",
                "last": "57902.52",
                "time": 1637898900000,
                "change_rate": "0.00288555"
            }]
        }
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results = self.async_run_with_timeout(
            asyncio.gather(
                self.data_source.get_last_traded_prices([self.trading_pair])))
        results: Dict[str, Any] = results[0]

        self.assertEqual(results[self.trading_pair], 57902.52)

    @patch("aiohttp.ClientSession.get")
    def test_fetch_trading_pairs_with_error_status_in_response(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        mock_response = {}
        self.mocking_assistant.add_http_response(mock_api, 100, mock_response)

        result = self.async_run_with_timeout(
            self.data_source.fetch_trading_pairs())
        self.assertEqual(0, len(result))

    @patch("aiohttp.ClientSession.get")
    def test_get_order_book_data(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        mock_response = {
            "code": 200,
            "data": {
                "asks": [{
                    "price": "57974.06",
                    "quantity": "0.247421"
                }],
                "bids": [{
                    "price": "57974.01",
                    "quantity": "0.201635"
                }],
                "version": "562370278"
            }
        }
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results = self.async_run_with_timeout(
            asyncio.gather(
                self.data_source.get_snapshot(self.data_source._shared_client,
                                              self.trading_pair)))
        result = results[0]

        self.assertTrue("asks" in result)
        self.assertGreaterEqual(len(result), 0)
        self.assertEqual(mock_response.get("data"), result)

    @patch("aiohttp.ClientSession.get")
    def test_get_order_book_data_raises_exception_when_response_has_error_code(
            self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response = {"Erroneous response"}
        self.mocking_assistant.add_http_response(mock_api, 100, mock_response)

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

        self.assertEqual(
            str(context.exception),
            f'Error fetching MEXC market snapshot for {self.trading_pair.replace("-", "_")}. '
            f'HTTP status is {100}.')

    @patch("aiohttp.ClientSession.get")
    def test_get_new_order_book(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response = {
            "code": 200,
            "data": {
                "asks": [{
                    "price": "57974.06",
                    "quantity": "0.247421"
                }],
                "bids": [{
                    "price": "57974.01",
                    "quantity": "0.201635"
                }],
                "version": "562370278"
            }
        }
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        results = self.async_run_with_timeout(
            asyncio.gather(
                self.data_source.get_new_order_book(self.trading_pair)))
        result: OrderBook = results[0]

        self.assertTrue(type(result) == OrderBook)

    @patch("aiohttp.ClientSession.get")
    def test_listen_for_snapshots_cancelled_when_fetching_snapshot(
            self, mock_api):
        mock_api.side_effect = 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(msg_queue.qsize(), 0)

    @patch(
        "hummingbot.connector.exchange.mexc.mexc_api_order_book_data_source.MexcAPIOrderBookDataSource._sleep"
    )
    @patch("aiohttp.ClientSession.get")
    def test_listen_for_snapshots_successful(self, mock_api, mock_sleep):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)

        mock_response = {
            "code": 200,
            "data": {
                "asks": [{
                    "price": "57974.06",
                    "quantity": "0.247421"
                }],
                "bids": [{
                    "price": "57974.01",
                    "quantity": "0.201635"
                }],
                "version": "562370278"
            }
        }
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        mock_sleep.side_effect = lambda delay: 1 / 0 if len(
            sync_queue) == 0 else sync_queue.pop()

        msg_queue: asyncio.Queue = asyncio.Queue()
        with self.assertRaises(ZeroDivisionError):
            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(msg_queue.qsize(), 1)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_subscriptions_cancelled_when_subscribing(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.send_str.side_effect = asyncio.CancelledError()

        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, {'channel': 'push.personal.order'})

        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("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_cancelled_when_listening(
            self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        data = {
            'symbol': 'MX_USDT',
            'data': {
                'version': '44000093',
                'bids': [{
                    'p': '2.9311',
                    'q': '0.00',
                    'a': '0.00000000'
                }],
                'asks': [{
                    'p': '2.9311',
                    'q': '22720.37',
                    'a': '66595.6765'
                }]
            },
            'channel': 'push.depth'
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            mock_ws.return_value, ujson.dumps(data))
        safe_ensure_future(self.data_source.listen_for_subscriptions())

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

        first_msg = self.async_run_with_timeout(msg_queue.get())
        self.assertTrue(first_msg.type == OrderBookMessageType.DIFF)

    @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_websocket_connection_creation_raises_cancel_exception(
            self, mock_ws):
        mock_ws.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_websocket_connection_creation_raises_exception_after_loging(
            self, mock_ws):
        mock_ws.side_effect = Exception

        with self.assertRaises(Exception):
            self.async_run_with_timeout(
                self.data_source._create_websocket_connection())

        self.assertTrue(
            self._is_logged(
                "NETWORK",
                'Unexpected error occured connecting to mexc WebSocket API. ()'
            ))
Ejemplo n.º 4
0
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.binance_client = MockBinanceClient(api_key="TEST_API_KEY")
        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.data_source = BinanceAPIUserStreamDataSource(
            binance_client=self.binance_client,
            domain=self.domain,
            throttler=self.throttler)

        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 _raise_exception(self, exception_class):
        raise exception_class

    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 ujson.dumps(resp)

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

    def test_get_throttler_instance(self):
        self.assertIsInstance(self.data_source._get_throttler_instance(),
                              AsyncThrottler)

    @patch("aiohttp.ClientSession.post")
    def test_get_listen_key_log_exception(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        self.mocking_assistant.add_http_response(mock_api, 400,
                                                 self._error_response())

        with self.assertRaises(IOError):
            self.ev_loop.run_until_complete(self.data_source.get_listen_key())

    @patch("aiohttp.ClientSession.post")
    def test_get_listen_key_successful(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        self.mocking_assistant.add_http_response(
            mock_api, 200, {"listenKey": self.listen_key})

        result: str = self.ev_loop.run_until_complete(
            self.data_source.get_listen_key())

        self.assertEqual(self.listen_key, result)

    @patch("aiohttp.ClientSession.put")
    def test_ping_listen_key_log_exception(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        self.mocking_assistant.add_http_response(mock_api, 400,
                                                 self._error_response())

        result: bool = self.ev_loop.run_until_complete(
            self.data_source.ping_listen_key(listen_key=self.listen_key))

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

    @patch("aiohttp.ClientSession.put")
    def test_ping_listen_key_successful(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)
        self.mocking_assistant.add_http_response(mock_api, 200, {})

        result: bool = self.ev_loop.run_until_complete(
            self.data_source.ping_listen_key(listen_key=self.listen_key))
        self.assertTrue(result)

    @patch("aiohttp.ClientSession.post")
    @patch("websockets.connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource.wait_til_next_tick",
        new_callable=AsyncMock)
    def test_listen_for_user_stream_no_listen_key(self, mock_next_tick,
                                                  mock_ws, mock_post):
        # mock_next_tick.return_value = None
        self.mocking_assistant.configure_http_request_mock(mock_post)
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()

        self.mocking_assistant.add_websocket_text_message(
            mock_ws.return_value, self._user_update_event())

        # Add REST API response for get_listen_key()
        self.mocking_assistant.add_http_response(
            mock_post, 200, {"listenKey": self.listen_key})
        # Add REST API response for _ping_listen_key()
        # self.mocking_assistant.add_http_response(mock_post, 200, {})

        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.ev_loop.run_until_complete(msg_queue.get())
        self.assertTrue(msg, self._user_update_event)
class BinanceAPIOrderBookDataSourceUnitTests(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 = "com"

    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 = BinanceAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            throttler=self.throttler,
            domain=self.domain)
        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 _raise_exception(self, exception_class):
        raise exception_class

    def _trade_update_event(self):
        resp = {
            "e": "trade",
            "E": 123456789,
            "s": self.ex_trading_pair,
            "t": 12345,
            "p": "0.001",
            "q": "100",
            "b": 88,
            "a": 50,
            "T": 123456785,
            "m": True,
            "M": True
        }
        return ujson.dumps(resp)

    def _order_diff_event(self):
        resp = {
            "e": "depthUpdate",
            "E": 123456789,
            "s": self.ex_trading_pair,
            "U": 157,
            "u": 160,
            "b": [["0.0024", "10"]],
            "a": [["0.0026", "100"]]
        }
        return ujson.dumps(resp)

    def _snapshot_response(self):
        resp = {
            "lastUpdateId": 1027024,
            "bids": [["4.00000000", "431.00000000"]],
            "asks": [["4.00000200", "12.00000000"]]
        }
        return resp

    @patch("aiohttp.ClientSession.get", new_callable=AsyncMock)
    def test_get_last_trade_prices(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response: Dict[str, Any] = {
            # Truncated Response
            "lastPrice": "100",
        }

        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        result: Dict[str, float] = self.ev_loop.run_until_complete(
            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])

    @patch(
        "hummingbot.connector.exchange.binance.binance_utils.convert_from_exchange_trading_pair"
    )
    @patch("aiohttp.ClientSession.get", new_callable=AsyncMock)
    def test_get_all_mid_prices(self, mock_api, mock_utils):
        # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange()
        mock_utils.return_value = self.trading_pair
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response: List[Dict[str, Any]] = [{
            # Truncated Response
            "symbol": self.ex_trading_pair,
            "bidPrice": "99",
            "askPrice": "101",
        }]

        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        result: Dict[str, float] = self.ev_loop.run_until_complete(
            self.data_source.get_all_mid_prices())

        self.assertEqual(1, len(result))
        self.assertEqual(100, result[self.trading_pair])

    @patch(
        "hummingbot.connector.exchange.binance.binance_utils.convert_from_exchange_trading_pair"
    )
    @patch("aiohttp.ClientSession.get")
    def test_fetch_trading_pairs(self, mock_api, mock_utils):
        # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange()
        mock_utils.return_value = self.trading_pair
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response: Dict[str, Any] = {
            # Truncated Response
            "symbols": [
                {
                    "symbol": self.ex_trading_pair,
                    "status": "TRADING",
                    "baseAsset": self.base_asset,
                    "quoteAsset": self.quote_asset,
                },
            ]
        }

        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        result: Dict[str] = self.ev_loop.run_until_complete(
            self.data_source.fetch_trading_pairs())

        self.assertEqual(1, len(result))
        self.assertTrue(self.trading_pair in result)

    def test_get_throttler_instance(self):
        self.assertIsInstance(
            BinanceAPIOrderBookDataSource._get_throttler_instance(),
            AsyncThrottler)

    @patch("aiohttp.ClientSession.get")
    def test_get_snapshot_successful(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        self.mocking_assistant.add_http_response(mock_api, 200,
                                                 self._snapshot_response())

        result: Dict[str, Any] = self.ev_loop.run_until_complete(
            self.data_source.get_snapshot(self.trading_pair))

        self.assertEqual(self._snapshot_response(), result)

    @patch("aiohttp.ClientSession.get")
    def test_get_snapshot_catch_exception(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        self.mocking_assistant.add_http_response(mock_api, 400, {})
        with self.assertRaises(IOError):
            self.ev_loop.run_until_complete(
                self.data_source.get_snapshot(self.trading_pair))

    @patch("aiohttp.ClientSession.get")
    def test_get_new_order_book(self, mock_api):
        self.mocking_assistant.configure_http_request_mock(mock_api)

        mock_response: Dict[str, Any] = {
            "lastUpdateId": 1,
            "bids": [["4.00000000", "431.00000000"]],
            "asks": [["4.00000200", "12.00000000"]]
        }
        self.mocking_assistant.add_http_response(mock_api, 200, mock_response)

        result: OrderBook = self.ev_loop.run_until_complete(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertEqual(1, result.snapshot_uid)

    @patch("websockets.connect")
    def test_listen_for_trades_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_trades(self.ev_loop, msg_queue))
            self.ev_loop.run_until_complete(self.listening_task)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades_cancelled_when_listening(self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.recv.side_effect = lambda: (self._raise_exception(
            asyncio.CancelledError))
        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.ev_loop.run_until_complete(self.listening_task)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades_logs_exception(self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        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_text_message(
            mock_ws.return_value, ujson.dumps(incomplete_resp))
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))

        with self.assertRaises(asyncio.TimeoutError):
            self.ev_loop.run_until_complete(
                asyncio.wait_for(self.listening_task, 1))

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

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades_successful(self, mock_ws):
        msg_queue: 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_text_message(
            mock_ws.return_value, self._trade_update_event())
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.ev_loop.run_until_complete(
            msg_queue.get())

        self.assertTrue(12345, msg.trade_id)

    @patch("websockets.connect")
    def test_listen_for_order_book_diffs_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_order_book_diffs(
                    self.ev_loop, msg_queue))
            self.ev_loop.run_until_complete(self.listening_task)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_cancelled_when_listening(
            self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        mock_ws.return_value.recv.side_effect = lambda: (self._raise_exception(
            asyncio.CancelledError))
        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.ev_loop.run_until_complete(self.listening_task)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_logs_exception(self, mock_ws):
        msg_queue: asyncio.Queue = asyncio.Queue()
        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_text_message(
            mock_ws.return_value, ujson.dumps(incomplete_resp))
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        with self.assertRaises(asyncio.TimeoutError):
            self.ev_loop.run_until_complete(
                asyncio.wait_for(self.listening_task, 1))

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

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_successful(self, mock_ws):
        msg_queue: 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_text_message(
            mock_ws.return_value, self._order_diff_event())
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        msg: OrderBookMessage = self.ev_loop.run_until_complete(
            msg_queue.get())

        self.assertTrue(12345, msg.update_id)

    @patch("aiohttp.ClientSession.get")
    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(
            self, mock_api):
        mock_api.side_effect = asyncio.CancelledError

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

    @patch("aiohttp.ClientSession.get")
    def test_listen_for_order_book_snapshots_log_exception(self, mock_api):
        msg_queue: asyncio.Queue = asyncio.Queue()
        mock_api.side_effect = lambda: self._raise_exception(Exception)

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, msg_queue))
        with self.assertRaises(asyncio.TimeoutError):
            self.ev_loop.run_until_complete(
                asyncio.wait_for(self.listening_task, 1))

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

    @patch("aiohttp.ClientSession.get")
    def test_listen_for_order_book_snapshots_successful(
        self,
        mock_api,
    ):
        msg_queue: asyncio.Queue = asyncio.Queue()
        self.mocking_assistant.configure_http_request_mock(mock_api)

        self.mocking_assistant.add_http_response(mock_api, 200,
                                                 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.ev_loop.run_until_complete(
            msg_queue.get())

        self.assertTrue(12345, msg.update_id)