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

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

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

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

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

        self.connector._set_trading_pair_symbol_map(
            bidict({self.ex_trading_pair: self.trading_pair}))
Exemple #2
0
    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.mocking_assistant = NetworkMockingAssistant()
        self.exchange = GateIoExchange(self.api_key,
                                       self.api_secret,
                                       trading_pairs=[self.trading_pair])
        self.event_listener = MockEventListener()

        self.exchange.logger().setLevel(1)
        self.exchange.logger().addHandler(self)
def get_gate_io(ec):
    exchange = GateIoExchange(
        gate_io_api_key=creds.k,
        gate_io_secret_key=creds.s,
        trading_pairs=[ec.pair],
    )
    return exchange
    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.async_tasks: List[asyncio.Task] = []

        self.mocking_assistant = NetworkMockingAssistant()
        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = GateIoExchange(
            client_config_map=client_config_map,
            gate_io_api_key="",
            gate_io_secret_key="",
            trading_pairs=[],
            trading_required=False)

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

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

        self.connector._set_trading_pair_symbol_map(bidict({self.ex_trading_pair: self.trading_pair}))
Exemple #5
0
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.ev_loop = asyncio.get_event_loop()

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: GateIoExchange = GateIoExchange(
            gate_io_api_key=API_KEY,
            gate_io_secret_key=API_SECRET,
            trading_pairs=[cls.trading_pair],
            trading_required=True)
        print("Initializing Gate.Io market... this will take about a minute.")
        cls.clock.add_iterator(cls.connector)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")
