class AltmarketsAPIOrderBookDataSourceTests(TestCase):
    # logging.Level required to receive logs from the exchange
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "HBOT"
        cls.quote_asset = "USDT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.exchange_trading_pair = convert_to_exchange_trading_pair(
            cls.trading_pair)
        cls.api_key = "testKey"
        cls.api_secret_key = "testSecretKey"
        cls.username = "******"
        cls.throttler = AsyncThrottler(Constants.RATE_LIMITS)

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.listening_task = None
        self.data_source = AltmarketsAPIOrderBookDataSource(
            throttler=self.throttler, trading_pairs=[self.trading_pair])
        self.mocking_assistant = NetworkMockingAssistant()

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

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

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

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

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

    def test_throttler_rates(self):
        self.assertEqual(
            str(self.throttler._rate_limits[0]),
            str(self.data_source._get_throttler_instance()._rate_limits[0]))
        self.assertEqual(
            str(self.throttler._rate_limits[-1]),
            str(self.data_source._get_throttler_instance()._rate_limits[-1]))

    @aioresponses()
    def test_get_last_traded_prices(self, mock_api):
        url = f"{Constants.REST_URL}/{Constants.ENDPOINT['TICKER_SINGLE'].format(trading_pair=self.exchange_trading_pair)}"
        resp = {"ticker": {"last": 51234.56}}
        mock_api.get(url, body=json.dumps(resp))

        results = self.async_run_with_timeout(
            AltmarketsAPIOrderBookDataSource.get_last_traded_prices(
                trading_pairs=[self.trading_pair], throttler=self.throttler))

        self.assertIn(self.trading_pair, results)
        self.assertEqual(Decimal("51234.56"), results[self.trading_pair])

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_http_utils.retry_sleep_time"
    )
    def test_get_last_traded_prices_multiple(self, mock_api,
                                             retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        url = f"{Constants.REST_URL}/{Constants.ENDPOINT['TICKER']}"
        resp = {
            f"{self.exchange_trading_pair}": {
                "ticker": {
                    "last": 51234.56
                }
            },
            "rogerbtc": {
                "ticker": {
                    "last": 0.00000002
                }
            },
            "btcusdt": {
                "ticker": {
                    "last": 51234.56
                }
            },
            "hbotbtc": {
                "ticker": {
                    "last": 0.9
                }
            },
        }
        mock_api.get(url, body=json.dumps(resp))

        results = self.async_run_with_timeout(
            AltmarketsAPIOrderBookDataSource.get_last_traded_prices(
                trading_pairs=[
                    self.trading_pair, 'rogerbtc', 'btcusdt', 'hbotbtc'
                ],
                throttler=self.throttler))

        self.assertIn(self.trading_pair, results)
        self.assertEqual(Decimal("51234.56"), results[self.trading_pair])
        self.assertEqual(Decimal("0.00000002"), results["rogerbtc"])
        self.assertEqual(Decimal("51234.56"), results["btcusdt"])
        self.assertEqual(Decimal("0.9"), results["hbotbtc"])

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = f"{Constants.REST_URL}/{Constants.ENDPOINT['SYMBOL']}"
        resp = [{
            "name": f"{self.base_asset}/{self.quote_asset}",
            "state": "enabled"
        }, {
            "name": "ROGER/BTC",
            "state": "enabled"
        }]
        mock_api.get(url, body=json.dumps(resp))

        results = self.async_run_with_timeout(
            AltmarketsAPIOrderBookDataSource.fetch_trading_pairs(
                throttler=self.throttler))

        self.assertIn(self.trading_pair, results)
        self.assertIn("ROGER-BTC", results)

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_http_utils.retry_sleep_time"
    )
    def test_fetch_trading_pairs_returns_empty_on_error(
            self, mock_api, retry_sleep_time_mock):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        url = f"{Constants.REST_URL}/{Constants.ENDPOINT['SYMBOL']}"
        for i in range(Constants.API_MAX_RETRIES):
            mock_api.get(url, body=json.dumps([{"noname": "empty"}]))

        results = self.async_run_with_timeout(
            AltmarketsAPIOrderBookDataSource.fetch_trading_pairs(
                throttler=self.throttler))

        self.assertEqual(0, len(results))

    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time"
    )
    @aioresponses()
    def test_get_new_order_book(self, time_mock, mock_api):
        time_mock.return_value = 1234567899
        url = f"{Constants.REST_URL}/" \
              f"{Constants.ENDPOINT['ORDER_BOOK'].format(trading_pair=self.exchange_trading_pair)}" \
              "?limit=300"
        resp = {"timestamp": 1234567899, "bids": [], "asks": []}
        mock_api.get(url, body=json.dumps(resp))

        order_book: AltmarketsOrderBook = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertEqual(1234567899 * 1e3, order_book.snapshot_uid)

    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_http_utils.retry_sleep_time"
    )
    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time"
    )
    @aioresponses()
    def test_get_new_order_book_raises_error(self, retry_sleep_time_mock,
                                             time_mock, mock_api):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        time_mock.return_value = 1234567899
        url = f"{Constants.REST_URL}/" \
              f"{Constants.ENDPOINT['ORDER_BOOK'].format(trading_pair=self.exchange_trading_pair)}" \
              "?limit=300"
        for i in range(Constants.API_MAX_RETRIES):
            mock_api.get(url,
                         body=json.dumps({
                             "errors": {
                                 "message": "Dummy error."
                             },
                             "status": 500
                         }))

        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                self.data_source.get_new_order_book(self.trading_pair))

    @aioresponses()
    def test_listen_for_snapshots_cancelled_when_fetching_snapshot(
            self, mock_get):
        trades_queue = asyncio.Queue()

        endpoint = Constants.ENDPOINT['ORDER_BOOK'].format(
            trading_pair=r'[\w]+')
        re_url = f"{Constants.REST_URL}/{endpoint}"
        regex_url = re.compile(re_url)
        resp = {"timestamp": 1234567899, "bids": [], "asks": []}
        mock_get.get(regex_url, body=json.dumps(resp))

        self.listening_task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_order_book_snapshots(
                ev_loop=asyncio.get_event_loop(), output=trades_queue))

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

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_snapshots_logs_exception_when_fetching_snapshot(
            self, mock_get, mock_sleep):
        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)

        endpoint = Constants.ENDPOINT['ORDER_BOOK'].format(
            trading_pair=r'[\w]+')
        re_url = f"{Constants.REST_URL}/{endpoint}"
        regex_url = re.compile(re_url)
        for x in range(2):
            mock_get.get(regex_url, body=json.dumps({}))

        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(0, msg_queue.qsize())

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error occurred listening for orderbook snapshots. Retrying in 5 secs..."
            ))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_snapshots_successful(self, mock_get, mock_sleep):
        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)

        mock_response = {
            "timestamp":
            1234567890,
            "asks": [[7221.08, 6.92321326], [7220.08, 6.92321326],
                     [7222.08, 6.92321326], [7219.2, 0.69259752]],
            "bids": [[7199.27, 6.95094164], [7192.27, 6.95094164],
                     [7193.27, 6.95094164], [7196.15, 0.69481598]]
        }
        endpoint = Constants.ENDPOINT['ORDER_BOOK'].format(
            trading_pair=r'[\w]+')
        regex_url = re.compile(f"{Constants.REST_URL}/{endpoint}")
        for x in range(2):
            mock_get.get(regex_url, body=json.dumps(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.ev_loop.run_until_complete(self.listening_task)

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

        snapshot_msg: OrderBookMessage = msg_queue.get_nowait()
        self.assertEqual(snapshot_msg.update_id,
                         mock_response["timestamp"] * 1e3)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {
            "hbotusdt.trades": {
                "trades": [{
                    "date": 1234567899,
                    "tid": '3333',
                    "taker_type": "buy",
                    "price": 8772.05,
                    "amount": 0.1,
                }]
            }
        }

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        trade_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.TRADE, trade_message.type)
        self.assertEqual(1234567899, trade_message.timestamp)
        self.assertEqual('3333', trade_message.trade_id)
        self.assertEqual(self.trading_pair, trade_message.trading_pair)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades_unrecognised(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=received_messages))

        message = {"hbotusdttrades": {}}

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        with self.assertRaises(asyncio.TimeoutError):
            self.async_run_with_timeout(received_messages.get())

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Unrecognized message received from Altmarkets websocket: {'hbotusdttrades': {}}"
            ))

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades_handles_exception(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(ev_loop=self.ev_loop,
                                               output=received_messages))

        message = {"hbotusdt.trades": {"tradess": []}}

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        with self.assertRaises(asyncio.TimeoutError):
            self.async_run_with_timeout(received_messages.get())

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

    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time"
    )
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diff(self, ws_connect_mock, time_mock):
        time_mock.return_value = 1234567890
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {
            "hbotusdt.ob-inc": {
                "timestamp":
                1234567890,
                "asks": [[7220.08, 0], [7221.08, 0], [7222.08, 6.92321326],
                         [7219.2, 0.69259752]],
                "bids": [[7190.27, 0], [7192.27, 0], [7193.27, 6.95094164],
                         [7196.15, 0.69481598]]
            }
        }

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                ev_loop=self.ev_loop, output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        diff_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.DIFF, diff_message.type)
        self.assertEqual(4, len(diff_message.content.get("bids")))
        self.assertEqual(4, len(diff_message.content.get("asks")))
        self.assertEqual(1234567890, diff_message.timestamp)
        self.assertEqual(int(1234567890 * 1e3), diff_message.update_id)
        self.assertEqual(-1, diff_message.trade_id)
        self.assertEqual(self.trading_pair, diff_message.trading_pair)

    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time"
    )
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_snapshot(self, ws_connect_mock, time_mock):
        time_mock.return_value = 1234567890
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {
            "hbotusdt.ob-snap": {
                "timestamp":
                1234567890,
                "asks": [[7220.08, 6.92321326], [7221.08, 6.92321326],
                         [7222.08, 6.92321326], [7219.2, 0.69259752]],
                "bids": [[7190.27, 6.95094164], [7192.27, 6.95094164],
                         [7193.27, 6.95094164], [7196.15, 0.69481598]]
            }
        }

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                ev_loop=self.ev_loop, output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        diff_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.SNAPSHOT, diff_message.type)
        self.assertEqual(4, len(diff_message.content.get("bids")))
        self.assertEqual(4, len(diff_message.content.get("asks")))
        self.assertEqual(1234567890, diff_message.timestamp)
        self.assertEqual(int(1234567890 * 1e3), diff_message.update_id)
        self.assertEqual(-1, diff_message.trade_id)
        self.assertEqual(self.trading_pair, diff_message.trading_pair)

    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time"
    )
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diff_unrecognised(self, ws_connect_mock,
                                                     time_mock):
        time_mock.return_value = 1234567890
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {"snapcracklepop": {}}

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                ev_loop=self.ev_loop, output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        with self.assertRaises(asyncio.TimeoutError):
            self.async_run_with_timeout(received_messages.get())

        self.assertTrue(
            self._is_logged(
                "INFO",
                "Unrecognized message received from Altmarkets websocket: {'snapcracklepop': {}}"
            ))

    @patch(
        "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time"
    )
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diff_handles_exception(
            self, ws_connect_mock, time_mock):
        time_mock.return_value = "NaN"
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {".ob-snap": {}}

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                ev_loop=self.ev_loop, output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        with self.assertRaises(asyncio.TimeoutError):
            self.async_run_with_timeout(received_messages.get())

        self.assertTrue(
            self._is_logged("NETWORK",
                            "Unexpected error with WebSocket connection."))
