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 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}))
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)
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..." ))