class TestGateIoExchange(unittest.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 = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}"
        cls.api_key = "someKey"
        cls.api_secret = "someSecret"

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

        self.exchange = GateIoExchange(self.api_key,
                                       self.api_secret,
                                       trading_pairs=[self.trading_pair])
        self.event_listener = EventLogger()

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

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

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

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

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

    @staticmethod
    def get_currency_data_mock() -> List:
        currency_data = [{
            "currency": "GT",
            "delisted": False,
            "withdraw_disabled": False,
            "withdraw_delayed": False,
            "deposit_disabled": False,
            "trade_disabled": False,
        }]
        return currency_data

    def get_trading_rules_mock(self) -> List:
        trading_rules = [{
            "id": f"{self.base_asset}_{self.quote_asset}",
            "base": self.base_asset,
            "quote": self.quote_asset,
            "fee": "0.2",
            "min_base_amount": "0.001",
            "min_quote_amount": "1.0",
            "amount_precision": 3,
            "precision": 6,
            "trade_status": "tradable",
            "sell_start": 1516378650,
            "buy_start": 1516378650,
        }]
        return trading_rules

    def get_order_create_response_mock(
            self,
            cancelled: bool = False,
            exchange_order_id: str = "someExchId") -> Dict:
        order_create_resp_mock = {
            "id": exchange_order_id,
            "text": "t-123456",
            "create_time": "1548000000",
            "update_time": "1548000100",
            "create_time_ms": 1548000000123,
            "update_time_ms": 1548000100123,
            "currency_pair": f"{self.base_asset}_{self.quote_asset}",
            "status": "cancelled" if cancelled else "open",
            "type": "limit",
            "account": "spot",
            "side": "buy",
            "iceberg": "0",
            "amount": "1",
            "price": "5.00032",
            "time_in_force": "gtc",
            "left": "0.5",
            "filled_total": "2.50016",
            "fee": "0.005",
            "fee_currency": "ETH",
            "point_fee": "0",
            "gt_fee": "0",
            "gt_discount": False,
            "rebated_fee": "0",
            "rebated_fee_currency": "BTC",
        }
        return order_create_resp_mock

    def get_in_flight_order(
            self,
            client_order_id: str,
            exchange_order_id: str = "someExchId") -> GateIoInFlightOrder:
        order = GateIoInFlightOrder(client_order_id,
                                    exchange_order_id,
                                    self.trading_pair,
                                    OrderType.LIMIT,
                                    TradeType.BUY,
                                    price=Decimal("5.1"),
                                    amount=Decimal("1"),
                                    creation_timestamp=1640001112.0)
        return order

    def get_user_balances_mock(self) -> List:
        user_balances = [
            {
                "currency": self.base_asset,
                "available": "968.8",
                "locked": "0",
            },
            {
                "currency": self.quote_asset,
                "available": "543.9",
                "locked": "0",
            },
        ]
        return user_balances

    def get_open_order_mock(self,
                            exchange_order_id: str = "someExchId") -> List:
        open_orders = [{
            "currency_pair":
            f"{self.base_asset}_{self.quote_asset}",
            "total":
            1,
            "orders": [{
                "id": exchange_order_id,
                "text": f"{CONSTANTS.HBOT_ORDER_ID}-{exchange_order_id}",
                "create_time": "1548000000",
                "update_time": "1548000100",
                "currency_pair": f"{self.base_asset}_{self.quote_asset}",
                "status": "open",
                "type": "limit",
                "account": "spot",
                "side": "buy",
                "amount": "1",
                "price": "5.00032",
                "time_in_force": "gtc",
                "left": "0.5",
                "filled_total": "2.50016",
                "fee": "0.005",
                "fee_currency": "ETH",
                "point_fee": "0",
                "gt_fee": "0",
                "gt_discount": False,
                "rebated_fee": "0",
                "rebated_fee_currency": "BTC",
            }],
        }]
        return open_orders

    def get_order_trade_response(
            self,
            order: GateIoInFlightOrder,
            is_completely_filled: bool = False) -> Dict[str, Any]:
        order_amount = order.amount
        if not is_completely_filled:
            order_amount = float(Decimal("0.5") * order_amount)
        base_asset, quote_asset = order.trading_pair.split(
            "-")[0], order.trading_pair.split("-")[1]
        return [{
            "id":
            5736713,
            "user_id":
            1000001,
            "order_id":
            order.exchange_order_id,
            "currency_pair":
            order.trading_pair,
            "create_time":
            1605176741,
            "create_time_ms":
            "1605176741123.456",
            "side":
            "buy" if order.trade_type == TradeType.BUY else "sell",
            "amount":
            str(order_amount),
            "role":
            "maker",
            "price":
            str(order.price),
            "fee":
            "0.00200000000000",
            "fee_currency":
            base_asset if order.trade_type == TradeType.BUY else quote_asset,
            "point_fee":
            "0",
            "gt_fee":
            "0",
            "text":
            order.client_order_id,
        }]

    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_utils.retry_sleep_time")
    @aioresponses()
    def test_check_network_not_connected(self, retry_sleep_time_mock,
                                         mock_api):
        retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.NETWORK_CHECK_PATH_URL}"
        resp = ""
        for i in range(CONSTANTS.API_MAX_RETRIES):
            mock_api.get(url, status=500, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=self.exchange.check_network())

        self.assertEqual(ret, NetworkStatus.NOT_CONNECTED)

    @aioresponses()
    def test_check_network(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.NETWORK_CHECK_PATH_URL}"
        resp = self.get_currency_data_mock()
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=self.exchange.check_network())

        self.assertEqual(ret, NetworkStatus.CONNECTED)

    @aioresponses()
    def test_update_trading_rules_polling_loop(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.SYMBOL_PATH_URL}"
        resp = self.get_trading_rules_mock()
        called_event = asyncio.Event()
        mock_api.get(url,
                     body=json.dumps(resp),
                     callback=lambda *args, **kwargs: called_event.set())

        self.ev_loop.create_task(self.exchange._trading_rules_polling_loop())
        self.async_run_with_timeout(called_event.wait())

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

    @aioresponses()
    def test_create_order(self, mock_api):
        trading_rules = self.get_trading_rules_mock()
        self.exchange._trading_rules = self.exchange._format_trading_rules(
            trading_rules)

        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_CREATE_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_create_response_mock()
        mock_api.post(regex_url, body=json.dumps(resp))

        self.exchange.add_listener(MarketEvent.BuyOrderCreated,
                                   self.event_listener)

        order_id = "someId"
        self.async_run_with_timeout(coroutine=self.exchange._create_order(
            trade_type=TradeType.BUY,
            order_id=order_id,
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("5.1"),
        ))

        self.assertEqual(1, len(self.event_listener.event_log))

        event = self.event_listener.event_log[0]

        self.assertEqual(order_id, event.order_id)
        self.assertTrue(order_id in self.exchange.in_flight_orders)

    @aioresponses()
    def test_create_order_when_order_is_instantly_closed(self, mock_api):
        trading_rules = self.get_trading_rules_mock()
        self.exchange._trading_rules = self.exchange._format_trading_rules(
            trading_rules)

        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_CREATE_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_create_response_mock()
        resp["status"] = "closed"
        mock_api.post(regex_url, body=json.dumps(resp))

        event_logger = EventLogger()
        self.exchange.add_listener(MarketEvent.BuyOrderCreated, event_logger)

        order_id = "someId"
        self.async_run_with_timeout(coroutine=self.exchange._create_order(
            trade_type=TradeType.BUY,
            order_id=order_id,
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("5.1"),
        ))

        self.assertEqual(1, len(event_logger.event_log))
        self.assertEqual(order_id, event_logger.event_log[0].order_id)
        self.assertTrue(order_id in self.exchange.in_flight_orders)

    @aioresponses()
    def test_order_with_less_amount_than_allowed_is_not_created(
            self, mock_api):
        trading_rules = self.get_trading_rules_mock()
        self.exchange._trading_rules = self.exchange._format_trading_rules(
            trading_rules)

        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_CREATE_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.post(regex_url,
                      exception=Exception("The request should never happen"))

        self.exchange.add_listener(MarketEvent.BuyOrderCreated,
                                   self.event_listener)

        order_id = "someId"
        self.async_run_with_timeout(coroutine=self.exchange._create_order(
            trade_type=TradeType.BUY,
            order_id=order_id,
            trading_pair=self.trading_pair,
            amount=Decimal("0.0001"),
            order_type=OrderType.LIMIT,
            price=Decimal("5.1"),
        ))

        self.assertEqual(0, len(self.event_listener.event_log))
        self.assertNotIn(order_id, self.exchange.in_flight_orders)
        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Error submitting BUY LIMIT order to gate_io for 0.000 COINALPHA-HBOT 5.100000 - Buy order amount 0.000 is lower than the minimum order size 0.001..",
            ))

    @patch("hummingbot.client.hummingbot_application.HummingbotApplication")
    @aioresponses()
    def test_create_order_fails(self, _, mock_api):
        trading_rules = self.get_trading_rules_mock()
        self.exchange._trading_rules = self.exchange._format_trading_rules(
            trading_rules)

        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_CREATE_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_create_response_mock(cancelled=True)
        mock_api.post(regex_url, body=json.dumps(resp))

        self.exchange.add_listener(MarketEvent.BuyOrderCreated,
                                   self.event_listener)

        order_id = "someId"
        self.async_run_with_timeout(coroutine=self.exchange._create_order(
            trade_type=TradeType.BUY,
            order_id=order_id,
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("5.1"),
        ))

        self.assertEqual(0, len(self.event_listener.event_log))
        self.assertTrue(order_id not in self.exchange.in_flight_orders)

    @aioresponses()
    def test_execute_cancel(self, mock_api):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_CREATE_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        resp = self.get_order_create_response_mock(cancelled=True)
        mock_api.delete(regex_url, body=json.dumps(resp))

        client_order_id = "someId"
        exchange_order_id = "someExchId"
        self.exchange._in_flight_orders[
            client_order_id] = self.get_in_flight_order(
                client_order_id, exchange_order_id)

        self.exchange.add_listener(MarketEvent.OrderCancelled,
                                   self.event_listener)

        self.async_run_with_timeout(coroutine=self.exchange._execute_cancel(
            self.trading_pair, client_order_id))

        self.assertEqual(1, len(self.event_listener.event_log))

        event = self.event_listener.event_log[0]

        self.assertEqual(client_order_id, event.order_id)
        self.assertTrue(client_order_id not in self.exchange.in_flight_orders)

    def test_cancel_order_not_present_in_inflight_orders(self):
        client_order_id = "test-id"
        event_logger = EventLogger()
        self.exchange.add_listener(MarketEvent.OrderCancelled, event_logger)

        result = self.async_run_with_timeout(
            coroutine=self.exchange._execute_cancel(self.trading_pair,
                                                    client_order_id))

        self.assertEqual(0, len(event_logger.event_log))
        self.assertTrue(
            self._is_logged(
                "WARNING",
                f"Failed to cancel order {client_order_id}. Order not found in inflight orders."
            ))
        self.assertFalse(result.success)

    def test_stop_tracking_order_exceed_not_found_limit(self):
        client_order_id = "someId"
        exchange_order_id = "someExchId"
        self.exchange._in_flight_orders[
            client_order_id] = self.get_in_flight_order(
                client_order_id, exchange_order_id)
        self.assertEqual(1, len(self.exchange.in_flight_orders))

        self.exchange._order_not_found_records[
            client_order_id] = self.exchange.ORDER_NOT_EXIST_CONFIRMATION_COUNT

        self.exchange.stop_tracking_order_exceed_not_found_limit(
            self.exchange._in_flight_orders[client_order_id])
        self.assertEqual(0, len(self.exchange.in_flight_orders))

    @aioresponses()
    def test_update_order_status_unable_to_fetch_trades(self, mock_api):
        client_order_id = "someId"
        exchange_order_id = "someExchId"
        self.exchange._in_flight_orders[
            client_order_id] = self.get_in_flight_order(
                client_order_id, exchange_order_id)
        self.exchange._order_not_found_records[
            client_order_id] = self.exchange.ORDER_NOT_EXIST_CONFIRMATION_COUNT

        # Order Trade Updates
        order_trade_updates_url = f"{CONSTANTS.REST_URL}/{CONSTANTS.MY_TRADES_PATH_URL}"
        regex_order_trade_updates_url = re.compile(
            f"^{order_trade_updates_url}")

        order_trade_updates_called_event = asyncio.Event()
        error_resp = {
            "label": "ORDER_NOT_FOUND",
            "message": "ORDER_NOT_FOUND",
            "status": 400,
        }
        mock_api.get(
            regex_order_trade_updates_url,
            body=json.dumps(error_resp),
            callback=lambda *args, **kwargs: order_trade_updates_called_event.
            set(),
        )

        self.async_tasks.append(
            self.ev_loop.create_task(self.exchange._update_order_status()))
        self.async_run_with_timeout(order_trade_updates_called_event.wait())

        self._is_logged(
            "WARNING",
            f"Failed to fetch trade updates for order {client_order_id}. Response: {error_resp}"
        )
        self.assertEqual(0, len(self.exchange.in_flight_orders))

    @aioresponses()
    def test_update_order_status_unable_to_fetch_order_status(self, mock_api):
        client_order_id = "someId"
        exchange_order_id = "someExchId"
        self.exchange._in_flight_orders[
            client_order_id] = self.get_in_flight_order(
                client_order_id, exchange_order_id)
        self.exchange._order_not_found_records[
            client_order_id] = self.exchange.ORDER_NOT_EXIST_CONFIRMATION_COUNT

        # Order Trade Updates
        order_trade_updates_url = f"{CONSTANTS.REST_URL}/{CONSTANTS.MY_TRADES_PATH_URL}"
        regex_order_trade_updates_url = re.compile(
            f"^{order_trade_updates_url}")
        order_trade_updates_resp = self.get_order_trade_response(
            order=self.exchange._in_flight_orders[client_order_id])
        order_trade_updates_called_event = asyncio.Event()
        mock_api.get(
            regex_order_trade_updates_url,
            body=json.dumps(order_trade_updates_resp),
            callback=lambda *args, **kwargs: order_trade_updates_called_event.
            set(),
        )

        # Order Status Updates
        order_status_url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_STATUS_PATH_URL}"
        regex_order_status_url = re.compile(
            f"^{order_status_url[:-4]}".replace(".",
                                                r"\.").replace("?", r"\?"))

        error_resp = {
            "label": "ORDER_NOT_FOUND",
            "message": "ORDER_NOT_FOUND",
            "status": 400,
        }
        order_status_called_event = asyncio.Event()
        mock_api.get(
            regex_order_status_url,
            body=json.dumps(error_resp),
            callback=lambda *args, **kwargs: order_status_called_event.set(),
        )

        self.async_tasks.append(
            self.ev_loop.create_task(self.exchange._update_order_status()))
        self.async_run_with_timeout(order_trade_updates_called_event.wait())
        self.async_run_with_timeout(order_trade_updates_called_event.wait())

        self._is_logged(
            "WARNING",
            f"Failed to fetch order updates for order {client_order_id}. Response: {error_resp}"
        )
        self.assertEqual(0, len(self.exchange.in_flight_orders))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_exchange.GateIoExchange.current_timestamp"
    )
    def test_status_polling_loop(self, mock_api, current_ts_mock):
        # Order Balance Updates
        balances_url = f"{CONSTANTS.REST_URL}/{CONSTANTS.USER_BALANCES_PATH_URL}"
        balances_resp = self.get_user_balances_mock()
        balances_called_event = asyncio.Event()
        mock_api.get(
            balances_url,
            body=json.dumps(balances_resp),
            callback=lambda *args, **kwargs: balances_called_event.set())

        client_order_id = "someId"
        exchange_order_id = "someExchId"
        self.exchange._in_flight_orders[
            client_order_id] = self.get_in_flight_order(
                client_order_id, exchange_order_id)

        # Order Trade Updates
        order_trade_updates_url = f"{CONSTANTS.REST_URL}/{CONSTANTS.MY_TRADES_PATH_URL}"
        regex_order_trade_updates_url = re.compile(
            f"^{order_trade_updates_url}")
        order_trade_updates_resp = self.get_order_trade_response(
            order=self.exchange._in_flight_orders[client_order_id])
        order_trade_updates_called_event = asyncio.Event()
        mock_api.get(
            regex_order_trade_updates_url,
            body=json.dumps(order_trade_updates_resp),
            callback=lambda *args, **kwargs: order_trade_updates_called_event.
            set(),
        )

        # Order Status Updates
        order_status_url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_STATUS_PATH_URL}"
        regex_order_status_url = re.compile(
            f"^{order_status_url[:-4]}".replace(".",
                                                r"\.").replace("?", r"\?"))
        order_status_resp = self.get_order_create_response_mock(
            cancelled=False, exchange_order_id=exchange_order_id)
        order_status_called_event = asyncio.Event()
        mock_api.get(
            regex_order_status_url,
            body=json.dumps(order_status_resp),
            callback=lambda *args, **kwargs: order_status_called_event.set(),
        )

        current_ts_mock.return_value = time.time()

        self.ev_loop.create_task(self.exchange._status_polling_loop())
        self.exchange._poll_notifier.set()
        self.async_run_with_timeout(balances_called_event.wait())
        self.async_run_with_timeout(order_trade_updates_called_event.wait())
        self.async_run_with_timeout(order_status_called_event.wait())

        self.assertEqual(self.exchange.available_balances[self.base_asset],
                         Decimal("968.8"))
        self.assertTrue(client_order_id in self.exchange.in_flight_orders)

        partially_filled_order = self.exchange.in_flight_orders[
            client_order_id]
        self.assertEqual(Decimal("0.5"),
                         partially_filled_order.executed_amount_base)

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

        ret = self.async_run_with_timeout(
            coroutine=self.exchange.get_open_orders())

        self.assertTrue(len(ret) == 1)

    def test_process_trade_message_matching_order_by_internal_order_id(self):
        self.exchange.start_tracking_order(
            order_id="OID-1",
            exchange_order_id="5736713",
            trading_pair=self.trading_pair,
            trade_type=TradeType.BUY,
            price=Decimal(10000),
            amount=Decimal(1),
            order_type=OrderType.LIMIT,
        )

        trade_message = {
            "id": 5736713,
            "user_id": 1000001,
            "order_id": "EOID-1",
            "currency_pair": self.ex_trading_pair,
            "create_time": 1605176741,
            "create_time_ms": "1605176741123.456",
            "side": "buy",
            "amount": "0.50000000",
            "role": "maker",
            "price": "10000.00000000",
            "fee": "0.00200000000000",
            "fee_currency": self.quote_asset,
            "point_fee": "0",
            "gt_fee": "0",
            "text": "OID-1",
        }

        self.exchange._process_trade_message(trade_message)

        order = self.exchange.in_flight_orders["OID-1"]

        self.assertIn(str(trade_message["id"]), order.trade_update_id_set)
        self.assertEqual(Decimal(0.5), order.executed_amount_base)
        self.assertEqual(Decimal(5000), order.executed_amount_quote)
        self.assertEqual(Decimal("0.002"), order.fee_paid)
        self.assertEqual(self.quote_asset, order.fee_asset)

    @patch("hummingbot.connector.utils.get_tracking_nonce_low_res")
    def test_client_order_id_on_order(self, mocked_nonce):
        mocked_nonce.return_value = 7

        result = self.exchange.buy(
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("2"),
        )
        expected_client_order_id = get_new_client_order_id(
            is_buy=True,
            trading_pair=self.trading_pair,
            hbot_order_id_prefix=CONSTANTS.HBOT_ORDER_ID,
            max_id_len=CONSTANTS.MAX_ID_LEN,
        )

        self.assertEqual(result, expected_client_order_id)

        result = self.exchange.sell(
            trading_pair=self.trading_pair,
            amount=Decimal("1"),
            order_type=OrderType.LIMIT,
            price=Decimal("2"),
        )
        expected_client_order_id = get_new_client_order_id(
            is_buy=False,
            trading_pair=self.trading_pair,
            hbot_order_id_prefix=CONSTANTS.HBOT_ORDER_ID,
            max_id_len=CONSTANTS.MAX_ID_LEN,
        )

        self.assertEqual(result, expected_client_order_id)