Beispiel #2
0
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(self.throttler, [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",
           new_callable=AsyncMock)
    @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",
           new_callable=AsyncMock)
    @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",
           new_callable=AsyncMock)
    @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("websockets.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.side_effect = lambda sent_message: (
            self._raise_exception(asyncio.CancelledError)
            if CONSTANTS.WS_ORDER_BOOK_CHANNEL in sent_message
            else self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, sent_message)
        )

        self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._subscribe_level_2_response())
        self.mocking_assistant.add_websocket_text_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("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)
        )

        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",
           new_callable=AsyncMock)
    @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.mocking_assistant.add_websocket_text_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("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()

        self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._subscribe_level_2_response())
        self.mocking_assistant.add_websocket_text_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("websockets.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("websockets.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 ()"))
class TestGateIoAPIOrderBookDataSource(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        gate_io_auth = GateIoAuth(api_key="someKey", secret_key="someSecret")
        self.data_source = GateIoAPIUserStreamDataSource(gate_io_auth, trading_pairs=[self.trading_pair])

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

    def get_user_trades_mock(self) -> Dict:
        user_trades = {
            "time": 1605176741,
            "channel": "spot.usertrades",
            "event": "update",
            "result": [
                {
                    "id": 5736713,
                    "user_id": 1000001,
                    "order_id": "30784428",
                    "currency_pair": f"{self.base_asset}_{self.quote_asset}",
                    "create_time": 1605176741,
                    "create_time_ms": "1605176741123.456",
                    "side": "sell",
                    "amount": "1.00000000",
                    "role": "taker",
                    "price": "10000.00000000",
                    "fee": "0.00200000000000",
                    "point_fee": "0",
                    "gt_fee": "0",
                    "text": "apiv4"
                }
            ]
        }
        return user_trades

    def get_user_orders_mock(self) -> Dict:
        user_orders = {
            "time": 1605175506,
            "channel": "spot.orders",
            "event": "update",
            "result": [
                {
                    "id": "30784435",
                    "user": 123456,
                    "text": "t-abc",
                    "create_time": "1605175506",
                    "create_time_ms": "1605175506123",
                    "update_time": "1605175506",
                    "update_time_ms": "1605175506123",
                    "event": "put",
                    "currency_pair": f"{self.base_asset}_{self.quote_asset}",
                    "type": "limit",
                    "account": "spot",
                    "side": "sell",
                    "amount": "1",
                    "price": "10001",
                    "time_in_force": "gtc",
                    "left": "1",
                    "filled_total": "0",
                    "fee": "0",
                    "fee_currency": "USDT",
                    "point_fee": "0",
                    "gt_fee": "0",
                    "gt_discount": True,
                    "rebated_fee": "0",
                    "rebated_fee_currency": "USDT"
                }
            ]
        }
        return user_orders

    def get_user_balance_mock(self) -> Dict:
        user_balance = {
            "time": 1605248616,
            "channel": "spot.balances",
            "event": "update",
            "result": [
                {
                    "timestamp": "1605248616",
                    "timestamp_ms": "1605248616123",
                    "user": "******",
                    "currency": self.base_asset,
                    "change": "100",
                    "total": "1032951.325075926",
                    "available": "1022943.325075926"
                }
            ]
        }
        return user_balance

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_user_stream(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        output_queue = asyncio.Queue()
        self.ev_loop.create_task(self.data_source.listen_for_user_stream(self.ev_loop, output_queue))

        resp = self.get_user_trades_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)
        )
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)

        resp = self.get_user_orders_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)
        )
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)

        resp = self.get_user_balance_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)
        )
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)
class KrakenAPIUserStreamDataSourceTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.api_tier = KrakenAPITier.STARTER

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(
            build_rate_limits_by_tier(self.api_tier))
        not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg=="
        kraken_auth = KrakenAuth(api_key="someKey",
                                 secret_key=not_a_real_secret)
        self.data_source = KrakenAPIUserStreamDataSource(
            self.throttler, kraken_auth)

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

    @staticmethod
    def get_auth_response_mock() -> Dict:
        auth_resp = {
            "error": [],
            "result": {
                "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw",
                "expires": 900
            }
        }
        return auth_resp

    @staticmethod
    def get_open_orders_mock() -> List:
        open_orders = [[{
            "OGTT3Y-C6I3P-XRI6HX": {
                "status": "closed"
            }
        }, {
            "OGTT3Y-C6I3P-XRI6HX": {
                "status": "closed"
            }
        }], "openOrders", {
            "sequence": 59342
        }]
        return open_orders

    @staticmethod
    def get_own_trades_mock() -> List:
        own_trades = [[
            {
                "TDLH43-DVQXD-2KHVYY": {
                    "cost": "1000000.00000",
                    "fee": "1600.00000",
                    "margin": "0.00000",
                    "ordertxid": "TDLH43-DVQXD-2KHVYY",
                    "ordertype": "limit",
                    "pair": "XBT/EUR",
                    "postxid": "OGTT3Y-C6I3P-XRI6HX",
                    "price": "100000.00000",
                    "time": "1560516023.070651",
                    "type": "sell",
                    "vol": "1000000000.00000000"
                }
            },
        ], "ownTrades", {
            "sequence": 2948
        }]
        return own_trades

    @aioresponses()
    def test_get_auth_token(self, mocked_api):
        url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_auth_response_mock()
        mocked_api.post(regex_url, body=json.dumps(resp))

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

        self.assertEqual(ret, resp["result"]["token"])

    @aioresponses()
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_user_stream(self, mocked_api, ws_connect_mock):
        url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_auth_response_mock()
        mocked_api.post(regex_url, body=json.dumps(resp))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        output_queue = asyncio.Queue()
        self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop,
                                                    output_queue))

        resp = self.get_open_orders_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)

        resp = self.get_own_trades_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)