Exemple #7
0
    def test_orders_saving_and_restoration(self):
        config_path = "test_config"
        strategy_name = "test_strategy"
        sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS,
                                   db_path=self.db_path)
        order_id = None
        recorder = MarketsRecorder(sql, [self.connector], config_path,
                                   strategy_name)
        recorder.start()

        try:
            self.connector._in_flight_orders.clear()
            self.assertEqual(0, len(self.connector.tracking_states))

            # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event.
            current_bid_price: Decimal = self.connector.get_price(
                self.trading_pair, True)
            price: Decimal = current_bid_price * Decimal("0.8")
            price = self.connector.quantize_order_price(
                self.trading_pair, price)

            amount: Decimal = self.order_amount
            amount = self.connector.quantize_order_amount(
                self.trading_pair, amount)

            cl_order_id = self._place_order(True, amount,
                                            OrderType.LIMIT_MAKER, price, 1)
            order_created_event = self.ev_loop.run_until_complete(
                self.event_logger.wait_for(BuyOrderCreatedEvent))
            self.assertEqual(cl_order_id, order_created_event.order_id)

            # Verify tracking states
            self.assertEqual(1, len(self.connector.tracking_states))
            self.assertEqual(cl_order_id,
                             list(self.connector.tracking_states.keys())[0])

            # Verify orders from recorder
            recorded_orders: List[
                Order] = recorder.get_orders_for_config_and_market(
                    config_path, self.connector)
            self.assertEqual(1, len(recorded_orders))
            self.assertEqual(cl_order_id, recorded_orders[0].id)

            # Verify saved market states
            saved_market_states: MarketState = recorder.get_market_states(
                config_path, self.connector)
            self.assertIsNotNone(saved_market_states)
            self.assertIsInstance(saved_market_states.saved_state, dict)
            self.assertGreater(len(saved_market_states.saved_state), 0)

            # Close out the current market and start another market.
            self.connector.stop(self._clock)
            self.ev_loop.run_until_complete(asyncio.sleep(5))
            self.clock.remove_iterator(self.connector)
            for event_tag in self.events:
                self.connector.remove_listener(event_tag, self.event_logger)
            # Clear the event loop
            self.event_logger.clear()
            new_connector = GateIoExchange(API_KEY, API_SECRET,
                                           [self.trading_pair], True)
            for event_tag in self.events:
                new_connector.add_listener(event_tag, self.event_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [new_connector], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, new_connector)
            self.clock.add_iterator(new_connector)
            self.ev_loop.run_until_complete(self.wait_til_ready(new_connector))
            self.assertEqual(0, len(new_connector.limit_orders))
            self.assertEqual(0, len(new_connector.tracking_states))
            new_connector.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(new_connector.limit_orders))
            self.assertEqual(1, len(new_connector.tracking_states))

            # Cancel the order and verify that the change is saved.
            self._cancel_order(cl_order_id, new_connector)
            self.ev_loop.run_until_complete(
                self.event_logger.wait_for(OrderCancelledEvent))
            recorder.save_market_states(config_path, new_connector)
            order_id = None
            self.assertEqual(0, len(new_connector.limit_orders))
            self.assertEqual(0, len(new_connector.tracking_states))
            saved_market_states = recorder.get_market_states(
                config_path, new_connector)
            self.assertEqual(0, len(saved_market_states.saved_state))
        finally:
            if order_id is not None:
                self.connector.cancel(self.trading_pair, cl_order_id)
                self.run_parallel(
                    self.event_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
class TestGateIoAPIUserStreamDataSource(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        output_queue = asyncio.Queue()

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

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

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

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

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

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

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

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

        output_queue = asyncio.Queue()

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

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())

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

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

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

        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(
            mock_ws.return_value)

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

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

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

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

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

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

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

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

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

        self.mocking_assistant = NetworkMockingAssistant()
        client_config_map = ClientConfigAdapter(ClientConfigMap())
        self.connector = GateIoExchange(
            client_config_map=client_config_map,
            gate_io_api_key="",
            gate_io_secret_key="",
            trading_pairs=[],
            trading_required=False)

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

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

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

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

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

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

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

    @staticmethod
    def get_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": self.ex_trading_pair,
                "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": self.ex_trading_pair,
                "U": 48776301,
                "u": 48776306,
                "b": [
                    [
                        "19137.74",
                        "0.0001"
                    ],
                ],
                "a": [
                    [
                        "19137.75",
                        "0.6135"
                    ]
                ]
            }
        }
        return ob_update

    def get_order_book_diff_mock(self, asks: List[str], bids: List[str]) -> Dict:
        ob_snapshot = {
            "time": 1606295412,
            "channel": "spot.order_book_update",
            "event": "update",
            "result": {
                "t": 1606295412123,
                "e": "depthUpdate",
                "E": 1606295412,
                "s": self.ex_trading_pair,
                "U": 48791820,
                "u": 48791830,
                "b": [bids],
                "a": [asks],
            }
        }
        return ob_snapshot

    @aioresponses()
    def test_get_order_book_data_raises(self, mock_api):
        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=self.data_source.get_new_order_book(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=self.data_source.get_new_order_book(self.trading_pair)
        )

        bid_entries = list(ret.bid_entries())
        ask_entries = list(ret.ask_entries())
        self.assertEqual(1, len(bid_entries))
        self.assertEqual(float(resp["bids"][0][0]), bid_entries[0].price)
        self.assertEqual(float(resp["bids"][0][1]), bid_entries[0].amount)
        self.assertEqual(int(resp["id"]), bid_entries[0].update_id)
        self.assertEqual(1, len(ask_entries))
        self.assertEqual(float(resp["asks"][0][0]), ask_entries[0].price)
        self.assertEqual(float(resp["asks"][0][1]), ask_entries[0].amount)
        self.assertEqual(int(resp["id"]), ask_entries[0].update_id)

    @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("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = self.get_trade_data_mock()
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

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

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

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_trades_skips_subscribe_unsubscribe_messages(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp1 = {"time": 1632223851, "channel": CONSTANTS.TRADES_ENDPOINT_NAME, "event": "subscribe", "result": {"status": "success"}}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp1)
        )
        resp2 = {
            "time": 1632223851, "channel": CONSTANTS.TRADES_ENDPOINT_NAME, "event": "unsubscribe", "result": {"status": "success"}
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp2)
        )

        output_queue = asyncio.Queue()
        t = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.async_tasks.append(t)
        t = self.ev_loop.create_task(self.data_source.listen_for_trades(self.ev_loop, output_queue))
        self.async_tasks.append(t)
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())
        self.assertFalse(
            self._is_logged(
                "ERROR",
                f"Unexpected error while parsing ws trades message {resp1}."
            )
        )
        self.assertFalse(
            self._is_logged(
                "ERROR",
                f"Unexpected error while parsing ws trades message {resp2}."
            )
        )

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_api_order_book_data_source.GateIoAPIOrderBookDataSource._sleep")
    def test_listen_for_trades_logs_error_when_exception_happens(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        incomplete_response = {
            "time": 1606292218,
            "channel": "spot.trades",
            "event": "update",
            "result": {
                "id": 309143071,
                "currency_pair": f"{self.base_asset}_{self.quote_asset}",
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(incomplete_response)
        )
        output_queue = asyncio.Queue()

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

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

    @patch("aiohttp.client.ClientSession.ws_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_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.async_tasks.append(t)
        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(websocket_mock=ws_connect_mock.return_value)

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

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_api_order_book_data_source.GateIoAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_update_logs_error_when_exception_happens(self, _, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        incomplete_response = {
            "time": 1606294781,
            "channel": "spot.order_book_update",
            "event": "update",
            "result": {}
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(incomplete_response)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.async_tasks.append(t)
        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(websocket_mock=ws_connect_mock.return_value)

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

    @patch("aiohttp.client.ClientSession.ws_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()
        asks = ["19080.24", "0.1638"]
        bids = ["19079.55", "0.0195"]
        resp = self.get_order_book_diff_mock(asks=asks, bids=bids)
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        output_queue = asyncio.Queue()

        t = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.async_tasks.append(t)
        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(websocket_mock=ws_connect_mock.return_value)

        self.assertTrue(not output_queue.empty())

        msg = output_queue.get_nowait()

        self.assertTrue(isinstance(msg, OrderBookMessage))
        self.assertEqual(asks, msg.content["asks"][0], msg=f"{msg}")

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    def test_listen_for_order_book_diffs_snapshot_skips_subscribe_unsubscribe_messages(self, ws_connect_mock):
        ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        resp = {"time": 1632223851, "channel": "spot.usertrades", "event": "subscribe", "result": {"status": "success"}}
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )
        resp = {
            "time": 1632223851, "channel": "spot.usertrades", "event": "unsubscribe", "result": {"status": "success"}
        }
        self.mocking_assistant.add_websocket_aiohttp_message(
            ws_connect_mock.return_value, json.dumps(resp)
        )

        output_queue = asyncio.Queue()
        t = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.async_tasks.append(t)
        t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue))
        self.async_tasks.append(t)
        self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value)

        self.assertTrue(output_queue.empty())

    @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()

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

        self.assertTrue(isinstance(ret, OrderBookMessage))

    @aioresponses()
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_api_order_book_data_source.GateIoAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_order_book_snapshots_logs_error_when_exception_happens(
            self,
            mock_api,
            sleep_mock):
        url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}"
        regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, exception=Exception("Test Error"))
        output_queue = asyncio.Queue()
        sleep_mock.side_effect = asyncio.CancelledError

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

        try:
            self.async_run_with_timeout(t)
        except asyncio.CancelledError:
            # Ignore the CancelledError raised by the mocked _sleep
            pass

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

    @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock)
    @patch(
        "hummingbot.connector.exchange.gate_io.gate_io_api_order_book_data_source.GateIoAPIOrderBookDataSource._sleep",
        new_callable=AsyncMock)
    def test_listen_for_subscriptions_logs_error_when_exception_happens(self, sleep_mock, ws_connect_mock):
        # ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.side_effect = Exception("Test Error")
        sleep_mock.side_effect = asyncio.CancelledError

        t = self.ev_loop.create_task(self.data_source.listen_for_subscriptions())
        self.async_tasks.append(t)

        try:
            self.async_run_with_timeout(t)
        except asyncio.CancelledError:
            # Ignore the CancelledError raised by the mocked _sleep
            pass

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