Beispiel #5
0
class TestGateIoAPIOrderBookDataSource(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.data_source = GateIoAPIOrderBookDataSource(
            self.throttler, trading_pairs=[self.trading_pair])

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

    def get_last_trade_instance_data_mock(self) -> List:
        last_trade_instance_data = [{
            "currency_pair": f"{self.base_asset}_{self.quote_asset}",
            "last": "0.2959",
            "lowest_ask": "0.295918",
            "highest_bid": "0.295898",
            "change_percentage": "-1.72",
            "base_volume": "78497066.828007",
            "quote_volume": "23432064.936692",
            "high_24h": "0.309372",
            "low_24h": "0.286827",
        }]
        return last_trade_instance_data

    @staticmethod
    def get_order_book_data_mock() -> Dict:
        order_book_data = {
            "id": 1890172054,
            "current": 1630644717528,
            "update": 1630644716786,
            "asks": [["0.298705", "5020"]],
            "bids": [["0.298642", "2703.17"]]
        }
        return order_book_data

    def get_trade_data_mock(self) -> Dict:
        trade_data = {
            "time": 1606292218,
            "channel": "spot.trades",
            "event": "update",
            "result": {
                "id": 309143071,
                "create_time": 1606292218,
                "create_time_ms": "1606292218213.4578",
                "side": "sell",
                "currency_pair": f"{self.base_asset}_{self.quote_asset}",
                "amount": "16.4700000000",
                "price": "0.4705000000"
            }
        }
        return trade_data

    def get_order_book_update_mock(self) -> Dict:
        ob_update = {
            "time": 1606294781,
            "channel": "spot.order_book_update",
            "event": "update",
            "result": {
                "t": 1606294781123,
                "e": "depthUpdate",
                "E": 1606294781,
                "s": f"{self.base_asset}_{self.quote_asset}",
                "U": 48776301,
                "u": 48776306,
                "b": [
                    ["19137.74", "0.0001"],
                ],
                "a": [["19137.75", "0.6135"]]
            }
        }
        return ob_update

    def get_order_book_snapshot_mock(self) -> Dict:
        ob_snapshot = {
            "time": 1606295412,
            "channel": "spot.order_book",
            "event": "update",
            "result": {
                "t": 1606295412123,
                "lastUpdateId": 48791820,
                "s": f"{self.base_asset}_{self.quote_asset}",
                "bids": [
                    ["19079.55", "0.0195"],
                ],
                "asks": [
                    ["19080.24", "0.1638"],
                ]
            }
        }
        return ob_snapshot

    @aioresponses()
    def test_get_last_trade_instance(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.TICKER_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_last_trade_instance_data_mock()
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=GateIoAPIOrderBookDataSource.get_last_traded_prices(
                trading_pairs=[self.trading_pair]))

        self.assertEqual(ret[self.trading_pair], Decimal(resp[0]["last"]))

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.SYMBOL_PATH_URL}"
        resp = [{
            "id": f"{self.base_asset}_{self.quote_asset}"
        }, {
            "id": "SOME_PAIR"
        }]
        mock_api.get(url, body=json.dumps(resp))

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

        self.assertTrue(self.trading_pair in ret)
        self.assertTrue("SOME-PAIR" in ret)

    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_utils.retry_sleep_time")
    @aioresponses()
    def test_get_order_book_data_raises(self, retry_sleep_time_mock, mock_api):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = ""
        for _ in range(CONSTANTS.API_MAX_RETRIES):
            mock_api.get(regex_url, body=json.dumps(resp), status=500)

        with self.assertRaises(IOError):
            self.async_run_with_timeout(coroutine=GateIoAPIOrderBookDataSource.
                                        get_order_book_data(self.trading_pair))

    @aioresponses()
    def test_get_order_book_data(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_book_data_mock()
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=GateIoAPIOrderBookDataSource.get_order_book_data(
                self.trading_pair))

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

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_book_data_mock()
        mock_api.get(regex_url, body=json.dumps(resp))

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

        self.assertTrue(isinstance(ret, OrderBook))

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_trade_data_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        output_queue = asyncio.Queue()

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

        self.assertTrue(not output_queue.empty())
        self.assertTrue(isinstance(output_queue.get_nowait(),
                                   OrderBookMessage))

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_update(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_order_book_update_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        output_queue = asyncio.Queue()

        self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, output_queue))
        self.mocking_assistant.run_until_all_text_messages_delivered(
            websocket_mock=ws_connect_mock.return_value)

        self.assertTrue(not output_queue.empty())
        self.assertTrue(isinstance(output_queue.get_nowait(),
                                   OrderBookMessage))

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_snapshot(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_order_book_snapshot_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        output_queue = asyncio.Queue()

        self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, output_queue))
        self.mocking_assistant.run_until_all_text_messages_delivered(
            websocket_mock=ws_connect_mock.return_value)

        self.assertTrue(not output_queue.empty())
        self.assertTrue(isinstance(output_queue.get_nowait(),
                                   OrderBookMessage))

    @aioresponses()
    def test_listen_for_order_book_snapshots(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_book_data_mock()
        mock_api.get(regex_url, body=json.dumps(resp))
        output_queue = asyncio.Queue()

        self.ev_loop.create_task(
            self.data_source.listen_for_order_book_snapshots(
                self.ev_loop, output_queue))
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertTrue(isinstance(ret, OrderBookMessage))
Beispiel #6
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 BinancePerpetualDerivativeUnitTest(unittest.TestCase):
    start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp()

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

    @patch(
        "hummingbot.connector.exchange.binance.binance_time.BinanceTime.start")
    def setUp(self, mocked_binance_time_start) -> None:
        super().setUp()
        self.ev_loop = asyncio.get_event_loop()

        self.api_responses = asyncio.Queue()

        self.ws_sent_messages = []
        self.ws_incoming_messages = asyncio.Queue()
        self.resume_test_event = asyncio.Event()
        self._finalMessage = 'FinalDummyMessage'

        self.exchange = BinancePerpetualDerivative(
            binance_perpetual_api_key="testAPIKey",
            binance_perpetual_api_secret="testSecret",
            trading_pairs=[self.trading_pair],
            domain=self.domain)

        self.mocking_assistant = NetworkMockingAssistant()

    async def _await_all_api_responses_delivered(self):
        await self.api_responses.join()

    def _get_position_risk_api_endpoint_single_position_list(
            self) -> List[Dict[str, Any]]:
        positions = [{
            "symbol": self.symbol,
            "positionAmt": "1",
            "entryPrice": "10",
            "markPrice": "11",
            "unRealizedProfit": "1",
            "liquidationPrice": "100",
            "leverage": "1",
            "maxNotionalValue": "9",
            "marginType": "cross",
            "isolatedMargin": "0",
            "isAutoAddMargin": "false",
            "positionSide": "BOTH",
            "notional": "11",
            "isolatedWallet": "0",
            "updateTime": int(self.start_timestamp),
        }]
        return positions

    def _create_ws_mock(self):
        ws = AsyncMock()
        ws.send.side_effect = lambda sent_message: self.ws_sent_messages.append(
            sent_message)
        ws.recv.side_effect = self._get_next_ws_received_message
        return ws

    async def _get_next_ws_received_message(self):
        message = await self.ws_incoming_messages.get()
        if json.loads(message) == self._finalMessage:
            self.resume_test_event.set()
        return message

    def _get_account_update_ws_event_single_position_dict(
            self) -> Dict[str, Any]:
        account_update = {
            "e": "ACCOUNT_UPDATE",
            "E": 1564745798939,
            "T": 1564745798938,
            "a": {
                "m":
                "POSITION",
                "B": [
                    {
                        "a": "USDT",
                        "wb": "122624.12345678",
                        "cw": "100.12345678",
                        "bc": "50.12345678"
                    },
                ],
                "P": [
                    {
                        "s": self.symbol,
                        "pa": "1",
                        "ep": "10",
                        "cr": "200",
                        "up": "1",
                        "mt": "cross",
                        "iw": "0.00000000",
                        "ps": "BOTH"
                    },
                ]
            }
        }
        return account_update

    @aioresponses()
    def test_existing_account_position_detected_on_positions_update(
            self, req_mock):
        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.trading_pair.replace("-", ""), self.symbol)

    @aioresponses()
    def test_account_position_updated_on_positions_update(self, req_mock):
        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 1)

        positions[0]["positionAmt"] = "2"
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 2)

    @aioresponses()
    def test_new_account_position_detected_on_positions_update(self, req_mock):
        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        req_mock.get(regex_url, body=json.dumps([]))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

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

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

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

    @aioresponses()
    def test_closed_account_position_removed_on_positions_update(
            self, req_mock):
        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        positions = self._get_position_risk_api_endpoint_single_position_list()
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

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

        positions[0]["positionAmt"] = "0"
        req_mock.get(regex_url, body=json.dumps(positions))
        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

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

    @aioresponses()
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_new_account_position_detected_on_stream_event(
            self, mock_api, ws_connect_mock):
        url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        listen_key_response = {"listenKey": "someListenKey"}
        mock_api.post(regex_url, body=json.dumps(listen_key_response))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.ev_loop.create_task(self.exchange._user_stream_tracker.start())

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

        account_update = self._get_account_update_ws_event_single_position_dict(
        )
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value, json.dumps(account_update))

        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        positions = self._get_position_risk_api_endpoint_single_position_list()
        mock_api.get(regex_url, body=json.dumps(positions))

        self.ev_loop.create_task(self.exchange._user_stream_event_listener())
        asyncio.get_event_loop().run_until_complete(asyncio.sleep(1))

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

    @aioresponses()
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_account_position_updated_on_stream_event(self, mock_api,
                                                      ws_connect_mock):
        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        positions = self._get_position_risk_api_endpoint_single_position_list()
        mock_api.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

        url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        listen_key_response = {"listenKey": "someListenKey"}
        mock_api.post(regex_url, body=json.dumps(listen_key_response))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        self.ev_loop.create_task(self.exchange._user_stream_tracker.start())
        self.ev_loop.run_until_complete(
            self._await_all_api_responses_delivered())

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 1)

        account_update = self._get_account_update_ws_event_single_position_dict(
        )
        account_update["a"]["P"][0]["pa"] = 2
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value, json.dumps(account_update))

        self.ev_loop.create_task(self.exchange._user_stream_event_listener())
        self.ev_loop.run_until_complete(asyncio.sleep(0.3))

        self.assertEqual(len(self.exchange.account_positions), 1)
        pos = list(self.exchange.account_positions.values())[0]
        self.assertEqual(pos.amount, 2)

    @aioresponses()
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_closed_account_position_removed_on_stream_event(
            self, mock_api, ws_connect_mock):
        url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL,
                             domain=self.domain,
                             api_version=CONSTANTS.API_VERSION_V2)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        positions = self._get_position_risk_api_endpoint_single_position_list()
        mock_api.get(regex_url, body=json.dumps(positions))

        task = self.ev_loop.create_task(self.exchange._update_positions())
        self.ev_loop.run_until_complete(task)

        url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        listen_key_response = {"listenKey": "someListenKey"}
        mock_api.post(regex_url, body=json.dumps(listen_key_response))
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )

        self.ev_loop.create_task(self.exchange._user_stream_tracker.start())
        self.ev_loop.run_until_complete(
            self._await_all_api_responses_delivered())

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

        account_update = self._get_account_update_ws_event_single_position_dict(
        )
        account_update["a"]["P"][0]["pa"] = 0
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value, json.dumps(account_update))

        self.ev_loop.create_task(self.exchange._user_stream_event_listener())
        self.ev_loop.run_until_complete(asyncio.sleep(0.3))

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

    @aioresponses()
    def test_set_position_mode_initial_mode_is_none(self, mock_api):
        self.assertIsNone(self.exchange.position_mode)

        url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        get_position_mode_response = {
            "dualSidePosition": False  # True: Hedge Mode; False: One-way Mode
        }
        post_position_mode_response = {"code": 200, "msg": "success"}
        mock_api.get(regex_url, body=json.dumps(get_position_mode_response))
        mock_api.post(regex_url, body=json.dumps(post_position_mode_response))

        task = self.ev_loop.create_task(
            self.exchange._set_position_mode(PositionMode.HEDGE))
        self.ev_loop.run_until_complete(task)

        self.assertEqual(PositionMode.HEDGE, self.exchange.position_mode)

    @aioresponses()
    def test_set_position_initial_mode_unchanged(self, mock_api):
        self.exchange._position_mode = PositionMode.ONEWAY
        url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        get_position_mode_response = {
            "dualSidePosition": False  # True: Hedge Mode; False: One-way Mode
        }

        mock_api.get(regex_url, body=json.dumps(get_position_mode_response))
        task = self.ev_loop.create_task(
            self.exchange._set_position_mode(PositionMode.ONEWAY))
        self.ev_loop.run_until_complete(task)

        self.assertEqual(PositionMode.ONEWAY, self.exchange.position_mode)

    @aioresponses()
    def test_set_position_mode_diff_initial_mode_change_successful(
            self, mock_api):
        self.exchange._position_mode = PositionMode.ONEWAY
        url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        get_position_mode_response = {
            "dualSidePosition": False  # True: Hedge Mode; False: One-way Mode
        }
        post_position_mode_response = {"code": 200, "msg": "success"}

        mock_api.get(regex_url, body=json.dumps(get_position_mode_response))
        mock_api.post(regex_url, body=json.dumps(post_position_mode_response))

        task = self.ev_loop.create_task(
            self.exchange._set_position_mode(PositionMode.HEDGE))
        self.ev_loop.run_until_complete(task)

        self.assertEqual(PositionMode.HEDGE, self.exchange.position_mode)

    @aioresponses()
    def test_set_position_mode_diff_initial_mode_change_fail(self, mock_api):
        self.exchange._position_mode = PositionMode.ONEWAY
        url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL,
                             domain=self.domain)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        get_position_mode_response = {
            "dualSidePosition": False  # True: Hedge Mode; False: One-way Mode
        }
        post_position_mode_response = {
            'code': -4059,
            'msg': 'No need to change position side.'
        }

        mock_api.get(regex_url, body=json.dumps(get_position_mode_response))
        mock_api.post(regex_url, body=json.dumps(post_position_mode_response))

        task = self.ev_loop.create_task(
            self.exchange._set_position_mode(PositionMode.HEDGE))
        self.ev_loop.run_until_complete(task)

        self.assertEqual(PositionMode.ONEWAY, self.exchange.position_mode)
class TestAltmarketsAPIUserStreamDataSource(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.mocking_assistant = NetworkMockingAssistant()
        altmarkets_auth = AltmarketsAuth(api_key="someKey", secret_key="someSecret")
        self.data_source = AltmarketsAPIUserStreamDataSource(AsyncThrottler(Constants.RATE_LIMITS), altmarkets_auth=altmarkets_auth, trading_pairs=[self.trading_pair])

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

    def get_user_trades_mock(self) -> Dict:
        user_trades = {
            "trade": {
                "amount": "1.0",
                "created_at": 1615978645,
                "id": 9618578,
                "market": "rogerbtc",
                "order_id": 2324774,
                "price": "0.00000004",
                "side": "sell",
                "taker_type": "sell",
                "total": "0.00000004"
            }
        }
        return user_trades

    def get_user_orders_mock(self) -> Dict:
        user_orders = {
            "order": {
                "id": 9401,
                "market": "rogerbtc",
                "kind": "ask",
                "side": "sell",
                "ord_type": "limit",
                "price": "0.00000099",
                "avg_price": "0.00000099",
                "state": "wait",
                "origin_volume": "7000.0",
                "remaining_volume": "2810.1",
                "executed_volume": "4189.9",
                "at": 1596481983,
                "created_at": 1596481983,
                "updated_at": 1596553643,
                "trades_count": 272
            }
        }
        return user_orders

    def get_user_balance_mock(self) -> Dict:
        user_balance = {
            "balance": {
                "currency": self.base_asset,
                "balance": "1032951.325075926",
                "locked": "1022943.325075926",
            }
        }
        return user_balance

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_user_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        output_queue = asyncio.Queue()
        self.ev_loop.create_task(self.data_source.listen_for_user_stream(self.ev_loop, output_queue))

        resp = self.get_user_trades_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        ret = self.async_run_with_timeout(coroutine=output_queue.get())
        self.assertEqual(ret, resp)

        resp = self.get_user_orders_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)

        resp = self.get_user_balance_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        ret = self.async_run_with_timeout(coroutine=output_queue.get())

        self.assertEqual(ret, resp)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_skips_subscribe_unsubscribe_messages_updates_last_recv_time(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = {
            "success": {
                "message": "subscribed",
                "time": 1632223851,
                "streams": "trade"
            }
        }
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        resp = {
            "success": {
                "message": "unsubscribed",
                "time": 1632223851,
                "streams": "trade"
            }
        }
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))

        output_queue = asyncio.Queue()
        self.ev_loop.create_task(self.data_source.listen_for_user_stream(self.ev_loop, output_queue))
        self.mocking_assistant.run_until_all_text_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())
        np.testing.assert_allclose([time.time()], self.data_source.last_recv_time, rtol=1)
Beispiel #9
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 KrakenAPIOrderBookDataSourceTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

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

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

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

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

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

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

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

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

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

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

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

        self.assertTrue(isinstance(ret, OrderBook))

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

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

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

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

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

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        resp = self.get_trade_data_mock()
        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(resp))
        output_queue = asyncio.Queue()

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

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

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

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.api_key = 'testAPIKey'
        cls.secret = 'testSecret'
        cls.memo = '001'

        cls.account_id = 528
        cls.username = '******'
        cls.oms_id = 1
        cls.ev_loop = asyncio.get_event_loop()

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

        throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        auth_assistant = BitmartAuth(api_key=self.api_key,
                                     secret_key=self.secret,
                                     memo=self.memo)

        self.data_source = BitmartAPIUserStreamDataSource(auth_assistant, throttler)
        self.data_source.logger().setLevel(1)
        self.data_source.logger().addHandler(self)
        self.data_source._trading_pairs = ["HBOT-USDT"]

        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 _is_logged(self, log_level: str, message: str) -> bool:
        return any(record.levelname == log_level and record.getMessage() == message
                   for record in self.log_records)

    def _raise_exception(self, exception_class):
        raise exception_class

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_authenticates_and_subscribes_to_events(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        initial_last_recv_time = self.data_source.last_recv_time

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop,
                                                    messages))
        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value,
            json.dumps({"event": "login"}))

        # 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.return_value, json.dumps('dummyMessage'))

        first_received_message = self.ev_loop.run_until_complete(messages.get())

        self.assertEqual('dummyMessage', first_received_message)

        self.assertTrue(self._is_logged('INFO', "Authenticating to User Stream..."))
        self.assertTrue(self._is_logged('INFO', "Successfully authenticated to User Stream."))
        self.assertTrue(self._is_logged('INFO', "Successfully subscribed to all Private channels."))

        sent_messages = self.mocking_assistant.text_messages_sent_through_websocket(ws_connect_mock.return_value)
        self.assertEqual(2, len(sent_messages))
        auth_req = json.loads(sent_messages[0])
        sub_req = json.loads(sent_messages[1])
        self.assertTrue("op" in auth_req and "args" in auth_req and "testAPIKey" in auth_req["args"])
        self.assertEqual({"op": "subscribe", "args": ["spot/user/order:HBOT_USDT"]},
                         sub_req)
        self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_fails_when_authentication_fails(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception)

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop,
                                                    messages))
        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value,
            json.dumps({"errorCode": "test code", "errorMessage": "test err message"})
        )
        try:
            self.ev_loop.run_until_complete(self.listening_task)
        except Exception:
            pass
        self.assertTrue(self._is_logged("ERROR", "WebSocket login errored with message: test err message"))
        self.assertTrue(self._is_logged("ERROR", "Error occurred when authenticating to user stream."))
        self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. "
                                                 "Retrying after 30 seconds..."))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_initialization(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(self.ev_loop,
                                                        messages))
            self.ev_loop.run_until_complete(self.listening_task)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_authentication(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(asyncio.CancelledError)
            if "testAPIKey" in sent_message
            else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(
                sent_message))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(self.ev_loop,
                                                        messages))
            self.ev_loop.run_until_complete(self.listening_task)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_events_subscription(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(asyncio.CancelledError)
            if "order:HBOT_USDT" in sent_message
            else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(sent_message)
        )
        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(self.ev_loop,
                                                        messages))
            # Add the authentication response for the websocket
            self.mocking_assistant.add_websocket_text_message(
                ws_connect_mock.return_value,
                json.dumps({"event": "login"})
            )
            self.ev_loop.run_until_complete(self.listening_task)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_logs_exception_details_during_initialization(self, ws_connect_mock):
        ws_connect_mock.side_effect = Exception
        with self.assertRaises(Exception):
            self.listening_task = self.ev_loop.create_task(self.data_source._init_websocket_connection())
            self.ev_loop.run_until_complete(self.listening_task)
        self.assertTrue(self._is_logged("NETWORK", "Unexpected error occured with BitMart WebSocket Connection"))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_logs_exception_details_during_authentication(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(Exception)
            if "testAPIKey" in sent_message
            else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(sent_message))
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception)

        try:
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(self.ev_loop,
                                                        messages))
            self.ev_loop.run_until_complete(self.listening_task)
        except Exception:
            pass

        self.assertTrue(self._is_logged("ERROR", "Error occurred when authenticating to user stream."))
        self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. "
                                                 "Retrying after 30 seconds..."))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_logs_exception_during_events_subscription(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(Exception)
            if "order:HBOT_USDT" in sent_message
            else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(sent_message)
        )
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception)

        try:
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(self.ev_loop,
                                                        messages))
            # Add the authentication response for the websocket
            self.mocking_assistant.add_websocket_text_message(
                ws_connect_mock.return_value,
                json.dumps({"event": "login"}))
            self.ev_loop.run_until_complete(self.listening_task)
        except Exception:
            pass

        self.assertTrue(self._is_logged("ERROR", "Error occured during subscribing to Bitmart private channels."))
        self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. "
                                                 "Retrying after 30 seconds..."))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_invalid_json_message_logged(self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception)

        try:
            self.listening_task = self.ev_loop.create_task(
                self.data_source.listen_for_user_stream(self.ev_loop,
                                                        messages))
            # Add the authentication response for the websocket
            self.mocking_assistant.add_websocket_text_message(
                ws_connect_mock.return_value,
                json.dumps({"event": "login"}))
            # Add invalid json message
            self.mocking_assistant.add_websocket_text_message(ws_connect_mock.return_value, 'invalid message')
            self.ev_loop.run_until_complete(self.listening_task)
        except Exception:
            pass

        self.assertTrue(self._is_logged("ERROR", "Unexpected error when parsing BitMart user_stream message. "))
        self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. "
                                                 "Retrying after 30 seconds..."))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listen_for_user_stream_inner_messages_recv_timeout(self, ws_connect_mock):
        self.data_source.MESSAGE_TIMEOUT = 0.1

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.ping.side_effect = lambda: done_callback_event.set()

        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value,
            json.dumps({"event": "login"}))

        done_callback_event = asyncio.Event()
        message_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop,
                                                    message_queue)
        )

        self.ev_loop.run_until_complete(done_callback_event.wait())

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listen_for_user_stream_inner_messages_recv_timeout_ping_timeout(self, ws_connect_mock):
        self.data_source.PING_TIMEOUT = 0.1
        self.data_source.MESSAGE_TIMEOUT = 0.1

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.close.side_effect = lambda: done_callback_event.set()
        ws_connect_mock.return_value.ping.side_effect = NetworkMockingAssistant.async_partial(
            self.mocking_assistant._get_next_websocket_text_message, ws_connect_mock.return_value
        )

        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value,
            json.dumps({"event": "login"}))

        done_callback_event = asyncio.Event()
        message_queue = asyncio.Queue()
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop,
                                                    message_queue)
        )

        self.ev_loop.run_until_complete(done_callback_event.wait())

        self.assertTrue(self._is_logged("WARNING", "WebSocket ping timed out. Going to reconnect..."))
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)
Beispiel #13
0
class NdaxAPIUserStreamDataSourceTests(TestCase):
    # the level is required to receive logs from the data source loger
    level = 0

    def setUp(self) -> None:
        super().setUp()
        self.uid = '001'
        self.api_key = 'testAPIKey'
        self.secret = 'testSecret'
        self.account_id = 528
        self.username = '******'
        self.oms_id = 1
        self.log_records = []
        self.listening_task = None

        throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        auth_assistant = NdaxAuth(uid=self.uid,
                                  api_key=self.api_key,
                                  secret_key=self.secret,
                                  account_name=self.username)
        self.data_source = NdaxAPIUserStreamDataSource(throttler,
                                                       auth_assistant)
        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 _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 _authentication_response(self, authenticated: bool) -> str:
        user = {
            "UserId": 492,
            "UserName": "******",
            "Email": "*****@*****.**",
            "EmailVerified": True,
            "AccountId": self.account_id,
            "OMSId": self.oms_id,
            "Use2FA": True
        }
        payload = {
            "Authenticated": authenticated,
            "SessionToken": "74e7c5b0-26b1-4ca5-b852-79b796b0e599",
            "User": user,
            "Locked": False,
            "Requires2FA": False,
            "EnforceEnable2FA": False,
            "TwoFAType": None,
            "TwoFAToken": None,
            "errormsg": None
        }
        message = {
            "m": 1,
            "i": 1,
            "n": CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME,
            "o": json.dumps(payload)
        }

        return json.dumps(message)

    def _raise_exception(self, exception_class):
        raise exception_class

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_authenticates_and_subscribes_to_events(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        initial_last_recv_time = self.data_source.last_recv_time

        self.listening_task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_user_stream(asyncio.get_event_loop(),
                                                    messages))
        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value, self._authentication_response(True))
        # 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.return_value, json.dumps('dummyMessage'))

        first_received_message = asyncio.get_event_loop().run_until_complete(
            messages.get())

        self.assertEqual('dummyMessage', first_received_message)

        self.assertTrue(
            self._is_logged('INFO', "Authenticating to User Stream..."))
        self.assertTrue(
            self._is_logged('INFO',
                            "Successfully authenticated to User Stream."))
        self.assertTrue(
            self._is_logged('INFO', "Successfully subscribed to user events."))

        sent_messages = self.mocking_assistant.text_messages_sent_through_websocket(
            ws_connect_mock.return_value)
        self.assertEqual(2, len(sent_messages))
        authentication_request = sent_messages[0]
        subscription_request = sent_messages[1]
        self.assertEqual(
            CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME,
            NdaxWebSocketAdaptor.endpoint_from_raw_message(
                authentication_request))
        self.assertEqual(
            CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME,
            NdaxWebSocketAdaptor.endpoint_from_raw_message(
                subscription_request))
        subscription_payload = NdaxWebSocketAdaptor.payload_from_raw_message(
            subscription_request)
        expected_payload = {"AccountId": self.account_id, "OMSId": self.oms_id}
        self.assertEqual(expected_payload, subscription_payload)

        self.assertGreater(self.data_source.last_recv_time,
                           initial_last_recv_time)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_fails_when_authentication_fails(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(
            Exception)

        self.listening_task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_user_stream(asyncio.get_event_loop(),
                                                    messages))
        # Add the authentication response for the websocket
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value, self._authentication_response(False))

        try:
            asyncio.get_event_loop().run_until_complete(self.listening_task)
        except Exception:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR", "Error occurred when authenticating to user stream "
                "(Could not authenticate websocket connection with NDAX)"))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. "
                "(Could not authenticate websocket connection with NDAX)"))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_initialization(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.side_effect = asyncio.CancelledError

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_user_stream(
                    asyncio.get_event_loop(), messages))
            asyncio.get_event_loop().run_until_complete(self.listening_task)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_authentication(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(asyncio.CancelledError)
            if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message else
            self.mocking_assistant._sent_websocket_text_messages[
                ws_connect_mock.return_value].append(sent_message))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_user_stream(
                    asyncio.get_event_loop(), messages))
            asyncio.get_event_loop().run_until_complete(self.listening_task)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_canceled_when_cancel_exception_during_events_subscription(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(asyncio.CancelledError)
            if CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message
            else self.mocking_assistant._sent_websocket_text_messages[
                ws_connect_mock.return_value].append(sent_message))

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_user_stream(
                    asyncio.get_event_loop(), messages))
            # Add the authentication response for the websocket
            self.mocking_assistant.add_websocket_text_message(
                ws_connect_mock.return_value,
                self._authentication_response(True))
            asyncio.get_event_loop().run_until_complete(self.listening_task)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_logs_exception_details_during_initialization(
            self, ws_connect_mock):
        ws_connect_mock.side_effect = Exception

        with self.assertRaises(Exception):
            self.listening_task = asyncio.get_event_loop().create_task(
                self.data_source._init_websocket_connection())
            asyncio.get_event_loop().run_until_complete(self.listening_task)
        self.assertTrue(
            self._is_logged(
                "NETWORK",
                "Unexpected error occurred during ndax WebSocket Connection ()"
            ))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_logs_exception_details_during_authentication(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            self._raise_exception(Exception)
            if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message else
            self.mocking_assistant._sent_websocket_text_messages[
                ws_connect_mock.return_value].append(sent_message))
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(
            Exception)

        try:
            self.listening_task = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_user_stream(
                    asyncio.get_event_loop(), messages))
            asyncio.get_event_loop().run_until_complete(self.listening_task)
        except Exception:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Error occurred when authenticating to user stream ()"))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. ()"
            ))

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listening_process_logs_exception_during_events_subscription(
            self, ws_connect_mock):
        messages = asyncio.Queue()
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.send.side_effect = lambda sent_message: (
            CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message
            and self._raise_exception(Exception))
        # Make the close function raise an exception to finish the execution
        ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(
            Exception)

        try:
            self.listening_task = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_user_stream(
                    asyncio.get_event_loop(), messages))
            # Add the authentication response for the websocket
            self.mocking_assistant.add_websocket_text_message(
                ws_connect_mock.return_value,
                self._authentication_response(True))
            asyncio.get_event_loop().run_until_complete(self.listening_task)
        except Exception:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Error occurred subscribing to ndax private channels ()"))
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. ()"
            ))
class CoinzoomAPIOrderBookDataSourceTests(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.api_key = "testKey"
        cls.api_secret_key = "testSecretKey"
        cls.username = "******"
        cls.throttler = AsyncThrottler(Constants.RATE_LIMITS)

    def setUp(self) -> None:
        super().setUp()
        self.listening_task = None
        self.data_source = CoinzoomAPIOrderBookDataSource(
            throttler=self.throttler, trading_pairs=[self.trading_pair])
        self.mocking_assistant = NetworkMockingAssistant()

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

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

    @aioresponses()
    def test_get_last_traded_prices(self, mock_api):
        url = f"{Constants.REST_URL}/{Constants.ENDPOINT['TICKER']}"
        resp = {
            f"{self.base_asset}_{self.quote_asset}": {
                "last_price": 51234.56
            }
        }
        mock_api.get(url, body=json.dumps(resp))

        results = self.async_run_with_timeout(
            CoinzoomAPIOrderBookDataSource.get_last_traded_prices(
                trading_pairs=[self.trading_pair], throttler=self.throttler))

        self.assertIn(self.trading_pair, results)
        self.assertEqual(Decimal("51234.56"), results[self.trading_pair])

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = f"{Constants.REST_URL}/{Constants.ENDPOINT['SYMBOL']}"
        resp = [{
            "symbol": f"{self.base_asset}/{self.quote_asset}"
        }, {
            "symbol": "BTC/USDT"
        }]
        mock_api.get(url, body=json.dumps(resp))

        results = self.async_run_with_timeout(
            CoinzoomAPIOrderBookDataSource.fetch_trading_pairs(
                throttler=self.throttler))

        self.assertIn(self.trading_pair, results)
        self.assertIn("BTC-USDT", results)

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = f"{Constants.REST_URL}/" \
              f"{Constants.ENDPOINT['ORDER_BOOK'].format(trading_pair=self.base_asset+'_'+self.quote_asset)}"
        resp = {"timestamp": 1234567899, "bids": [], "asks": []}
        mock_api.get(url, body=json.dumps(resp))

        order_book: CoinzoomOrderBook = self.async_run_with_timeout(
            self.data_source.get_new_order_book(self.trading_pair))

        self.assertEqual(1234567899, order_book.snapshot_uid)

    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {
            "ts": [
                f"{self.base_asset}/{self.quote_asset}", 8772.05, 0.01,
                "2020-01-16T21:02:23Z"
            ]
        }

        self.listening_task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_trades(
                ev_loop=asyncio.get_event_loop(), output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        trade_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.TRADE, trade_message.type)
        self.assertEqual(
            int(dateparse("2020-01-16T21:02:23Z").timestamp() * 1e3),
            trade_message.timestamp)
        self.assertEqual(trade_message.timestamp, trade_message.trade_id)
        self.assertEqual(self.trading_pair, trade_message.trading_pair)

    @patch(
        "hummingbot.connector.exchange.coinzoom.coinzoom_api_order_book_data_source.CoinzoomAPIOrderBookDataSource._time"
    )
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diff(self, ws_connect_mock, time_mock):
        time_mock.return_value = 1234567890
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        received_messages = asyncio.Queue()

        message = {
            "oi":
            f"{self.base_asset}/{self.quote_asset}",
            "b": [["9"], ["5"], ["7", 7193.27, 6.95094164],
                  ["8", 7196.15, 0.69481598]],
            "s": [["2"], ["1"], ["4", 7222.08, 6.92321326],
                  ["6", 7219.2, 0.69259752]]
        }

        self.listening_task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_order_book_diffs(
                ev_loop=asyncio.get_event_loop(), output=received_messages))

        self.mocking_assistant.add_websocket_text_message(
            websocket_mock=ws_connect_mock.return_value,
            message=json.dumps(message))
        diff_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.DIFF, diff_message.type)
        self.assertEqual(1234567890 * 1e3, diff_message.timestamp)
        self.assertEqual(diff_message.timestamp, diff_message.update_id)
        self.assertEqual(-1, diff_message.trade_id)
        self.assertEqual(self.trading_pair, diff_message.trading_pair)
class BitmartAPIOrderBookDataSourceUnitTests(unittest.TestCase):
    # logging.Level required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()

        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"

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

        self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)
        self.data_source = BitmartAPIOrderBookDataSource(
            self.throttler, [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 _order_book_snapshot_example(self):
        return {
            "data": {
                "timestamp":
                1527777538000,
                "buys": [
                    {
                        "amount": "4800.00",
                        "total": "4800.00",
                        "price": "0.000767",
                        "count": "1"
                    },
                    {
                        "amount": "99996475.79",
                        "total": "100001275.79",
                        "price": "0.000201",
                        "count": "1"
                    },
                ],
                "sells": [
                    {
                        "amount": "100.00",
                        "total": "100.00",
                        "price": "0.007000",
                        "count": "1"
                    },
                    {
                        "amount": "6997.00",
                        "total": "7097.00",
                        "price": "1.000000",
                        "count": "1"
                    },
                ]
            }
        }

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

    @aioresponses()
    def test_get_last_traded_prices(self, mock_get):
        mock_response: Dict[Any] = {
            "message": "OK",
            "code": 1000,
            "trace": "6e42c7c9-fdc5-461b-8fd1-b4e2e1b9ed57",
            "data": {
                "tickers": [{
                    "symbol":
                    "COINALPHA_HBOT",
                    "last_price":
                    "1.00",
                    "quote_volume_24h":
                    "201477650.88000",
                    "base_volume_24h":
                    "25186.48000",
                    "high_24h":
                    "8800.00",
                    "low_24h":
                    "1.00",
                    "open_24h":
                    "8800.00",
                    "close_24h":
                    "1.00",
                    "best_ask":
                    "0.00",
                    "best_ask_size":
                    "0.00000",
                    "best_bid":
                    "0.00",
                    "best_bid_size":
                    "0.00000",
                    "fluctuation":
                    "-0.9999",
                    "url":
                    "https://www.bitmart.com/trade?symbol=COINALPHA_HBOT"
                }]
            }
        }
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_LAST_TRADING_PRICES_PATH_URL}"
        )
        mock_get.get(regex_url, body=json.dumps(mock_response))

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

        self.assertEqual(results[self.trading_pair], float("1.00"))

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_get):
        mock_response: List[Any] = {
            "code": 1000,
            "trace": "886fb6ae-456b-4654-b4e0-d681ac05cea1",
            "message": "OK",
            "data": {
                "symbols": [
                    "COINALPHA_HBOT",
                    "ANOTHER_MARKET",
                ]
            }
        }
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_TRADING_PAIRS_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps(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-MARKET" in results)

    @aioresponses()
    def test_fetch_trading_pairs_with_error_status_in_response(self, mock_get):
        mock_response = {}
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_TRADING_PAIRS_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps(mock_response))
        result = self.ev_loop.run_until_complete(
            self.data_source.fetch_trading_pairs())
        self.assertEqual(0, len(result))

    @aioresponses()
    def test_get_order_book_data(self, mock_get):
        mock_response: Dict[str, Any] = self._order_book_snapshot_example()
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps(mock_response))

        results = self.ev_loop.run_until_complete(
            asyncio.gather(
                self.data_source.get_order_book_data(self.trading_pair)))

        result = results[0]
        self.assertTrue("timestamp" in result)
        self.assertTrue("buys" in result)
        self.assertTrue("sells" in result)
        self.assertGreaterEqual(len(result["buys"]) + len(result["sells"]), 0)
        self.assertEqual(mock_response["data"]["buys"][0], result["buys"][0])

    @aioresponses()
    def test_get_order_book_data_raises_exception_when_response_has_error_code(
            self, mock_get):
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}")
        mock_get.get(regex_url, status=100, body=json.dumps({}))

        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} at {CONSTANTS.EXCHANGE_NAME}. "
            f"HTTP status is {100}.")

    @aioresponses()
    def test_get_new_order_book(self, mock_get):
        mock_response: Dict[str, Any] = self._order_book_snapshot_example()
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps(mock_response))

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

        self.assertTrue(type(result) == OrderBook)
        self.assertEqual(result.snapshot_uid,
                         mock_response["data"]["timestamp"])

    def test_listen_for_snapshots_cancelled_when_fetching_snapshot(self):
        trades_queue = asyncio.Queue()
        task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_order_book_snapshots(
                ev_loop=asyncio.get_event_loop(), output=trades_queue))

        with self.assertRaises(asyncio.CancelledError):
            task.cancel()
            asyncio.get_event_loop().run_until_complete(task)

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.bitmart.bitmart_api_order_book_data_source.BitmartAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_snapshots_logs_exception_when_fetching_snapshot(
            self, mock_get, mock_sleep):
        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)
        sync_queue.append(2)

        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps({}))

        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 = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_order_book_snapshots(
                    asyncio.get_event_loop(), msg_queue))
            asyncio.get_event_loop().run_until_complete(self.listening_task)

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

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

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.bitmart.bitmart_api_order_book_data_source.BitmartAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_snapshots_successful(self, mock_get, mock_sleep):
        # the queue and the division by zero error are used just to synchronize the test
        sync_queue = deque()
        sync_queue.append(1)
        sync_queue.append(2)

        mock_response: Dict[str, Any] = self._order_book_snapshot_example()
        regex_url = re.compile(
            f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}")
        mock_get.get(regex_url, body=json.dumps(mock_response))

        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 = asyncio.get_event_loop().create_task(
                self.data_source.listen_for_order_book_snapshots(
                    asyncio.get_event_loop(), msg_queue))
            asyncio.get_event_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,
                         mock_response["data"]["timestamp"])

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_cancelled_when_listening(
            self, mock_ws):
        mock_ws.return_value = self.mocking_assistant.create_websocket_mock()
        msg_queue: asyncio.Queue = asyncio.Queue()
        self.listening_task = asyncio.get_event_loop().create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

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

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

    @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

        resp = {
            "table":
            "spot/depth500",
            "data": [{
                "asks": [["161.96", "7.37567"]],
                "bids": [["161.94", "4.552355"]],
                "symbol": "ETH_USDT",
                "ms_t": 1542337219120
            }]
        }
        self.mocking_assistant.add_websocket_text_message(
            mock_ws.return_value, ujson.dumps(resp))
        BitmartAPIOrderBookDataSource._trading_pairs = ["ETH-USDT"]
        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))
        first_msg: OrderBookMessage = self.ev_loop.run_until_complete(
            msg_queue.get())
        self.assertTrue(first_msg.type == OrderBookMessageType.SNAPSHOT)

    @patch("websockets.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("websockets.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 bitmart WebSocket Connection ()"
            ))

    def _trade_ws_messsage(self):
        resp = {
            "table":
            "spot/trade",
            "data": [{
                "symbol": "ETH_USDT",
                "price": "162.12",
                "side": "buy",
                "size": "11.085",
                "s_t": 1542337219
            }, {
                "symbol": "ETH_USDT",
                "price": "163.12",
                "side": "buy",
                "size": "15",
                "s_t": 1542337238
            }]
        }
        return ujson.dumps(resp)

    @patch('websockets.connect', new_callable=AsyncMock)
    def test_listen_for_trades(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

        # Add message to be processed after subscriptions, to unlock the test
        self.mocking_assistant.add_websocket_text_message(
            mock_ws.return_value, self._trade_ws_messsage())
        BitmartAPIOrderBookDataSource._trading_pairs = ["ETH-USDT"]

        self.listening_task = self.ev_loop.create_task(
            self.data_source.listen_for_trades(self.ev_loop, msg_queue))
        trade1: OrderBookMessage = self.ev_loop.run_until_complete(
            msg_queue.get())
        trade2: OrderBookMessage = self.ev_loop.run_until_complete(
            msg_queue.get())

        self.assertTrue(msg_queue.empty())
        self.assertEqual(1542337219 * 1000, int(trade1.trade_id))
        self.assertEqual(1542337238 * 1000, int(trade2.trade_id))
class TestKucoinAPIUserStreamDataSource(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.ev_loop = asyncio.get_event_loop()
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.api_key = "someKey"
        cls.api_passphrase = "somePassPhrase"
        cls.api_secret_key = "someSecretKey"

    def setUp(self) -> None:
        super().setUp()
        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.auth = KucoinAuth(self.api_key, self.api_passphrase,
                               self.api_secret_key)
        self.data_source = KucoinAPIUserStreamDataSource(
            self.throttler, self.auth)
        self.mocking_assistant = NetworkMockingAssistant()

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

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

    @aioresponses()
    def test_get_listen_key_raises(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL
        mock_api.post(url, status=500)

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

    @aioresponses()
    def test_get_listen_key(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL
        resp = self.get_listen_key_mock()
        mock_api.post(url, body=json.dumps(resp))

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

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

    @aioresponses()
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_to_user_stream(self, mock_api, ws_connect_mock):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL
        resp = self.get_listen_key_mock()
        mock_api.post(url, body=json.dumps(resp))

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        msg = "someMsg"
        msg_queue = asyncio.Queue()
        self.mocking_assistant.add_websocket_text_message(
            ws_connect_mock.return_value, json.dumps(msg))

        self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop, msg_queue))
        self.mocking_assistant.run_until_all_text_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(not msg_queue.empty())

        queued = msg_queue.get_nowait()

        self.assertEqual(msg, queued)

    @aioresponses()
    @patch("websockets.connect", new_callable=AsyncMock)
    def test_listen_for_user_stream_closes_ws_on_exception(
            self, mock_api, ws_connect_mock):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL
        resp = self.get_listen_key_mock()
        mock_api.post(url, body=json.dumps(resp))
        raised_event = asyncio.Event()

        async def raise_exception():
            raised_event.set()
            raise IOError

        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock(
        )
        ws_connect_mock.return_value.recv.side_effect = raise_exception

        self.ev_loop.create_task(
            self.data_source.listen_for_user_stream(self.ev_loop,
                                                    asyncio.Queue()))
        self.async_run_with_timeout(coroutine=raised_event.wait())

        ws_connect_mock.return_value.close.assert_called()