class AltmarketsAPIOrderBookDataSourceTests(TestCase): # logging.Level required to receive logs from the exchange level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "HBOT" cls.quote_asset = "USDT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.exchange_trading_pair = convert_to_exchange_trading_pair( cls.trading_pair) cls.api_key = "testKey" cls.api_secret_key = "testSecretKey" cls.username = "******" cls.throttler = AsyncThrottler(Constants.RATE_LIMITS) def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.data_source = AltmarketsAPIOrderBookDataSource( throttler=self.throttler, trading_pairs=[self.trading_pair]) self.mocking_assistant = NetworkMockingAssistant() self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def test_throttler_rates(self): self.assertEqual( str(self.throttler._rate_limits[0]), str(self.data_source._get_throttler_instance()._rate_limits[0])) self.assertEqual( str(self.throttler._rate_limits[-1]), str(self.data_source._get_throttler_instance()._rate_limits[-1])) @aioresponses() def test_get_last_traded_prices(self, mock_api): url = f"{Constants.REST_URL}/{Constants.ENDPOINT['TICKER_SINGLE'].format(trading_pair=self.exchange_trading_pair)}" resp = {"ticker": {"last": 51234.56}} mock_api.get(url, body=json.dumps(resp)) results = self.async_run_with_timeout( AltmarketsAPIOrderBookDataSource.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertIn(self.trading_pair, results) self.assertEqual(Decimal("51234.56"), results[self.trading_pair]) @aioresponses() @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_http_utils.retry_sleep_time" ) def test_get_last_traded_prices_multiple(self, mock_api, retry_sleep_time_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 url = f"{Constants.REST_URL}/{Constants.ENDPOINT['TICKER']}" resp = { f"{self.exchange_trading_pair}": { "ticker": { "last": 51234.56 } }, "rogerbtc": { "ticker": { "last": 0.00000002 } }, "btcusdt": { "ticker": { "last": 51234.56 } }, "hbotbtc": { "ticker": { "last": 0.9 } }, } mock_api.get(url, body=json.dumps(resp)) results = self.async_run_with_timeout( AltmarketsAPIOrderBookDataSource.get_last_traded_prices( trading_pairs=[ self.trading_pair, 'rogerbtc', 'btcusdt', 'hbotbtc' ], throttler=self.throttler)) self.assertIn(self.trading_pair, results) self.assertEqual(Decimal("51234.56"), results[self.trading_pair]) self.assertEqual(Decimal("0.00000002"), results["rogerbtc"]) self.assertEqual(Decimal("51234.56"), results["btcusdt"]) self.assertEqual(Decimal("0.9"), results["hbotbtc"]) @aioresponses() def test_fetch_trading_pairs(self, mock_api): url = f"{Constants.REST_URL}/{Constants.ENDPOINT['SYMBOL']}" resp = [{ "name": f"{self.base_asset}/{self.quote_asset}", "state": "enabled" }, { "name": "ROGER/BTC", "state": "enabled" }] mock_api.get(url, body=json.dumps(resp)) results = self.async_run_with_timeout( AltmarketsAPIOrderBookDataSource.fetch_trading_pairs( throttler=self.throttler)) self.assertIn(self.trading_pair, results) self.assertIn("ROGER-BTC", results) @aioresponses() @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_http_utils.retry_sleep_time" ) def test_fetch_trading_pairs_returns_empty_on_error( self, mock_api, retry_sleep_time_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 url = f"{Constants.REST_URL}/{Constants.ENDPOINT['SYMBOL']}" for i in range(Constants.API_MAX_RETRIES): mock_api.get(url, body=json.dumps([{"noname": "empty"}])) results = self.async_run_with_timeout( AltmarketsAPIOrderBookDataSource.fetch_trading_pairs( throttler=self.throttler)) self.assertEqual(0, len(results)) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time" ) @aioresponses() def test_get_new_order_book(self, time_mock, mock_api): time_mock.return_value = 1234567899 url = f"{Constants.REST_URL}/" \ f"{Constants.ENDPOINT['ORDER_BOOK'].format(trading_pair=self.exchange_trading_pair)}" \ "?limit=300" resp = {"timestamp": 1234567899, "bids": [], "asks": []} mock_api.get(url, body=json.dumps(resp)) order_book: AltmarketsOrderBook = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertEqual(1234567899 * 1e3, order_book.snapshot_uid) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_http_utils.retry_sleep_time" ) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time" ) @aioresponses() def test_get_new_order_book_raises_error(self, retry_sleep_time_mock, time_mock, mock_api): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 time_mock.return_value = 1234567899 url = f"{Constants.REST_URL}/" \ f"{Constants.ENDPOINT['ORDER_BOOK'].format(trading_pair=self.exchange_trading_pair)}" \ "?limit=300" for i in range(Constants.API_MAX_RETRIES): mock_api.get(url, body=json.dumps({ "errors": { "message": "Dummy error." }, "status": 500 })) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) @aioresponses() def test_listen_for_snapshots_cancelled_when_fetching_snapshot( self, mock_get): trades_queue = asyncio.Queue() endpoint = Constants.ENDPOINT['ORDER_BOOK'].format( trading_pair=r'[\w]+') re_url = f"{Constants.REST_URL}/{endpoint}" regex_url = re.compile(re_url) resp = {"timestamp": 1234567899, "bids": [], "asks": []} mock_get.get(regex_url, body=json.dumps(resp)) self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_order_book_snapshots( ev_loop=asyncio.get_event_loop(), output=trades_queue)) with self.assertRaises(asyncio.CancelledError): self.listening_task.cancel() asyncio.get_event_loop().run_until_complete(self.listening_task) @aioresponses() @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._sleep", new_callable=AsyncMock) def test_listen_for_snapshots_logs_exception_when_fetching_snapshot( self, mock_get, mock_sleep): # the queue and the division by zero error are used just to synchronize the test sync_queue = deque() sync_queue.append(1) endpoint = Constants.ENDPOINT['ORDER_BOOK'].format( trading_pair=r'[\w]+') re_url = f"{Constants.REST_URL}/{endpoint}" regex_url = re.compile(re_url) for x in range(2): mock_get.get(regex_url, body=json.dumps({})) mock_sleep.side_effect = lambda delay: 1 / 0 if len( sync_queue) == 0 else sync_queue.pop() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(ZeroDivisionError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred listening for orderbook snapshots. Retrying in 5 secs..." )) @aioresponses() @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._sleep", new_callable=AsyncMock) def test_listen_for_snapshots_successful(self, mock_get, mock_sleep): # the queue and the division by zero error are used just to synchronize the test sync_queue = deque() sync_queue.append(1) mock_response = { "timestamp": 1234567890, "asks": [[7221.08, 6.92321326], [7220.08, 6.92321326], [7222.08, 6.92321326], [7219.2, 0.69259752]], "bids": [[7199.27, 6.95094164], [7192.27, 6.95094164], [7193.27, 6.95094164], [7196.15, 0.69481598]] } endpoint = Constants.ENDPOINT['ORDER_BOOK'].format( trading_pair=r'[\w]+') regex_url = re.compile(f"{Constants.REST_URL}/{endpoint}") for x in range(2): mock_get.get(regex_url, body=json.dumps(mock_response)) mock_sleep.side_effect = lambda delay: 1 / 0 if len( sync_queue) == 0 else sync_queue.pop() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(ZeroDivisionError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 2) snapshot_msg: OrderBookMessage = msg_queue.get_nowait() self.assertEqual(snapshot_msg.update_id, mock_response["timestamp"] * 1e3) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = { "hbotusdt.trades": { "trades": [{ "date": 1234567899, "tid": '3333', "taker_type": "buy", "price": 8772.05, "amount": 0.1, }] } } self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) trade_message = self.async_run_with_timeout(received_messages.get()) self.assertEqual(OrderBookMessageType.TRADE, trade_message.type) self.assertEqual(1234567899, trade_message.timestamp) self.assertEqual('3333', trade_message.trade_id) self.assertEqual(self.trading_pair, trade_message.trading_pair) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades_unrecognised(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=received_messages)) message = {"hbotusdttrades": {}} self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout(received_messages.get()) self.assertTrue( self._is_logged( "INFO", "Unrecognized message received from Altmarkets websocket: {'hbotusdttrades': {}}" )) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades_handles_exception(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=received_messages)) message = {"hbotusdt.trades": {"tradess": []}} self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout(received_messages.get()) self.assertTrue( self._is_logged( "ERROR", "Trades: Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time" ) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diff(self, ws_connect_mock, time_mock): time_mock.return_value = 1234567890 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = { "hbotusdt.ob-inc": { "timestamp": 1234567890, "asks": [[7220.08, 0], [7221.08, 0], [7222.08, 6.92321326], [7219.2, 0.69259752]], "bids": [[7190.27, 0], [7192.27, 0], [7193.27, 6.95094164], [7196.15, 0.69481598]] } } self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( ev_loop=self.ev_loop, output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) diff_message = self.async_run_with_timeout(received_messages.get()) self.assertEqual(OrderBookMessageType.DIFF, diff_message.type) self.assertEqual(4, len(diff_message.content.get("bids"))) self.assertEqual(4, len(diff_message.content.get("asks"))) self.assertEqual(1234567890, diff_message.timestamp) self.assertEqual(int(1234567890 * 1e3), diff_message.update_id) self.assertEqual(-1, diff_message.trade_id) self.assertEqual(self.trading_pair, diff_message.trading_pair) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time" ) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_snapshot(self, ws_connect_mock, time_mock): time_mock.return_value = 1234567890 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = { "hbotusdt.ob-snap": { "timestamp": 1234567890, "asks": [[7220.08, 6.92321326], [7221.08, 6.92321326], [7222.08, 6.92321326], [7219.2, 0.69259752]], "bids": [[7190.27, 6.95094164], [7192.27, 6.95094164], [7193.27, 6.95094164], [7196.15, 0.69481598]] } } self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( ev_loop=self.ev_loop, output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) diff_message = self.async_run_with_timeout(received_messages.get()) self.assertEqual(OrderBookMessageType.SNAPSHOT, diff_message.type) self.assertEqual(4, len(diff_message.content.get("bids"))) self.assertEqual(4, len(diff_message.content.get("asks"))) self.assertEqual(1234567890, diff_message.timestamp) self.assertEqual(int(1234567890 * 1e3), diff_message.update_id) self.assertEqual(-1, diff_message.trade_id) self.assertEqual(self.trading_pair, diff_message.trading_pair) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time" ) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diff_unrecognised(self, ws_connect_mock, time_mock): time_mock.return_value = 1234567890 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = {"snapcracklepop": {}} self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( ev_loop=self.ev_loop, output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout(received_messages.get()) self.assertTrue( self._is_logged( "INFO", "Unrecognized message received from Altmarkets websocket: {'snapcracklepop': {}}" )) @patch( "hummingbot.connector.exchange.altmarkets.altmarkets_api_order_book_data_source.AltmarketsAPIOrderBookDataSource._time" ) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diff_handles_exception( self, ws_connect_mock, time_mock): time_mock.return_value = "NaN" ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = {".ob-snap": {}} self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( ev_loop=self.ev_loop, output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) with self.assertRaises(asyncio.TimeoutError): self.async_run_with_timeout(received_messages.get()) self.assertTrue( self._is_logged("NETWORK", "Unexpected error with WebSocket connection."))
class NdaxAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.instrument_id = 1 def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = NdaxAPIOrderBookDataSource(self.throttler, [self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.data_source._trading_pair_id_map.clear() self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def simulate_trading_pair_ids_initialized(self): self.data_source._trading_pair_id_map.update({self.trading_pair: self.instrument_id}) def _raise_exception(self, exception_class): raise exception_class def _is_logged(self, log_level: str, message: str) -> bool: return any(record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _subscribe_level_2_response(self): resp = { "m": 1, "i": 2, "n": "SubscribeLevel2", "o": "[[93617617, 1, 1626788175000, 0, 37800.0, 1, 37750.0, 1, 0.015, 0],[93617617, 1, 1626788175000, 0, 37800.0, 1, 37751.0, 1, 0.015, 1]]" } return ujson.dumps(resp) def _orderbook_update_event(self): resp = { "m": 3, "i": 3, "n": "Level2UpdateEvent", "o": "[[93617618, 1, 1626788175001, 0, 37800.0, 1, 37740.0, 1, 0.015, 0]]" } return ujson.dumps(resp) @patch("aiohttp.ClientSession.get") def test_init_trading_pair_ids(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) mock_response: List[Any] = [ { "Product1Symbol": self.base_asset, "Product2Symbol": self.quote_asset, "InstrumentId": self.instrument_id, "SessionStatus": "Running" }, { "Product1Symbol": "ANOTHER_ACTIVE", "Product2Symbol": "MARKET", "InstrumentId": 2, "SessionStatus": "Running" }, { "Product1Symbol": "NOT_ACTIVE", "Product2Symbol": "MARKET", "InstrumentId": 3, "SessionStatus": "Stopped" } ] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) self.ev_loop.run_until_complete(self.data_source.init_trading_pair_ids()) self.assertEqual(2, len(self.data_source._trading_pair_id_map)) self.assertEqual(1, self.data_source._trading_pair_id_map[self.trading_pair]) self.assertEqual(2, self.data_source._trading_pair_id_map["ANOTHER_ACTIVE-MARKET"]) @patch("aiohttp.ClientSession.get") def test_get_last_traded_prices(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.simulate_trading_pair_ids_initialized() mock_response: Dict[Any] = { "LastTradedPx": 1.0 } self.mocking_assistant.add_http_response(mock_api, 200, mock_response) results = self.ev_loop.run_until_complete( asyncio.gather(self.data_source.get_last_traded_prices([self.trading_pair]))) results: Dict[str, Any] = results[0] self.assertEqual(results[self.trading_pair], mock_response["LastTradedPx"]) @patch("aiohttp.ClientSession.get") def test_fetch_trading_pairs(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.simulate_trading_pair_ids_initialized() mock_response: List[Any] = [ { "Product1Symbol": self.base_asset, "Product2Symbol": self.quote_asset, "InstrumentId": self.instrument_id, "SessionStatus": "Running" }, { "Product1Symbol": "ANOTHER_ACTIVE", "Product2Symbol": "MARKET", "InstrumentId": 2, "SessionStatus": "Running" }, { "Product1Symbol": "NOT_ACTIVE", "Product2Symbol": "MARKET", "InstrumentId": 3, "SessionStatus": "Stopped" } ] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) self.mocking_assistant.add_http_response(mock_api, 200, mock_response) results: List[str] = self.ev_loop.run_until_complete(self.data_source.fetch_trading_pairs()) self.assertTrue(self.trading_pair in results) self.assertTrue("ANOTHER_ACTIVE-MARKET" in results) self.assertFalse("NOT_ACTIVE-MARKET" in results) @patch("aiohttp.ClientSession.get") def test_fetch_trading_pairs_with_error_status_in_response(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) mock_response = {} self.mocking_assistant.add_http_response(mock_api, 100, mock_response) result = self.ev_loop.run_until_complete(self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @patch("aiohttp.ClientSession.get") def test_get_order_book_data(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.simulate_trading_pair_ids_initialized() mock_response: List[List[Any]] = [ # mdUpdateId, accountId, actionDateTime, actionType, lastTradePrice, orderId, price, productPairCode, quantity, side [93617617, 1, 1626788175416, 0, 37813.22, 1, 37750.6, 1, 0.014698, 0] ] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) results = self.ev_loop.run_until_complete( asyncio.gather(self.data_source.get_order_book_data(self.trading_pair))) result = results[0] self.assertTrue("data" in result) self.assertGreaterEqual(len(result["data"]), 0) self.assertEqual(NdaxOrderBookEntry(*mock_response[0]), result["data"][0]) @patch("aiohttp.ClientSession.get") def test_get_order_book_data_raises_exception_when_response_has_error_code(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.simulate_trading_pair_ids_initialized() mock_response = {"Erroneous response"} self.mocking_assistant.add_http_response(mock_api, 100, mock_response) with self.assertRaises(IOError) as context: self.ev_loop.run_until_complete(self.data_source.get_order_book_data(self.trading_pair)) self.assertEqual(str(context.exception), f"Error fetching OrderBook for {self.trading_pair} " f"at {CONSTANTS.ORDER_BOOK_URL}. " f"HTTP {100}. Response: {mock_response}") @patch("aiohttp.ClientSession.get") def test_get_new_order_book(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.simulate_trading_pair_ids_initialized() mock_response: List[List[Any]] = [ # mdUpdateId, accountId, actionDateTime, actionType, lastTradePrice, orderId, price, productPairCode, quantity, side [93617617, 1, 1626788175416, 0, 37800.0, 1, 37750.0, 1, 0.015, 0], [93617617, 1, 1626788175416, 0, 37800.0, 1, 37751.0, 1, 0.015, 1] ] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) results = self.ev_loop.run_until_complete( asyncio.gather(self.data_source.get_new_order_book(self.trading_pair))) result: OrderBook = results[0] self.assertTrue(type(result) == OrderBook) self.assertEqual(result.snapshot_uid, 0) @patch("aiohttp.ClientSession.get") def test_get_instrument_ids(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) mock_response: List[Any] = [{ "Product1Symbol": self.base_asset, "Product2Symbol": self.quote_asset, "InstrumentId": self.instrument_id, "SessionStatus": "Running", }] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) results = self.ev_loop.run_until_complete(asyncio.gather(self.data_source.get_instrument_ids())) result: Dict[str, Any] = results[0] self.assertEqual(1, self.data_source._trading_pair_id_map[self.trading_pair]) self.assertEqual(result[self.trading_pair], self.instrument_id) @patch("hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep", new_callable=AsyncMock) @patch("aiohttp.ClientSession.get") def test_listen_for_snapshots_cancelled_when_fetching_snapshot(self, mock_api, mock_sleep): mock_api.side_effect = asyncio.CancelledError self.simulate_trading_pair_ids_initialized() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue) ) self.ev_loop.run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 0) @patch("hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep", new_callable=AsyncMock) @patch("aiohttp.ClientSession.get") def test_listen_for_snapshots_logs_exception_when_fetching_snapshot(self, mock_api, mock_sleep): # the queue and the division by zero error are used just to synchronize the test sync_queue = deque() sync_queue.append(1) self.simulate_trading_pair_ids_initialized() mock_api.side_effect = Exception mock_sleep.side_effect = lambda delay: 1 / 0 if len(sync_queue) == 0 else sync_queue.pop() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(ZeroDivisionError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 0) self.assertTrue(self._is_logged("ERROR", "Unexpected error occured listening for orderbook snapshots. Retrying in 5 secs...")) @patch("hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep", new_callable=AsyncMock) @patch("aiohttp.ClientSession.get") def test_listen_for_snapshots_successful(self, mock_api, mock_sleep): self.mocking_assistant.configure_http_request_mock(mock_api) # the queue and the division by zero error are used just to synchronize the test sync_queue = deque() sync_queue.append(1) mock_response: List[List[Any]] = [ # mdUpdateId, accountId, actionDateTime, actionType, lastTradePrice, orderId, price, productPairCode, quantity, side [93617617, 1, 1626788175416, 0, 37800.0, 1, 37750.0, 1, 0.015, 0], [93617617, 1, 1626788175416, 0, 37800.0, 1, 37751.0, 1, 0.015, 1], ] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) self.simulate_trading_pair_ids_initialized() mock_sleep.side_effect = lambda delay: 1 / 0 if len(sync_queue) == 0 else sync_queue.pop() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(ZeroDivisionError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 1) snapshot_msg: OrderBookMessage = msg_queue.get_nowait() self.assertEqual(snapshot_msg.update_id, 0) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_cancelled_when_subscribing(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.WS_ORDER_BOOK_CHANNEL in sent_message else self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, sent_message) ) self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._subscribe_level_2_response()) self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._orderbook_update_event()) self.simulate_trading_pair_ids_initialized() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue) ) self.ev_loop.run_until_complete(self.listening_task) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_cancelled_when_listening(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.recv.side_effect = lambda: ( self._raise_exception(asyncio.CancelledError) ) self.simulate_trading_pair_ids_initialized() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue) ) self.ev_loop.run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 0) @patch("hummingbot.client.hummingbot_application.HummingbotApplication") @patch("hummingbot.connector.exchange.ndax.ndax_api_order_book_data_source.NdaxAPIOrderBookDataSource._sleep", new_callable=AsyncMock) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_logs_exception(self, mock_ws, *_): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None incomplete_resp = { "m": 1, "i": 2, } self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, ujson.dumps(incomplete_resp)) self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._orderbook_update_event()) self.simulate_trading_pair_ids_initialized() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(msg_queue.get()) self.assertTrue(self._is_logged("NETWORK", "Unexpected error with WebSocket connection.")) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._subscribe_level_2_response()) self.mocking_assistant.add_websocket_text_message(mock_ws.return_value, self._orderbook_update_event()) self.simulate_trading_pair_ids_initialized() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue)) first_msg = self.ev_loop.run_until_complete(msg_queue.get()) second_msg = self.ev_loop.run_until_complete(msg_queue.get()) self.assertTrue(first_msg.type == OrderBookMessageType.SNAPSHOT) self.assertTrue(second_msg.type == OrderBookMessageType.DIFF) @patch("websockets.connect", new_callable=AsyncMock) def test_websocket_connection_creation_raises_cancel_exception(self, mock_ws): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): asyncio.get_event_loop().run_until_complete(self.data_source._create_websocket_connection()) @patch("websockets.connect", new_callable=AsyncMock) def test_websocket_connection_creation_raises_exception_after_loging(self, mock_ws): mock_ws.side_effect = Exception with self.assertRaises(Exception): asyncio.get_event_loop().run_until_complete(self.data_source._create_websocket_connection()) self.assertTrue(self._is_logged("NETWORK", "Unexpected error occurred during ndax WebSocket Connection ()"))
class TestGateIoAPIOrderBookDataSource(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() gate_io_auth = GateIoAuth(api_key="someKey", secret_key="someSecret") self.data_source = GateIoAPIUserStreamDataSource(gate_io_auth, trading_pairs=[self.trading_pair]) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def get_user_trades_mock(self) -> Dict: user_trades = { "time": 1605176741, "channel": "spot.usertrades", "event": "update", "result": [ { "id": 5736713, "user_id": 1000001, "order_id": "30784428", "currency_pair": f"{self.base_asset}_{self.quote_asset}", "create_time": 1605176741, "create_time_ms": "1605176741123.456", "side": "sell", "amount": "1.00000000", "role": "taker", "price": "10000.00000000", "fee": "0.00200000000000", "point_fee": "0", "gt_fee": "0", "text": "apiv4" } ] } return user_trades def get_user_orders_mock(self) -> Dict: user_orders = { "time": 1605175506, "channel": "spot.orders", "event": "update", "result": [ { "id": "30784435", "user": 123456, "text": "t-abc", "create_time": "1605175506", "create_time_ms": "1605175506123", "update_time": "1605175506", "update_time_ms": "1605175506123", "event": "put", "currency_pair": f"{self.base_asset}_{self.quote_asset}", "type": "limit", "account": "spot", "side": "sell", "amount": "1", "price": "10001", "time_in_force": "gtc", "left": "1", "filled_total": "0", "fee": "0", "fee_currency": "USDT", "point_fee": "0", "gt_fee": "0", "gt_discount": True, "rebated_fee": "0", "rebated_fee_currency": "USDT" } ] } return user_orders def get_user_balance_mock(self) -> Dict: user_balance = { "time": 1605248616, "channel": "spot.balances", "event": "update", "result": [ { "timestamp": "1605248616", "timestamp_ms": "1605248616123", "user": "******", "currency": self.base_asset, "change": "100", "total": "1032951.325075926", "available": "1022943.325075926" } ] } return user_balance @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_user_stream(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() output_queue = asyncio.Queue() self.ev_loop.create_task(self.data_source.listen_for_user_stream(self.ev_loop, output_queue)) resp = self.get_user_trades_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) ) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp) resp = self.get_user_orders_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) ) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp) resp = self.get_user_balance_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp) ) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp)
class KrakenAPIUserStreamDataSourceTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_tier = KrakenAPITier.STARTER def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler( build_rate_limits_by_tier(self.api_tier)) not_a_real_secret = "kQH5HW/8p1uGOVjbgWA7FunAmGO8lsSUXNsu3eow76sz84Q18fWxnyRzBHCd3pd5nE9qa99HAZtuZuj6F1huXg==" kraken_auth = KrakenAuth(api_key="someKey", secret_key=not_a_real_secret) self.data_source = KrakenAPIUserStreamDataSource( self.throttler, kraken_auth) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @staticmethod def get_auth_response_mock() -> Dict: auth_resp = { "error": [], "result": { "token": "1Dwc4lzSwNWOAwkMdqhssNNFhs1ed606d1WcF3XfEMw", "expires": 900 } } return auth_resp @staticmethod def get_open_orders_mock() -> List: open_orders = [[{ "OGTT3Y-C6I3P-XRI6HX": { "status": "closed" } }, { "OGTT3Y-C6I3P-XRI6HX": { "status": "closed" } }], "openOrders", { "sequence": 59342 }] return open_orders @staticmethod def get_own_trades_mock() -> List: own_trades = [[ { "TDLH43-DVQXD-2KHVYY": { "cost": "1000000.00000", "fee": "1600.00000", "margin": "0.00000", "ordertxid": "TDLH43-DVQXD-2KHVYY", "ordertype": "limit", "pair": "XBT/EUR", "postxid": "OGTT3Y-C6I3P-XRI6HX", "price": "100000.00000", "time": "1560516023.070651", "type": "sell", "vol": "1000000000.00000000" } }, ], "ownTrades", { "sequence": 2948 }] return own_trades @aioresponses() def test_get_auth_token(self, mocked_api): url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_auth_response_mock() mocked_api.post(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout(self.data_source.get_auth_token()) self.assertEqual(ret, resp["result"]["token"]) @aioresponses() @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_user_stream(self, mocked_api, ws_connect_mock): url = f"{CONSTANTS.BASE_URL}{CONSTANTS.GET_TOKEN_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_auth_response_mock() mocked_api.post(regex_url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) output_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, output_queue)) resp = self.get_open_orders_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp) resp = self.get_own_trades_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp)
class TestGateIoAPIOrderBookDataSource(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.data_source = GateIoAPIOrderBookDataSource( self.throttler, trading_pairs=[self.trading_pair]) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def get_last_trade_instance_data_mock(self) -> List: last_trade_instance_data = [{ "currency_pair": f"{self.base_asset}_{self.quote_asset}", "last": "0.2959", "lowest_ask": "0.295918", "highest_bid": "0.295898", "change_percentage": "-1.72", "base_volume": "78497066.828007", "quote_volume": "23432064.936692", "high_24h": "0.309372", "low_24h": "0.286827", }] return last_trade_instance_data @staticmethod def get_order_book_data_mock() -> Dict: order_book_data = { "id": 1890172054, "current": 1630644717528, "update": 1630644716786, "asks": [["0.298705", "5020"]], "bids": [["0.298642", "2703.17"]] } return order_book_data def get_trade_data_mock(self) -> Dict: trade_data = { "time": 1606292218, "channel": "spot.trades", "event": "update", "result": { "id": 309143071, "create_time": 1606292218, "create_time_ms": "1606292218213.4578", "side": "sell", "currency_pair": f"{self.base_asset}_{self.quote_asset}", "amount": "16.4700000000", "price": "0.4705000000" } } return trade_data def get_order_book_update_mock(self) -> Dict: ob_update = { "time": 1606294781, "channel": "spot.order_book_update", "event": "update", "result": { "t": 1606294781123, "e": "depthUpdate", "E": 1606294781, "s": f"{self.base_asset}_{self.quote_asset}", "U": 48776301, "u": 48776306, "b": [ ["19137.74", "0.0001"], ], "a": [["19137.75", "0.6135"]] } } return ob_update def get_order_book_snapshot_mock(self) -> Dict: ob_snapshot = { "time": 1606295412, "channel": "spot.order_book", "event": "update", "result": { "t": 1606295412123, "lastUpdateId": 48791820, "s": f"{self.base_asset}_{self.quote_asset}", "bids": [ ["19079.55", "0.0195"], ], "asks": [ ["19080.24", "0.1638"], ] } } return ob_snapshot @aioresponses() def test_get_last_trade_instance(self, mock_api): url = f"{CONSTANTS.REST_URL}/{CONSTANTS.TICKER_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_last_trade_instance_data_mock() mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=GateIoAPIOrderBookDataSource.get_last_traded_prices( trading_pairs=[self.trading_pair])) self.assertEqual(ret[self.trading_pair], Decimal(resp[0]["last"])) @aioresponses() def test_fetch_trading_pairs(self, mock_api): url = f"{CONSTANTS.REST_URL}/{CONSTANTS.SYMBOL_PATH_URL}" resp = [{ "id": f"{self.base_asset}_{self.quote_asset}" }, { "id": "SOME_PAIR" }] mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=GateIoAPIOrderBookDataSource.fetch_trading_pairs()) self.assertTrue(self.trading_pair in ret) self.assertTrue("SOME-PAIR" in ret) @patch( "hummingbot.connector.exchange.gate_io.gate_io_utils.retry_sleep_time") @aioresponses() def test_get_order_book_data_raises(self, retry_sleep_time_mock, mock_api): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = "" for _ in range(CONSTANTS.API_MAX_RETRIES): mock_api.get(regex_url, body=json.dumps(resp), status=500) with self.assertRaises(IOError): self.async_run_with_timeout(coroutine=GateIoAPIOrderBookDataSource. get_order_book_data(self.trading_pair)) @aioresponses() def test_get_order_book_data(self, mock_api): url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_order_book_data_mock() mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=GateIoAPIOrderBookDataSource.get_order_book_data( self.trading_pair)) self.assertEqual(resp, ret) # shallow comparison is ok @aioresponses() def test_get_new_order_book(self, mock_api): url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_order_book_data_mock() mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.data_source.get_new_order_book(self.trading_pair)) self.assertTrue(isinstance(ret, OrderBook)) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_trade_data_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) output_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_text_messages_delivered( websocket_mock=ws_connect_mock.return_value) self.assertTrue(not output_queue.empty()) self.assertTrue(isinstance(output_queue.get_nowait(), OrderBookMessage)) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_update(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_order_book_update_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) output_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_text_messages_delivered( websocket_mock=ws_connect_mock.return_value) self.assertTrue(not output_queue.empty()) self.assertTrue(isinstance(output_queue.get_nowait(), OrderBookMessage)) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_snapshot(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_order_book_snapshot_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) output_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_text_messages_delivered( websocket_mock=ws_connect_mock.return_value) self.assertTrue(not output_queue.empty()) self.assertTrue(isinstance(output_queue.get_nowait(), OrderBookMessage)) @aioresponses() def test_listen_for_order_book_snapshots(self, mock_api): url = f"{CONSTANTS.REST_URL}/{CONSTANTS.ORDER_BOOK_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_order_book_data_mock() mock_api.get(regex_url, body=json.dumps(resp)) output_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, output_queue)) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertTrue(isinstance(ret, OrderBookMessage))
class MexcExchangeTests(TestCase): # the level is required to receive logs from the data source loger level = 0 start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp() @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.base_asset = "MX" cls.quote_asset = "USDT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ev_loop = asyncio.get_event_loop() def setUp(self) -> None: super().setUp() self.tracker_task = None self.exchange_task = None self.log_records = [] self.resume_test_event = asyncio.Event() self._account_name = "hbot" self.exchange = MexcExchange(mexc_api_key='testAPIKey', mexc_secret_key='testSecret', trading_pairs=[self.trading_pair]) self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self.exchange._account_id = 1 self.mocking_assistant = NetworkMockingAssistant() self.mock_done_event = asyncio.Event() def tearDown(self) -> None: self.tracker_task and self.tracker_task.cancel() self.exchange_task and self.exchange_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _return_calculation_and_set_done_event(self, calculation: Callable, *args, **kwargs): if self.resume_test_event.is_set(): raise asyncio.CancelledError self.resume_test_event.set() return calculation(*args, **kwargs) def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _mock_responses_done_callback(self, *_, **__): self.mock_done_event.set() def _simulate_reset_poll_notifier(self): self.exchange._poll_notifier.clear() def _simulate_ws_message_received(self, timestamp: float): self.exchange._user_stream_tracker._data_source._last_recv_time = timestamp def _simulate_trading_rules_initialized(self): self.exchange._trading_rules = { self.trading_pair: TradingRule(trading_pair=self.trading_pair, min_order_size=4, min_price_increment=Decimal(str(0.0001)), min_base_amount_increment=2, min_notional_size=Decimal(str(5))) } @property def order_book_data(self): _data = { "code": 200, "data": { "asks": [{ "price": "56454.0", "quantity": "0.799072" }, { "price": "56455.28", "quantity": "0.008663" }], "bids": [{ "price": "56451.0", "quantity": "0.008663" }, { "price": "56449.99", "quantity": "0.173078" }], "version": "547878563" } } return _data def _simulate_create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, price: Decimal = Decimal("0"), order_type: OrderType = OrderType.MARKET): future = safe_ensure_future( self.exchange.execute_buy(order_id, trading_pair, amount, order_type, price)) self.exchange.start_tracking_order(order_id, None, self.trading_pair, TradeType.BUY, Decimal(10.0), Decimal(1.0), OrderType.LIMIT) return future @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_user_event_queue_error_is_logged(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) dummy_user_stream = AsyncMock() dummy_user_stream.get.side_effect = lambda: self._create_exception_and_unlock_test_with_event( Exception("Dummy test error")) self.exchange._user_stream_tracker._user_stream = dummy_user_stream # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_text_message( ws_connect_mock, ujson.dumps({'channel': 'push.personal.order'})) self.async_run_with_timeout(self.resume_test_event.wait()) self.resume_test_event.clear() try: self.exchange_task.cancel() self.async_run_with_timeout(self.exchange_task) except asyncio.CancelledError: pass except Exception: pass self.assertTrue( self._is_logged( 'ERROR', "Unknown error. Retrying after 1 second. Dummy test error")) def test_user_event_queue_notifies_cancellations(self): self.tracker_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) dummy_user_stream = AsyncMock() dummy_user_stream.get.side_effect = lambda: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) self.exchange._user_stream_tracker._user_stream = dummy_user_stream with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout(self.tracker_task) def test_exchange_logs_unknown_event_message(self): payload = {'channel': 'test'} mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = functools.partial( self._return_calculation_and_set_done_event, lambda: payload) self.exchange._user_stream_tracker._user_stream = mock_user_stream self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( 'DEBUG', f"Unknown event received from the connector ({payload})")) @property def balances_mock_data(self): return { "code": 200, "data": { "MX": { "frozen": "30.9863", "available": "450.0137" } } } @property def user_stream_data(self): return { 'symbol': 'MX_USDT', 'data': { 'price': 3.1504, 'quantity': 2, 'amount': 6.3008, 'remainAmount': 6.3008, 'remainQuantity': 2, 'remainQ': 2, 'id': '40728558ead64032a676e6f0a4afc4ca', 'status': 4, 'tradeType': 2, 'createTime': 1638156451000, 'symbolDisplay': 'MX_USDT', 'clientOrderId': 'sell-MX-USDT-1638156451005305' }, 'channel': 'push.personal.order', 'symbol_display': 'MX_USDT' } @aioresponses() def test_order_event_with_cancel_status_cancels_in_flight_order( self, mock_api): mock_response = self.balances_mock_data url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get( regex_url, body=json.dumps(mock_response), ) self.exchange.start_tracking_order( order_id="sell-MX-USDT-1638156451005305", exchange_order_id="40728558ead64032a676e6f0a4afc4ca", trading_pair="MX-USDT", trade_type=TradeType.SELL, price=Decimal("3.1504"), amount=Decimal("6.3008"), order_type=OrderType.LIMIT) inflight_order = self.exchange.in_flight_orders[ "sell-MX-USDT-1638156451005305"] mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = [ self.user_stream_data, asyncio.CancelledError ] self.exchange._user_stream_tracker._user_stream = mock_user_stream try: self.async_run_with_timeout( self.exchange._user_stream_event_listener(), 1000000) except asyncio.CancelledError: pass self.assertEqual("CANCELED", inflight_order.last_state) self.assertTrue(inflight_order.is_cancelled) self.assertFalse( inflight_order.client_order_id in self.exchange.in_flight_orders) self.assertTrue( self._is_logged( "INFO", f"Order {inflight_order.client_order_id} " f"has been canceled according to order delta websocket API.")) self.assertEqual(1, len(self.exchange.event_logs)) cancel_event = self.exchange.event_logs[0] self.assertEqual(OrderCancelledEvent, type(cancel_event)) self.assertEqual(inflight_order.client_order_id, cancel_event.order_id) @aioresponses() def test_order_event_with_rejected_status_makes_in_flight_order_fail( self, mock_api): mock_response = self.balances_mock_data url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get( regex_url, body=json.dumps(mock_response), ) self.exchange.start_tracking_order( order_id="sell-MX-USDT-1638156451005305", exchange_order_id="40728558ead64032a676e6f0a4afc4ca", trading_pair="MX-USDT", trade_type=TradeType.SELL, price=Decimal("3.1504"), amount=Decimal("6.3008"), order_type=OrderType.LIMIT) inflight_order = self.exchange.in_flight_orders[ "sell-MX-USDT-1638156451005305"] stream_data = self.user_stream_data stream_data.get("data")["status"] = 5 mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = [ stream_data, asyncio.CancelledError ] self.exchange._user_stream_tracker._user_stream = mock_user_stream try: self.async_run_with_timeout( self.exchange._user_stream_event_listener(), 1000000) except asyncio.CancelledError: pass self.assertEqual("PARTIALLY_CANCELED", inflight_order.last_state) self.assertTrue(inflight_order.is_failure) self.assertFalse( inflight_order.client_order_id in self.exchange.in_flight_orders) self.assertTrue( self._is_logged( "INFO", f"Order {inflight_order.client_order_id} " f"has been canceled according to order delta websocket API.")) self.assertEqual(1, len(self.exchange.event_logs)) failure_event = self.exchange.event_logs[0] self.assertEqual(OrderCancelledEvent, type(failure_event)) self.assertEqual(inflight_order.client_order_id, failure_event.order_id) @aioresponses() def test_trade_event_fills_and_completes_buy_in_flight_order( self, mock_api): fee_mock_data = { 'code': 200, 'data': [{ 'id': 'c85b7062f69c4bf1b6c153dca5c0318a', 'symbol': 'MX_USDT', 'quantity': '2', 'price': '3.1265', 'amount': '6.253', 'fee': '0.012506', 'trade_type': 'BID', 'order_id': '95c4ce45fdd34cf99bfd1e1378eb38ae', 'is_taker': False, 'fee_currency': 'USDT', 'create_time': 1638177115000 }] } mock_response = self.balances_mock_data url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get( regex_url, body=json.dumps(mock_response), ) url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_DEAL_DETAIL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get( regex_url, body=json.dumps(fee_mock_data), ) self.exchange.start_tracking_order( order_id="sell-MX-USDT-1638156451005305", exchange_order_id="40728558ead64032a676e6f0a4afc4ca", trading_pair="MX-USDT", trade_type=TradeType.SELL, price=Decimal("3.1504"), amount=Decimal("6.3008"), order_type=OrderType.LIMIT) inflight_order = self.exchange.in_flight_orders[ "sell-MX-USDT-1638156451005305"] _user_stream = self.user_stream_data _user_stream.get("data")["status"] = 2 mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = [ _user_stream, asyncio.CancelledError ] self.exchange._user_stream_tracker._user_stream = mock_user_stream try: self.async_run_with_timeout( self.exchange._user_stream_event_listener(), 1000000) except asyncio.CancelledError: pass self.assertEqual("FILLED", inflight_order.last_state) self.assertEqual(Decimal(0), inflight_order.executed_amount_base) self.assertEqual(Decimal(0), inflight_order.executed_amount_quote) self.assertEqual(1, len(self.exchange.event_logs)) fill_event = self.exchange.event_logs[0] self.assertEqual(SellOrderCompletedEvent, type(fill_event)) self.assertEqual(inflight_order.client_order_id, fill_event.order_id) self.assertEqual(inflight_order.trading_pair, f'{fill_event.base_asset}-{fill_event.quote_asset}') def test_tick_initial_tick_successful(self): start_ts: float = time.time() * 1e3 self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) @patch("time.time") def test_tick_subsequent_tick_within_short_poll_interval(self, mock_ts): # Assumes user stream tracker has NOT been receiving messages, Hence SHORT_POLL_INTERVAL in use start_ts: float = self.start_timestamp next_tick: float = start_ts + (self.exchange.SHORT_POLL_INTERVAL - 1) mock_ts.return_value = start_ts self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) self._simulate_reset_poll_notifier() mock_ts.return_value = next_tick self.exchange.tick(next_tick) self.assertEqual(next_tick, self.exchange._last_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) @patch("time.time") def test_tick_subsequent_tick_exceed_short_poll_interval(self, mock_ts): # Assumes user stream tracker has NOT been receiving messages, Hence SHORT_POLL_INTERVAL in use start_ts: float = self.start_timestamp next_tick: float = start_ts + (self.exchange.SHORT_POLL_INTERVAL + 1) mock_ts.return_value = start_ts self.exchange.tick(start_ts) self.assertEqual(start_ts, self.exchange._last_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) self._simulate_reset_poll_notifier() mock_ts.return_value = next_tick self.exchange.tick(next_tick) self.assertEqual(next_tick, self.exchange._last_timestamp) self.assertTrue(self.exchange._poll_notifier.is_set()) @aioresponses() def test_update_balances(self, mock_api): self.assertEqual(0, len(self.exchange._account_balances)) self.assertEqual(0, len(self.exchange._account_available_balances)) mock_response = self.balances_mock_data url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get( regex_url, body=json.dumps(mock_response), ) self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._update_balances()) self.async_run_with_timeout(self.exchange_task) self.assertEqual(Decimal(str(481.0)), self.exchange.get_balance(self.base_asset)) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp", new_callable=PropertyMock) def test_update_order_status(self, mock_api, mock_ts): # Simulates order being tracked order: MexcInFlightOrder = MexcInFlightOrder( "0", "2628", self.trading_pair, OrderType.LIMIT, TradeType.SELL, Decimal(str(41720.83)), Decimal("1"), 1640001112.0, "Working", ) self.exchange._in_flight_orders.update({order.client_order_id: order}) self.exchange._last_poll_timestamp = 10 ts: float = time.time() mock_ts.return_value = ts self.exchange._current_timestamp = ts self.assertTrue(1, len(self.exchange.in_flight_orders)) # Add TradeHistory API Response url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_DETAILS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "code": 200, "data": [{ "id": "504feca6ba6349e39c82262caf0be3f4", "symbol": "MX_USDT", "price": "3.001", "quantity": "30", "state": "CANCELED", "type": "BID", "deal_quantity": "0", "deal_amount": "0", "create_time": 1573117266000 }] } mock_api.get(regex_url, body=json.dumps(mock_response)) self.async_run_with_timeout(self.exchange._update_order_status()) self.assertEqual(0, len(self.exchange.in_flight_orders)) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp", new_callable=PropertyMock) def test_update_order_status_error_response(self, mock_api, mock_ts): # Simulates order being tracked order: MexcInFlightOrder = MexcInFlightOrder( "0", "2628", self.trading_pair, OrderType.LIMIT, TradeType.SELL, Decimal(str(41720.83)), Decimal("1"), creation_timestamp=1640001112.0) self.exchange._in_flight_orders.update({order.client_order_id: order}) self.assertTrue(1, len(self.exchange.in_flight_orders)) ts: float = time.time() mock_ts.return_value = ts self.exchange._current_timestamp = ts # Add TradeHistory API Response url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_DETAILS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "result": False, "errormsg": "Invalid Request", "errorcode": 100, "detail": None } mock_api.get(regex_url, body=json.dumps(mock_response)) self.async_run_with_timeout(self.exchange._update_order_status()) self.assertEqual(1, len(self.exchange.in_flight_orders)) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_balances", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_order_status", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp", new_callable=PropertyMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._reset_poll_notifier" ) def test_status_polling_loop(self, _, mock_ts, mock_update_order_status, mock_balances): mock_balances.return_value = None mock_update_order_status.return_value = None ts: float = time.time() mock_ts.return_value = ts self.exchange._current_timestamp = ts with self.assertRaises(asyncio.TimeoutError): self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._status_polling_loop()) self.exchange._poll_notifier.set() self.async_run_with_timeout( asyncio.wait_for(self.exchange_task, 2.0)) self.assertEqual(ts, self.exchange._last_poll_timestamp) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp", new_callable=PropertyMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._reset_poll_notifier" ) @aioresponses() def test_status_polling_loop_cancels(self, _, mock_ts, mock_api): url = CONSTANTS.MEXC_BASE_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) ts: float = time.time() mock_ts.return_value = ts self.exchange._current_timestamp = ts with self.assertRaises(asyncio.CancelledError): self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._status_polling_loop()) self.exchange._poll_notifier.set() self.async_run_with_timeout(self.exchange_task) self.assertEqual(0, self.exchange._last_poll_timestamp) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_balances", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_order_status", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp", new_callable=PropertyMock) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._reset_poll_notifier" ) def test_status_polling_loop_exception_raised(self, _, mock_ts, mock_update_order_status, mock_balances): mock_balances.side_effect = lambda: self._create_exception_and_unlock_test_with_event( Exception("Dummy test error")) mock_update_order_status.side_effect = lambda: self._create_exception_and_unlock_test_with_event( Exception("Dummy test error")) ts: float = time.time() mock_ts.return_value = ts self.exchange._current_timestamp = ts self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._status_polling_loop()) self.exchange._poll_notifier.set() self.async_run_with_timeout(self.resume_test_event.wait()) self.assertEqual(0, self.exchange._last_poll_timestamp) self._is_logged( "ERROR", "Unexpected error while in status polling loop. Error: ") def test_format_trading_rules_success(self): instrument_info: List[Dict[str, Any]] = [{ "symbol": f"{self.base_asset}_{self.quote_asset}", "price_scale": 3, "quantity_scale": 3, "min_amount": "1", }] result: List[str, TradingRule] = self.exchange._format_trading_rules( instrument_info) self.assertTrue(self.trading_pair == result[0].trading_pair) def test_format_trading_rules_failure(self): # Simulate invalid API response instrument_info: List[Dict[str, Any]] = [{}] result: Dict[str, TradingRule] = self.exchange._format_trading_rules( instrument_info) self.assertTrue(self.trading_pair not in result) self.assertTrue( self._is_logged( "ERROR", 'Error parsing the trading pair rule {}. Skipping.')) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.current_timestamp", new_callable=PropertyMock) def test_update_trading_rules(self, mock_api, mock_ts): url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_SYMBOL_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "code": 200, "data": [{ "symbol": "MX_USDT", "state": "ENABLED", "price_scale": 4, "quantity_scale": 2, "min_amount": "5", "max_amount": "5000000", "maker_fee_rate": "0.002", "taker_fee_rate": "0.002", "limited": False, "etf_mark": 0, "symbol_partition": "MAIN" }] } mock_api.get(regex_url, body=json.dumps(mock_response)) self.exchange._last_poll_timestamp = 10 ts: float = time.time() mock_ts.return_value = ts self.exchange._current_timestamp = ts task = asyncio.get_event_loop().create_task( self.exchange._update_trading_rules()) self.async_run_with_timeout(task) self.assertTrue(self.trading_pair in self.exchange.trading_rules) self.exchange.trading_rules[self.trading_pair] @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_trading_rules", new_callable=AsyncMock) def test_trading_rules_polling_loop(self, mock_update): # No Side Effects expected mock_update.return_value = None with self.assertRaises(asyncio.TimeoutError): self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._trading_rules_polling_loop()) self.async_run_with_timeout( asyncio.wait_for(self.exchange_task, 1.0)) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_trading_rules", new_callable=AsyncMock) def test_trading_rules_polling_loop_cancels(self, mock_update): mock_update.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._trading_rules_polling_loop()) self.async_run_with_timeout(self.exchange_task) self.assertEqual(0, self.exchange._last_poll_timestamp) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange._update_trading_rules", new_callable=AsyncMock) def test_trading_rules_polling_loop_exception_raised(self, mock_update): mock_update.side_effect = lambda: self._create_exception_and_unlock_test_with_event( Exception("Dummy test error")) self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._trading_rules_polling_loop()) self.async_run_with_timeout(self.resume_test_event.wait()) self._is_logged( "ERROR", "Unexpected error while fetching trading rules. Error: ") @aioresponses() def test_check_network_succeeds_when_ping_replies_pong(self, mock_api): url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"code": 200} mock_api.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout(self.exchange.check_network()) self.assertEqual(NetworkStatus.CONNECTED, result) @aioresponses() def test_check_network_fails_when_ping_does_not_reply_pong(self, mock_api): url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"code": 100} mock_api.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout(self.exchange.check_network()) self.assertEqual(NetworkStatus.NOT_CONNECTED, result) url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {} mock_api.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout(self.exchange.check_network()) self.assertEqual(NetworkStatus.NOT_CONNECTED, result) @aioresponses() def test_check_network_fails_when_ping_returns_error_code(self, mock_api): url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PING_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"code": 100} mock_api.get(regex_url, body=json.dumps(mock_response), status=404) result = self.async_run_with_timeout(self.exchange.check_network()) self.assertEqual(NetworkStatus.NOT_CONNECTED, result) def test_get_order_book_for_valid_trading_pair(self): dummy_order_book = MexcOrderBook() self.exchange._order_book_tracker.order_books[ "BTC-USDT"] = dummy_order_book self.assertEqual(dummy_order_book, self.exchange.get_order_book("BTC-USDT")) def test_get_order_book_for_invalid_trading_pair_raises_error(self): self.assertRaisesRegex(ValueError, "No order book exists for 'BTC-USDT'", self.exchange.get_order_book, "BTC-USDT") @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.execute_buy", new_callable=AsyncMock) def test_buy(self, mock_create): mock_create.side_effect = None order_details = [ self.trading_pair, Decimal(1.0), Decimal(10.0), OrderType.LIMIT, ] # Note: BUY simply returns immediately with the client order id. order_id: str = self.exchange.buy(*order_details) # Order ID is simply a timestamp. The assertion below checks if it is created within 1 sec self.assertTrue(len(order_id) > 0) def test_sell(self): order_details = [ self.trading_pair, Decimal(1.0), Decimal(10.0), OrderType.LIMIT, ] # Note: SELL simply returns immediately with the client order id. order_id: str = self.exchange.buy(*order_details) # Order ID is simply a timestamp. The assertion below checks if it is created within 1 sec self.assertTrue(len(order_id) > 0) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.quantize_order_amount" ) def test_create_limit_order(self, mock_post, amount_mock): amount_mock.return_value = Decimal("1") url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PLACE_ORDER regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) expected_response = {"code": 200, "data": "123"} mock_post.post(regex_url, body=json.dumps(expected_response)) self._simulate_trading_rules_initialized() order_details = [ TradeType.BUY, str(1), self.trading_pair, Decimal(1.0), Decimal(10.0), OrderType.LIMIT, ] self.assertEqual(0, len(self.exchange.in_flight_orders)) future = self._simulate_create_order(*order_details) self.async_run_with_timeout(future) self.assertEqual(1, len(self.exchange.in_flight_orders)) self._is_logged( "INFO", f"Created {OrderType.LIMIT.name} {TradeType.BUY.name} order {123} for {Decimal(1.0)} {self.trading_pair}" ) tracked_order: MexcInFlightOrder = self.exchange.in_flight_orders["1"] self.assertEqual(tracked_order.client_order_id, "1") self.assertEqual(tracked_order.exchange_order_id, "123") self.assertEqual(tracked_order.last_state, "NEW") self.assertEqual(tracked_order.trading_pair, self.trading_pair) self.assertEqual(tracked_order.price, Decimal(10.0)) self.assertEqual(tracked_order.amount, Decimal(1.0)) self.assertEqual(tracked_order.trade_type, TradeType.BUY) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.quantize_order_amount" ) def test_create_market_order(self, mock_post, amount_mock): amount_mock.return_value = Decimal("1") url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_PLACE_ORDER regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) expected_response = {"code": 200, "data": "123"} mock_post.post(regex_url, body=json.dumps(expected_response)) self._simulate_trading_rules_initialized() order_details = [ TradeType.BUY, str(1), self.trading_pair, Decimal(1.0), Decimal(10.0), OrderType.LIMIT_MAKER, ] self.assertEqual(0, len(self.exchange.in_flight_orders)) future = self._simulate_create_order(*order_details) self.async_run_with_timeout(future) self.assertEqual(1, len(self.exchange.in_flight_orders)) self._is_logged( "INFO", f"Created {OrderType.LIMIT.name} {TradeType.BUY.name} order {123} for {Decimal(1.0)} {self.trading_pair}" ) tracked_order: MexcInFlightOrder = self.exchange.in_flight_orders["1"] self.assertEqual(tracked_order.client_order_id, "1") self.assertEqual(tracked_order.exchange_order_id, "123") self.assertEqual(tracked_order.last_state, "NEW") self.assertEqual(tracked_order.trading_pair, self.trading_pair) self.assertEqual(tracked_order.amount, Decimal(1.0)) self.assertEqual(tracked_order.trade_type, TradeType.BUY) @aioresponses() def test_detect_created_order_server_acknowledgement(self, mock_api): url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_BALANCE_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(self.balances_mock_data)) self.exchange.start_tracking_order( order_id="sell-MX-USDT-1638156451005305", exchange_order_id="40728558ead64032a676e6f0a4afc4ca", trading_pair="MX-USDT", trade_type=TradeType.SELL, price=Decimal("3.1504"), amount=Decimal("6.3008"), order_type=OrderType.LIMIT) _user_data = self.user_stream_data _user_data.get("data")["status"] = 2 mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = functools.partial( self._return_calculation_and_set_done_event, lambda: _user_data) self.exchange._user_stream_tracker._user_stream = mock_user_stream self.exchange_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertEqual(1, len(self.exchange.in_flight_orders)) tracked_order: MexcInFlightOrder = self.exchange.in_flight_orders[ "sell-MX-USDT-1638156451005305"] self.assertEqual(tracked_order.last_state, "NEW") @aioresponses() def test_execute_cancel_success(self, mock_cancel): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0, initial_state="Working", ) self.exchange._in_flight_orders.update({order.client_order_id: order}) mock_response = {"code": 200, "data": {"123": "success"}} url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_cancel.delete(regex_url, body=json.dumps(mock_response)) self.mocking_assistant.configure_http_request_mock(mock_cancel) self.mocking_assistant.add_http_response(mock_cancel, 200, mock_response, "") result = self.async_run_with_timeout( self.exchange.execute_cancel(self.trading_pair, order.client_order_id)) self.assertIsNone(result) @aioresponses() def test_execute_cancel_all_success(self, mock_post_request): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0) self.exchange._in_flight_orders.update({order.client_order_id: order}) mock_response = {"code": 200, "data": {"0": "success"}} url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_post_request.delete(regex_url, body=json.dumps(mock_response)) cancellation_results = self.async_run_with_timeout( self.exchange.cancel_all(10)) self.assertEqual(1, len(cancellation_results)) self.assertEqual("0", cancellation_results[0].order_id) self.assertTrue(cancellation_results[0].success) @aioresponses() @patch("hummingbot.client.hummingbot_application.HummingbotApplication") def test_execute_cancel_fail(self, mock_cancel, mock_main_app): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0, initial_state="Working", ) self.exchange._in_flight_orders.update({order.client_order_id: order}) mock_response = {"code": 100, "data": {"123": "success"}} url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_cancel.delete(regex_url, body=json.dumps(mock_response)) self.async_run_with_timeout( self.exchange.execute_cancel(self.trading_pair, order.client_order_id)) self._is_logged( "NETWORK", "Failed to cancel order 0 : MexcAPIError('Order could not be canceled')" ) @aioresponses() def test_execute_cancel_cancels(self, mock_cancel): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0, initial_state="Working", ) self.exchange._in_flight_orders.update({order.client_order_id: order}) url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_ORDER_CANCEL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_cancel.delete(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.exchange.execute_cancel(self.trading_pair, order.client_order_id)) @patch( "hummingbot.connector.exchange.mexc.mexc_exchange.MexcExchange.execute_cancel", new_callable=AsyncMock) def test_cancel(self, mock_cancel): mock_cancel.return_value = None order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0) self.exchange._in_flight_orders.update({order.client_order_id: order}) # Note: BUY simply returns immediately with the client order id. return_val: str = self.exchange.cancel(self.trading_pair, order.client_order_id) # Order ID is simply a timestamp. The assertion below checks if it is created within 1 sec self.assertTrue(order.client_order_id, return_val) def test_ready_trading_required_all_ready(self): self.exchange._trading_required = True # Simulate all components initialized self.exchange._account_id = 1 self.exchange._order_book_tracker._order_books_initialized.set() self.exchange._account_balances = {self.base_asset: Decimal(str(10.0))} self._simulate_trading_rules_initialized() self.exchange._user_stream_tracker.data_source._last_recv_time = 1 self.assertTrue(self.exchange.ready) def test_ready_trading_required_not_ready(self): self.exchange._trading_required = True # Simulate all components but account_id not initialized self.exchange._account_id = None self.exchange._order_book_tracker._order_books_initialized.set() self.exchange._account_balances = {} self._simulate_trading_rules_initialized() self.exchange._user_stream_tracker.data_source._last_recv_time = 0 self.assertFalse(self.exchange.ready) def test_ready_trading_not_required_ready(self): self.exchange._trading_required = False # Simulate all components but account_id not initialized self.exchange._account_id = None self.exchange._order_book_tracker._order_books_initialized.set() self.exchange._account_balances = {} self._simulate_trading_rules_initialized() self.exchange._user_stream_tracker.data_source._last_recv_time = 0 self.assertTrue(self.exchange.ready) def test_ready_trading_not_required_not_ready(self): self.exchange._trading_required = False self.assertFalse(self.exchange.ready) def test_limit_orders(self): self.assertEqual(0, len(self.exchange.limit_orders)) # Simulate orders being placed and tracked order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0) self.exchange._in_flight_orders.update({order.client_order_id: order}) self.assertEqual(1, len(self.exchange.limit_orders)) def test_tracking_states_order_not_done(self): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0) order_json = order.to_json() self.exchange._in_flight_orders.update({order.client_order_id: order}) self.assertEqual(1, len(self.exchange.tracking_states)) self.assertEqual(order_json, self.exchange.tracking_states[order.client_order_id]) def test_tracking_states_order_done(self): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0, initial_state="FILLED") self.exchange._in_flight_orders.update({order.client_order_id: order}) self.assertEqual(0, len(self.exchange.tracking_states)) def test_restore_tracking_states(self): order: MexcInFlightOrder = MexcInFlightOrder( client_order_id="0", exchange_order_id="123", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal(10.0), amount=Decimal(1.0), creation_timestamp=1640001112.0) order_json = order.to_json() self.exchange.restore_tracking_states( {order.client_order_id: order_json}) self.assertEqual(1, len(self.exchange.in_flight_orders)) self.assertEqual( str(self.exchange.in_flight_orders[order.client_order_id]), str(order))
class BinancePerpetualDerivativeUnitTest(unittest.TestCase): start_timestamp: float = pd.Timestamp("2021-01-01", tz="UTC").timestamp() @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.symbol = f"{cls.base_asset}{cls.quote_asset}" cls.domain = "binance_perpetual_testnet" @patch( "hummingbot.connector.exchange.binance.binance_time.BinanceTime.start") def setUp(self, mocked_binance_time_start) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.api_responses = asyncio.Queue() self.ws_sent_messages = [] self.ws_incoming_messages = asyncio.Queue() self.resume_test_event = asyncio.Event() self._finalMessage = 'FinalDummyMessage' self.exchange = BinancePerpetualDerivative( binance_perpetual_api_key="testAPIKey", binance_perpetual_api_secret="testSecret", trading_pairs=[self.trading_pair], domain=self.domain) self.mocking_assistant = NetworkMockingAssistant() async def _await_all_api_responses_delivered(self): await self.api_responses.join() def _get_position_risk_api_endpoint_single_position_list( self) -> List[Dict[str, Any]]: positions = [{ "symbol": self.symbol, "positionAmt": "1", "entryPrice": "10", "markPrice": "11", "unRealizedProfit": "1", "liquidationPrice": "100", "leverage": "1", "maxNotionalValue": "9", "marginType": "cross", "isolatedMargin": "0", "isAutoAddMargin": "false", "positionSide": "BOTH", "notional": "11", "isolatedWallet": "0", "updateTime": int(self.start_timestamp), }] return positions def _create_ws_mock(self): ws = AsyncMock() ws.send.side_effect = lambda sent_message: self.ws_sent_messages.append( sent_message) ws.recv.side_effect = self._get_next_ws_received_message return ws async def _get_next_ws_received_message(self): message = await self.ws_incoming_messages.get() if json.loads(message) == self._finalMessage: self.resume_test_event.set() return message def _get_account_update_ws_event_single_position_dict( self) -> Dict[str, Any]: account_update = { "e": "ACCOUNT_UPDATE", "E": 1564745798939, "T": 1564745798938, "a": { "m": "POSITION", "B": [ { "a": "USDT", "wb": "122624.12345678", "cw": "100.12345678", "bc": "50.12345678" }, ], "P": [ { "s": self.symbol, "pa": "1", "ep": "10", "cr": "200", "up": "1", "mt": "cross", "iw": "0.00000000", "ps": "BOTH" }, ] } } return account_update @aioresponses() def test_existing_account_position_detected_on_positions_update( self, req_mock): url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) self.assertEqual(len(self.exchange.account_positions), 1) pos = list(self.exchange.account_positions.values())[0] self.assertEqual(pos.trading_pair.replace("-", ""), self.symbol) @aioresponses() def test_account_position_updated_on_positions_update(self, req_mock): url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) self.assertEqual(len(self.exchange.account_positions), 1) pos = list(self.exchange.account_positions.values())[0] self.assertEqual(pos.amount, 1) positions[0]["positionAmt"] = "2" req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) pos = list(self.exchange.account_positions.values())[0] self.assertEqual(pos.amount, 2) @aioresponses() def test_new_account_position_detected_on_positions_update(self, req_mock): url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) req_mock.get(regex_url, body=json.dumps([])) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) self.assertEqual(len(self.exchange.account_positions), 0) positions = self._get_position_risk_api_endpoint_single_position_list() req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) self.assertEqual(len(self.exchange.account_positions), 1) @aioresponses() def test_closed_account_position_removed_on_positions_update( self, req_mock): url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) self.assertEqual(len(self.exchange.account_positions), 1) positions[0]["positionAmt"] = "0" req_mock.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) self.assertEqual(len(self.exchange.account_positions), 0) @aioresponses() @patch("websockets.connect", new_callable=AsyncMock) def test_new_account_position_detected_on_stream_event( self, mock_api, ws_connect_mock): url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) listen_key_response = {"listenKey": "someListenKey"} mock_api.post(regex_url, body=json.dumps(listen_key_response)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.ev_loop.create_task(self.exchange._user_stream_tracker.start()) self.assertEqual(len(self.exchange.account_positions), 0) account_update = self._get_account_update_ws_event_single_position_dict( ) self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps(account_update)) url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() mock_api.get(regex_url, body=json.dumps(positions)) self.ev_loop.create_task(self.exchange._user_stream_event_listener()) asyncio.get_event_loop().run_until_complete(asyncio.sleep(1)) self.assertEqual(len(self.exchange.account_positions), 1) @aioresponses() @patch("websockets.connect", new_callable=AsyncMock) def test_account_position_updated_on_stream_event(self, mock_api, ws_connect_mock): url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() mock_api.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) listen_key_response = {"listenKey": "someListenKey"} mock_api.post(regex_url, body=json.dumps(listen_key_response)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.ev_loop.create_task(self.exchange._user_stream_tracker.start()) self.ev_loop.run_until_complete( self._await_all_api_responses_delivered()) self.assertEqual(len(self.exchange.account_positions), 1) pos = list(self.exchange.account_positions.values())[0] self.assertEqual(pos.amount, 1) account_update = self._get_account_update_ws_event_single_position_dict( ) account_update["a"]["P"][0]["pa"] = 2 self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps(account_update)) self.ev_loop.create_task(self.exchange._user_stream_event_listener()) self.ev_loop.run_until_complete(asyncio.sleep(0.3)) self.assertEqual(len(self.exchange.account_positions), 1) pos = list(self.exchange.account_positions.values())[0] self.assertEqual(pos.amount, 2) @aioresponses() @patch("websockets.connect", new_callable=AsyncMock) def test_closed_account_position_removed_on_stream_event( self, mock_api, ws_connect_mock): url = utils.rest_url(CONSTANTS.POSITION_INFORMATION_URL, domain=self.domain, api_version=CONSTANTS.API_VERSION_V2) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) positions = self._get_position_risk_api_endpoint_single_position_list() mock_api.get(regex_url, body=json.dumps(positions)) task = self.ev_loop.create_task(self.exchange._update_positions()) self.ev_loop.run_until_complete(task) url = utils.rest_url(CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) listen_key_response = {"listenKey": "someListenKey"} mock_api.post(regex_url, body=json.dumps(listen_key_response)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.ev_loop.create_task(self.exchange._user_stream_tracker.start()) self.ev_loop.run_until_complete( self._await_all_api_responses_delivered()) self.assertEqual(len(self.exchange.account_positions), 1) account_update = self._get_account_update_ws_event_single_position_dict( ) account_update["a"]["P"][0]["pa"] = 0 self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps(account_update)) self.ev_loop.create_task(self.exchange._user_stream_event_listener()) self.ev_loop.run_until_complete(asyncio.sleep(0.3)) self.assertEqual(len(self.exchange.account_positions), 0) @aioresponses() def test_set_position_mode_initial_mode_is_none(self, mock_api): self.assertIsNone(self.exchange.position_mode) url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) get_position_mode_response = { "dualSidePosition": False # True: Hedge Mode; False: One-way Mode } post_position_mode_response = {"code": 200, "msg": "success"} mock_api.get(regex_url, body=json.dumps(get_position_mode_response)) mock_api.post(regex_url, body=json.dumps(post_position_mode_response)) task = self.ev_loop.create_task( self.exchange._set_position_mode(PositionMode.HEDGE)) self.ev_loop.run_until_complete(task) self.assertEqual(PositionMode.HEDGE, self.exchange.position_mode) @aioresponses() def test_set_position_initial_mode_unchanged(self, mock_api): self.exchange._position_mode = PositionMode.ONEWAY url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) get_position_mode_response = { "dualSidePosition": False # True: Hedge Mode; False: One-way Mode } mock_api.get(regex_url, body=json.dumps(get_position_mode_response)) task = self.ev_loop.create_task( self.exchange._set_position_mode(PositionMode.ONEWAY)) self.ev_loop.run_until_complete(task) self.assertEqual(PositionMode.ONEWAY, self.exchange.position_mode) @aioresponses() def test_set_position_mode_diff_initial_mode_change_successful( self, mock_api): self.exchange._position_mode = PositionMode.ONEWAY url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) get_position_mode_response = { "dualSidePosition": False # True: Hedge Mode; False: One-way Mode } post_position_mode_response = {"code": 200, "msg": "success"} mock_api.get(regex_url, body=json.dumps(get_position_mode_response)) mock_api.post(regex_url, body=json.dumps(post_position_mode_response)) task = self.ev_loop.create_task( self.exchange._set_position_mode(PositionMode.HEDGE)) self.ev_loop.run_until_complete(task) self.assertEqual(PositionMode.HEDGE, self.exchange.position_mode) @aioresponses() def test_set_position_mode_diff_initial_mode_change_fail(self, mock_api): self.exchange._position_mode = PositionMode.ONEWAY url = utils.rest_url(CONSTANTS.CHANGE_POSITION_MODE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) get_position_mode_response = { "dualSidePosition": False # True: Hedge Mode; False: One-way Mode } post_position_mode_response = { 'code': -4059, 'msg': 'No need to change position side.' } mock_api.get(regex_url, body=json.dumps(get_position_mode_response)) mock_api.post(regex_url, body=json.dumps(post_position_mode_response)) task = self.ev_loop.create_task( self.exchange._set_position_mode(PositionMode.HEDGE)) self.ev_loop.run_until_complete(task) self.assertEqual(PositionMode.ONEWAY, self.exchange.position_mode)
class TestAltmarketsAPIUserStreamDataSource(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() altmarkets_auth = AltmarketsAuth(api_key="someKey", secret_key="someSecret") self.data_source = AltmarketsAPIUserStreamDataSource(AsyncThrottler(Constants.RATE_LIMITS), altmarkets_auth=altmarkets_auth, trading_pairs=[self.trading_pair]) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def get_user_trades_mock(self) -> Dict: user_trades = { "trade": { "amount": "1.0", "created_at": 1615978645, "id": 9618578, "market": "rogerbtc", "order_id": 2324774, "price": "0.00000004", "side": "sell", "taker_type": "sell", "total": "0.00000004" } } return user_trades def get_user_orders_mock(self) -> Dict: user_orders = { "order": { "id": 9401, "market": "rogerbtc", "kind": "ask", "side": "sell", "ord_type": "limit", "price": "0.00000099", "avg_price": "0.00000099", "state": "wait", "origin_volume": "7000.0", "remaining_volume": "2810.1", "executed_volume": "4189.9", "at": 1596481983, "created_at": 1596481983, "updated_at": 1596553643, "trades_count": 272 } } return user_orders def get_user_balance_mock(self) -> Dict: user_balance = { "balance": { "currency": self.base_asset, "balance": "1032951.325075926", "locked": "1022943.325075926", } } return user_balance @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_user_stream_user_trades(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() output_queue = asyncio.Queue() self.ev_loop.create_task(self.data_source.listen_for_user_stream(self.ev_loop, output_queue)) resp = self.get_user_trades_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp) resp = self.get_user_orders_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp) resp = self.get_user_balance_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) ret = self.async_run_with_timeout(coroutine=output_queue.get()) self.assertEqual(ret, resp) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_user_stream_skips_subscribe_unsubscribe_messages_updates_last_recv_time(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = { "success": { "message": "subscribed", "time": 1632223851, "streams": "trade" } } self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) resp = { "success": { "message": "unsubscribed", "time": 1632223851, "streams": "trade" } } self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) output_queue = asyncio.Queue() self.ev_loop.create_task(self.data_source.listen_for_user_stream(self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_text_messages_delivered(ws_connect_mock.return_value) self.assertTrue(output_queue.empty()) np.testing.assert_allclose([time.time()], self.data_source.last_recv_time, rtol=1)
class BinanceUserStreamDataSourceUnitTests(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = cls.base_asset + cls.quote_asset cls.domain = "com" cls.listen_key = "TEST_LISTEN_KEY" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() self.binance_client = MockBinanceClient(api_key="TEST_API_KEY") self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = BinanceAPIUserStreamDataSource( binance_client=self.binance_client, domain=self.domain, throttler=self.throttler) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _raise_exception(self, exception_class): raise exception_class def _error_response(self) -> Dict[str, Any]: resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"} return resp def _user_update_event(self): # Balance Update resp = { "e": "balanceUpdate", "E": 1573200697110, "a": "BTC", "d": "100.00000000", "T": 1573200697068 } return ujson.dumps(resp) def test_last_recv_time(self): # Initial last_recv_time self.assertEqual(0, self.data_source.last_recv_time) def test_get_throttler_instance(self): self.assertIsInstance(self.data_source._get_throttler_instance(), AsyncThrottler) @patch("aiohttp.ClientSession.post") def test_get_listen_key_log_exception(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response(mock_api, 400, self._error_response()) with self.assertRaises(IOError): self.ev_loop.run_until_complete(self.data_source.get_listen_key()) @patch("aiohttp.ClientSession.post") def test_get_listen_key_successful(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response( mock_api, 200, {"listenKey": self.listen_key}) result: str = self.ev_loop.run_until_complete( self.data_source.get_listen_key()) self.assertEqual(self.listen_key, result) @patch("aiohttp.ClientSession.put") def test_ping_listen_key_log_exception(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response(mock_api, 400, self._error_response()) result: bool = self.ev_loop.run_until_complete( self.data_source.ping_listen_key(listen_key=self.listen_key)) self.assertTrue( self._is_logged( "WARNING", f"Failed to refresh the listen key {self.listen_key}: {self._error_response()}" )) self.assertFalse(result) @patch("aiohttp.ClientSession.put") def test_ping_listen_key_successful(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response(mock_api, 200, {}) result: bool = self.ev_loop.run_until_complete( self.data_source.ping_listen_key(listen_key=self.listen_key)) self.assertTrue(result) @patch("aiohttp.ClientSession.post") @patch("websockets.connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource.wait_til_next_tick", new_callable=AsyncMock) def test_listen_for_user_stream_no_listen_key(self, mock_next_tick, mock_ws, mock_post): # mock_next_tick.return_value = None self.mocking_assistant.configure_http_request_mock(mock_post) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, self._user_update_event()) # Add REST API response for get_listen_key() self.mocking_assistant.add_http_response( mock_post, 200, {"listenKey": self.listen_key}) # Add REST API response for _ping_listen_key() # self.mocking_assistant.add_http_response(mock_post, 200, {}) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) msg = self.ev_loop.run_until_complete(msg_queue.get()) self.assertTrue(msg, self._user_update_event)
class KrakenAPIOrderBookDataSourceTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.data_source = KrakenAPIOrderBookDataSource( self.throttler, trading_pairs=[self.trading_pair]) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def get_last_traded_prices_mock(self, last_trade_close: Decimal) -> Dict: last_traded_prices = { "error": [], "result": { f"X{self.base_asset}{self.quote_asset}": { "a": ["52609.60000", "1", "1.000"], "b": ["52609.50000", "1", "1.000"], "c": [str(last_trade_close), "0.00080000"], "v": ["1920.83610601", "7954.00219674"], "p": ["52389.94668", "54022.90683"], "t": [23329, 80463], "l": ["51513.90000", "51513.90000"], "h": ["53219.90000", "57200.00000"], "o": "52280.40000" } } } return last_traded_prices def get_depth_mock(self) -> Dict: depth = { "error": [], "result": { f"X{self.base_asset}{self.quote_asset}": { "asks": [["52523.00000", "1.199", 1616663113], ["52536.00000", "0.300", 1616663112]], "bids": [["52522.90000", "0.753", 1616663112], ["52522.80000", "0.006", 1616663109]] } } } return depth def get_public_asset_pair_mock(self) -> Dict: asset_pairs = { "error": [], "result": { f"X{self.base_asset}{self.quote_asset}": { "altname": f"{self.base_asset}{self.quote_asset}", "wsname": f"{self.base_asset}/{self.quote_asset}", "aclass_base": "currency", "base": self.base_asset, "aclass_quote": "currency", "quote": self.quote_asset, "lot": "unit", "pair_decimals": 5, "lot_decimals": 8, "lot_multiplier": 1, "leverage_buy": [2, 3, 4, 5], "leverage_sell": [2, 3, 4, 5], "fees": [ [0, 0.26], [50000, 0.24], ], "fees_maker": [ [0, 0.16], [50000, 0.14], ], "fee_volume_currency": "ZUSD", "margin_call": 80, "margin_stop": 40, "ordermin": "0.005" }, } } return asset_pairs def get_trade_data_mock(self) -> List: trade_data = [ 0, [["5541.20000", "0.15850568", "1534614057.321597", "s", "l", ""], ["6060.00000", "0.02455000", "1534614057.324998", "b", "l", ""]], "trade", f"{self.base_asset}/{self.quote_asset}" ] return trade_data @aioresponses() def test_get_last_traded_prices(self, mocked_api): url = f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) last_traded_price = Decimal("52641.10000") resp = self.get_last_traded_prices_mock( last_trade_close=last_traded_price) mocked_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( KrakenAPIOrderBookDataSource.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertIn(self.trading_pair, ret) self.assertEqual(float(last_traded_price), ret[self.trading_pair]) @aioresponses() def test_get_new_order_book(self, mocked_api): url = f"{CONSTANTS.BASE_URL}{CONSTANTS.SNAPSHOT_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_depth_mock() mocked_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertTrue(isinstance(ret, OrderBook)) bids_df, asks_df = ret.snapshot pair_data = resp["result"][f"X{self.base_asset}{self.quote_asset}"] first_bid_price = float(pair_data["bids"][0][0]) first_ask_price = float(pair_data["asks"][0][0]) self.assertEqual(first_bid_price, bids_df.iloc[0]["price"]) self.assertEqual(first_ask_price, asks_df.iloc[0]["price"]) @aioresponses() def test_fetch_trading_pairs(self, mocked_api): url = f"{CONSTANTS.BASE_URL}{CONSTANTS.ASSET_PAIRS_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_public_asset_pair_mock() mocked_api.get(regex_url, body=json.dumps(resp)) resp = self.async_run_with_timeout( KrakenAPIOrderBookDataSource.fetch_trading_pairs()) self.assertTrue(len(resp) == 1) self.assertIn(self.trading_pair, resp) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_trade_data_mock() self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(resp)) output_queue = asyncio.Queue() self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, output_queue)) self.mocking_assistant.run_until_all_text_messages_delivered( websocket_mock=ws_connect_mock.return_value) self.assertTrue(not output_queue.empty()) msg = output_queue.get_nowait() self.assertTrue(isinstance(msg, OrderBookMessage)) first_trade_price = resp[1][0][0] self.assertEqual(msg.content["price"], first_trade_price) self.assertTrue(not output_queue.empty()) msg = output_queue.get_nowait() self.assertTrue(isinstance(msg, OrderBookMessage)) second_trade_price = resp[1][1][0] self.assertEqual(msg.content["price"], second_trade_price)
class BitmartAPIUserStreamDataSourceTests(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.api_key = 'testAPIKey' cls.secret = 'testSecret' cls.memo = '001' cls.account_id = 528 cls.username = '******' cls.oms_id = 1 cls.ev_loop = asyncio.get_event_loop() def setUp(self) -> None: super().setUp() self.listening_task = None self.log_records = [] throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = BitmartAuth(api_key=self.api_key, secret_key=self.secret, memo=self.memo) self.data_source = BitmartAPIUserStreamDataSource(auth_assistant, throttler) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.data_source._trading_pairs = ["HBOT-USDT"] self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any(record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _raise_exception(self, exception_class): raise exception_class @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() initial_last_recv_time = self.data_source.last_recv_time self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_text_message(ws_connect_mock.return_value, json.dumps('dummyMessage')) first_received_message = self.ev_loop.run_until_complete(messages.get()) self.assertEqual('dummyMessage', first_received_message) self.assertTrue(self._is_logged('INFO', "Authenticating to User Stream...")) self.assertTrue(self._is_logged('INFO', "Successfully authenticated to User Stream.")) self.assertTrue(self._is_logged('INFO', "Successfully subscribed to all Private channels.")) sent_messages = self.mocking_assistant.text_messages_sent_through_websocket(ws_connect_mock.return_value) self.assertEqual(2, len(sent_messages)) auth_req = json.loads(sent_messages[0]) sub_req = json.loads(sent_messages[1]) self.assertTrue("op" in auth_req and "args" in auth_req and "testAPIKey" in auth_req["args"]) self.assertEqual({"op": "subscribe", "args": ["spot/user/order:HBOT_USDT"]}, sub_req) self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_fails_when_authentication_fails(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"errorCode": "test code", "errorMessage": "test err message"}) ) try: self.ev_loop.run_until_complete(self.listening_task) except Exception: pass self.assertTrue(self._is_logged("ERROR", "WebSocket login errored with message: test err message")) self.assertTrue(self._is_logged("ERROR", "Error occurred when authenticating to user stream.")) self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds...")) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_initialization(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) self.ev_loop.run_until_complete(self.listening_task) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_authentication(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if "testAPIKey" in sent_message else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append( sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) self.ev_loop.run_until_complete(self.listening_task) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_events_subscription(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if "order:HBOT_USDT" in sent_message else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(sent_message) ) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"event": "login"}) ) self.ev_loop.run_until_complete(self.listening_task) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_initialization(self, ws_connect_mock): ws_connect_mock.side_effect = Exception with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task(self.data_source._init_websocket_connection()) self.ev_loop.run_until_complete(self.listening_task) self.assertTrue(self._is_logged("NETWORK", "Unexpected error occured with BitMart WebSocket Connection")) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_authentication(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(Exception) if "testAPIKey" in sent_message else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception) try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) self.ev_loop.run_until_complete(self.listening_task) except Exception: pass self.assertTrue(self._is_logged("ERROR", "Error occurred when authenticating to user stream.")) self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds...")) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_events_subscription(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(Exception) if "order:HBOT_USDT" in sent_message else self.mocking_assistant._sent_websocket_text_messages[ws_connect_mock.return_value].append(sent_message) ) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception) try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) self.ev_loop.run_until_complete(self.listening_task) except Exception: pass self.assertTrue(self._is_logged("ERROR", "Error occured during subscribing to Bitmart private channels.")) self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds...")) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_invalid_json_message_logged(self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception(Exception) try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) # Add invalid json message self.mocking_assistant.add_websocket_text_message(ws_connect_mock.return_value, 'invalid message') self.ev_loop.run_until_complete(self.listening_task) except Exception: pass self.assertTrue(self._is_logged("ERROR", "Unexpected error when parsing BitMart user_stream message. ")) self.assertTrue(self._is_logged("ERROR", "Unexpected error with BitMart WebSocket connection. " "Retrying after 30 seconds...")) @patch('websockets.connect', new_callable=AsyncMock) def test_listen_for_user_stream_inner_messages_recv_timeout(self, ws_connect_mock): self.data_source.MESSAGE_TIMEOUT = 0.1 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() ws_connect_mock.return_value.ping.side_effect = lambda: done_callback_event.set() # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) done_callback_event = asyncio.Event() message_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, message_queue) ) self.ev_loop.run_until_complete(done_callback_event.wait()) @patch('websockets.connect', new_callable=AsyncMock) def test_listen_for_user_stream_inner_messages_recv_timeout_ping_timeout(self, ws_connect_mock): self.data_source.PING_TIMEOUT = 0.1 self.data_source.MESSAGE_TIMEOUT = 0.1 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() ws_connect_mock.return_value.close.side_effect = lambda: done_callback_event.set() ws_connect_mock.return_value.ping.side_effect = NetworkMockingAssistant.async_partial( self.mocking_assistant._get_next_websocket_text_message, ws_connect_mock.return_value ) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps({"event": "login"})) done_callback_event = asyncio.Event() message_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, message_queue) ) self.ev_loop.run_until_complete(done_callback_event.wait()) self.assertTrue(self._is_logged("WARNING", "WebSocket ping timed out. Going to reconnect..."))
class BinanceAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = cls.base_asset + cls.quote_asset cls.domain = "com" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = BinanceAPIOrderBookDataSource( trading_pairs=[self.trading_pair], throttler=self.throttler, domain=self.domain) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _raise_exception(self, exception_class): raise exception_class def _trade_update_event(self): resp = { "e": "trade", "E": 123456789, "s": self.ex_trading_pair, "t": 12345, "p": "0.001", "q": "100", "b": 88, "a": 50, "T": 123456785, "m": True, "M": True } return ujson.dumps(resp) def _order_diff_event(self): resp = { "e": "depthUpdate", "E": 123456789, "s": self.ex_trading_pair, "U": 157, "u": 160, "b": [["0.0024", "10"]], "a": [["0.0026", "100"]] } return ujson.dumps(resp) def _snapshot_response(self): resp = { "lastUpdateId": 1027024, "bids": [["4.00000000", "431.00000000"]], "asks": [["4.00000200", "12.00000000"]] } return resp @patch("aiohttp.ClientSession.get", new_callable=AsyncMock) def test_get_last_trade_prices(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) mock_response: Dict[str, Any] = { # Truncated Response "lastPrice": "100", } self.mocking_assistant.add_http_response(mock_api, 200, mock_response) result: Dict[str, float] = self.ev_loop.run_until_complete( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair]) @patch( "hummingbot.connector.exchange.binance.binance_utils.convert_from_exchange_trading_pair" ) @patch("aiohttp.ClientSession.get", new_callable=AsyncMock) def test_get_all_mid_prices(self, mock_api, mock_utils): # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange() mock_utils.return_value = self.trading_pair self.mocking_assistant.configure_http_request_mock(mock_api) mock_response: List[Dict[str, Any]] = [{ # Truncated Response "symbol": self.ex_trading_pair, "bidPrice": "99", "askPrice": "101", }] self.mocking_assistant.add_http_response(mock_api, 200, mock_response) result: Dict[str, float] = self.ev_loop.run_until_complete( self.data_source.get_all_mid_prices()) self.assertEqual(1, len(result)) self.assertEqual(100, result[self.trading_pair]) @patch( "hummingbot.connector.exchange.binance.binance_utils.convert_from_exchange_trading_pair" ) @patch("aiohttp.ClientSession.get") def test_fetch_trading_pairs(self, mock_api, mock_utils): # Mocks binance_utils for BinanceOrderBook.diff_message_from_exchange() mock_utils.return_value = self.trading_pair self.mocking_assistant.configure_http_request_mock(mock_api) mock_response: Dict[str, Any] = { # Truncated Response "symbols": [ { "symbol": self.ex_trading_pair, "status": "TRADING", "baseAsset": self.base_asset, "quoteAsset": self.quote_asset, }, ] } self.mocking_assistant.add_http_response(mock_api, 200, mock_response) result: Dict[str] = self.ev_loop.run_until_complete( self.data_source.fetch_trading_pairs()) self.assertEqual(1, len(result)) self.assertTrue(self.trading_pair in result) def test_get_throttler_instance(self): self.assertIsInstance( BinanceAPIOrderBookDataSource._get_throttler_instance(), AsyncThrottler) @patch("aiohttp.ClientSession.get") def test_get_snapshot_successful(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response(mock_api, 200, self._snapshot_response()) result: Dict[str, Any] = self.ev_loop.run_until_complete( self.data_source.get_snapshot(self.trading_pair)) self.assertEqual(self._snapshot_response(), result) @patch("aiohttp.ClientSession.get") def test_get_snapshot_catch_exception(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response(mock_api, 400, {}) with self.assertRaises(IOError): self.ev_loop.run_until_complete( self.data_source.get_snapshot(self.trading_pair)) @patch("aiohttp.ClientSession.get") def test_get_new_order_book(self, mock_api): self.mocking_assistant.configure_http_request_mock(mock_api) mock_response: Dict[str, Any] = { "lastUpdateId": 1, "bids": [["4.00000000", "431.00000000"]], "asks": [["4.00000200", "12.00000000"]] } self.mocking_assistant.add_http_response(mock_api, 200, mock_response) result: OrderBook = self.ev_loop.run_until_complete( self.data_source.get_new_order_book(self.trading_pair)) self.assertEqual(1, result.snapshot_uid) @patch("websockets.connect") def test_listen_for_trades_cancelled_when_connecting(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades_cancelled_when_listening(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.recv.side_effect = lambda: (self._raise_exception( asyncio.CancelledError)) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades_logs_exception(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None incomplete_resp = { "m": 1, "i": 2, } self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, ujson.dumps(incomplete_resp)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) with self.assertRaises(asyncio.TimeoutError): self.ev_loop.run_until_complete( asyncio.wait_for(self.listening_task, 1)) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades_successful(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, self._trade_update_event()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) msg: OrderBookMessage = self.ev_loop.run_until_complete( msg_queue.get()) self.assertTrue(12345, msg.trade_id) @patch("websockets.connect") def test_listen_for_order_book_diffs_cancelled_when_connecting( self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_cancelled_when_listening( self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.recv.side_effect = lambda: (self._raise_exception( asyncio.CancelledError)) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) self.ev_loop.run_until_complete(self.listening_task) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_logs_exception(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None incomplete_resp = { "m": 1, "i": 2, } self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, ujson.dumps(incomplete_resp)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) with self.assertRaises(asyncio.TimeoutError): self.ev_loop.run_until_complete( asyncio.wait_for(self.listening_task, 1)) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, self._order_diff_event()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.ev_loop.run_until_complete( msg_queue.get()) self.assertTrue(12345, msg.update_id) @patch("aiohttp.ClientSession.get") def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self, mock_api): mock_api.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.ev_loop.run_until_complete( self.data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue())) @patch("aiohttp.ClientSession.get") def test_listen_for_order_book_snapshots_log_exception(self, mock_api): msg_queue: asyncio.Queue = asyncio.Queue() mock_api.side_effect = lambda: self._raise_exception(Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) with self.assertRaises(asyncio.TimeoutError): self.ev_loop.run_until_complete( asyncio.wait_for(self.listening_task, 1)) self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." )) @patch("aiohttp.ClientSession.get") def test_listen_for_order_book_snapshots_successful( self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() self.mocking_assistant.configure_http_request_mock(mock_api) self.mocking_assistant.add_http_response(mock_api, 200, self._snapshot_response()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.ev_loop.run_until_complete( msg_queue.get()) self.assertTrue(12345, msg.update_id)
class NdaxAPIUserStreamDataSourceTests(TestCase): # the level is required to receive logs from the data source loger level = 0 def setUp(self) -> None: super().setUp() self.uid = '001' self.api_key = 'testAPIKey' self.secret = 'testSecret' self.account_id = 528 self.username = '******' self.oms_id = 1 self.log_records = [] self.listening_task = None throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = NdaxAuth(uid=self.uid, api_key=self.api_key, secret_key=self.secret, account_name=self.username) self.data_source = NdaxAPIUserStreamDataSource(throttler, auth_assistant) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _authentication_response(self, authenticated: bool) -> str: user = { "UserId": 492, "UserName": "******", "Email": "*****@*****.**", "EmailVerified": True, "AccountId": self.account_id, "OMSId": self.oms_id, "Use2FA": True } payload = { "Authenticated": authenticated, "SessionToken": "74e7c5b0-26b1-4ca5-b852-79b796b0e599", "User": user, "Locked": False, "Requires2FA": False, "EnforceEnable2FA": False, "TwoFAType": None, "TwoFAToken": None, "errormsg": None } message = { "m": 1, "i": 1, "n": CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME, "o": json.dumps(payload) } return json.dumps(message) def _raise_exception(self, exception_class): raise exception_class @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) initial_last_recv_time = self.data_source.last_recv_time self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, self._authentication_response(True)) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps('dummyMessage')) first_received_message = asyncio.get_event_loop().run_until_complete( messages.get()) self.assertEqual('dummyMessage', first_received_message) self.assertTrue( self._is_logged('INFO', "Authenticating to User Stream...")) self.assertTrue( self._is_logged('INFO', "Successfully authenticated to User Stream.")) self.assertTrue( self._is_logged('INFO', "Successfully subscribed to user events.")) sent_messages = self.mocking_assistant.text_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(2, len(sent_messages)) authentication_request = sent_messages[0] subscription_request = sent_messages[1] self.assertEqual( CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME, NdaxWebSocketAdaptor.endpoint_from_raw_message( authentication_request)) self.assertEqual( CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME, NdaxWebSocketAdaptor.endpoint_from_raw_message( subscription_request)) subscription_payload = NdaxWebSocketAdaptor.payload_from_raw_message( subscription_request) expected_payload = {"AccountId": self.account_id, "OMSId": self.oms_id} self.assertEqual(expected_payload, subscription_payload) self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_fails_when_authentication_fails( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, self._authentication_response(False)) try: asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream " "(Could not authenticate websocket connection with NDAX)")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. " "(Could not authenticate websocket connection with NDAX)")) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_initialization( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message else self.mocking_assistant._sent_websocket_text_messages[ ws_connect_mock.return_value].append(sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_events_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message else self.mocking_assistant._sent_websocket_text_messages[ ws_connect_mock.return_value].append(sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_initialization( self, ws_connect_mock): ws_connect_mock.side_effect = Exception with self.assertRaises(Exception): self.listening_task = asyncio.get_event_loop().create_task( self.data_source._init_websocket_connection()) asyncio.get_event_loop().run_until_complete(self.listening_task) self.assertTrue( self._is_logged( "NETWORK", "Unexpected error occurred during ndax WebSocket Connection ()" )) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_logs_exception_details_during_authentication( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message else self.mocking_assistant._sent_websocket_text_messages[ ws_connect_mock.return_value].append(sent_message)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred when authenticating to user stream ()")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. ()" )) @patch('websockets.connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_events_subscription( self, ws_connect_mock): messages = asyncio.Queue() ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send.side_effect = lambda sent_message: ( CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message and self._raise_exception(Exception)) # Make the close function raise an exception to finish the execution ws_connect_mock.return_value.close.side_effect = lambda: self._raise_exception( Exception) try: self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream( asyncio.get_event_loop(), messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Error occurred subscribing to ndax private channels ()")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with NDAX WebSocket connection. Retrying in 30 seconds. ()" ))
class CoinzoomAPIOrderBookDataSourceTests(TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "testKey" cls.api_secret_key = "testSecretKey" cls.username = "******" cls.throttler = AsyncThrottler(Constants.RATE_LIMITS) def setUp(self) -> None: super().setUp() self.listening_task = None self.data_source = CoinzoomAPIOrderBookDataSource( throttler=self.throttler, trading_pairs=[self.trading_pair]) self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = asyncio.get_event_loop().run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @aioresponses() def test_get_last_traded_prices(self, mock_api): url = f"{Constants.REST_URL}/{Constants.ENDPOINT['TICKER']}" resp = { f"{self.base_asset}_{self.quote_asset}": { "last_price": 51234.56 } } mock_api.get(url, body=json.dumps(resp)) results = self.async_run_with_timeout( CoinzoomAPIOrderBookDataSource.get_last_traded_prices( trading_pairs=[self.trading_pair], throttler=self.throttler)) self.assertIn(self.trading_pair, results) self.assertEqual(Decimal("51234.56"), results[self.trading_pair]) @aioresponses() def test_fetch_trading_pairs(self, mock_api): url = f"{Constants.REST_URL}/{Constants.ENDPOINT['SYMBOL']}" resp = [{ "symbol": f"{self.base_asset}/{self.quote_asset}" }, { "symbol": "BTC/USDT" }] mock_api.get(url, body=json.dumps(resp)) results = self.async_run_with_timeout( CoinzoomAPIOrderBookDataSource.fetch_trading_pairs( throttler=self.throttler)) self.assertIn(self.trading_pair, results) self.assertIn("BTC-USDT", results) @aioresponses() def test_get_new_order_book(self, mock_api): url = f"{Constants.REST_URL}/" \ f"{Constants.ENDPOINT['ORDER_BOOK'].format(trading_pair=self.base_asset+'_'+self.quote_asset)}" resp = {"timestamp": 1234567899, "bids": [], "asks": []} mock_api.get(url, body=json.dumps(resp)) order_book: CoinzoomOrderBook = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertEqual(1234567899, order_book.snapshot_uid) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_trades(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = { "ts": [ f"{self.base_asset}/{self.quote_asset}", 8772.05, 0.01, "2020-01-16T21:02:23Z" ] } self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_trades( ev_loop=asyncio.get_event_loop(), output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) trade_message = self.async_run_with_timeout(received_messages.get()) self.assertEqual(OrderBookMessageType.TRADE, trade_message.type) self.assertEqual( int(dateparse("2020-01-16T21:02:23Z").timestamp() * 1e3), trade_message.timestamp) self.assertEqual(trade_message.timestamp, trade_message.trade_id) self.assertEqual(self.trading_pair, trade_message.trading_pair) @patch( "hummingbot.connector.exchange.coinzoom.coinzoom_api_order_book_data_source.CoinzoomAPIOrderBookDataSource._time" ) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diff(self, ws_connect_mock, time_mock): time_mock.return_value = 1234567890 ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) received_messages = asyncio.Queue() message = { "oi": f"{self.base_asset}/{self.quote_asset}", "b": [["9"], ["5"], ["7", 7193.27, 6.95094164], ["8", 7196.15, 0.69481598]], "s": [["2"], ["1"], ["4", 7222.08, 6.92321326], ["6", 7219.2, 0.69259752]] } self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_order_book_diffs( ev_loop=asyncio.get_event_loop(), output=received_messages)) self.mocking_assistant.add_websocket_text_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(message)) diff_message = self.async_run_with_timeout(received_messages.get()) self.assertEqual(OrderBookMessageType.DIFF, diff_message.type) self.assertEqual(1234567890 * 1e3, diff_message.timestamp) self.assertEqual(diff_message.timestamp, diff_message.update_id) self.assertEqual(-1, diff_message.trade_id) self.assertEqual(self.trading_pair, diff_message.trading_pair)
class BitmartAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.log_records = [] self.ws_sent_messages = [] self.ws_incoming_messages = asyncio.Queue() self.listening_task = None self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = BitmartAPIOrderBookDataSource( self.throttler, [self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _order_book_snapshot_example(self): return { "data": { "timestamp": 1527777538000, "buys": [ { "amount": "4800.00", "total": "4800.00", "price": "0.000767", "count": "1" }, { "amount": "99996475.79", "total": "100001275.79", "price": "0.000201", "count": "1" }, ], "sells": [ { "amount": "100.00", "total": "100.00", "price": "0.007000", "count": "1" }, { "amount": "6997.00", "total": "7097.00", "price": "1.000000", "count": "1" }, ] } } def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) @aioresponses() def test_get_last_traded_prices(self, mock_get): mock_response: Dict[Any] = { "message": "OK", "code": 1000, "trace": "6e42c7c9-fdc5-461b-8fd1-b4e2e1b9ed57", "data": { "tickers": [{ "symbol": "COINALPHA_HBOT", "last_price": "1.00", "quote_volume_24h": "201477650.88000", "base_volume_24h": "25186.48000", "high_24h": "8800.00", "low_24h": "1.00", "open_24h": "8800.00", "close_24h": "1.00", "best_ask": "0.00", "best_ask_size": "0.00000", "best_bid": "0.00", "best_bid_size": "0.00000", "fluctuation": "-0.9999", "url": "https://www.bitmart.com/trade?symbol=COINALPHA_HBOT" }] } } regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_LAST_TRADING_PRICES_PATH_URL}" ) mock_get.get(regex_url, body=json.dumps(mock_response)) results = self.ev_loop.run_until_complete( asyncio.gather( self.data_source.get_last_traded_prices([self.trading_pair]))) results: Dict[str, Any] = results[0] self.assertEqual(results[self.trading_pair], float("1.00")) @aioresponses() def test_fetch_trading_pairs(self, mock_get): mock_response: List[Any] = { "code": 1000, "trace": "886fb6ae-456b-4654-b4e0-d681ac05cea1", "message": "OK", "data": { "symbols": [ "COINALPHA_HBOT", "ANOTHER_MARKET", ] } } regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_TRADING_PAIRS_PATH_URL}") mock_get.get(regex_url, body=json.dumps(mock_response)) results: List[str] = self.ev_loop.run_until_complete( self.data_source.fetch_trading_pairs()) self.assertTrue(self.trading_pair in results) self.assertTrue("ANOTHER-MARKET" in results) @aioresponses() def test_fetch_trading_pairs_with_error_status_in_response(self, mock_get): mock_response = {} regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_TRADING_PAIRS_PATH_URL}") mock_get.get(regex_url, body=json.dumps(mock_response)) result = self.ev_loop.run_until_complete( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @aioresponses() def test_get_order_book_data(self, mock_get): mock_response: Dict[str, Any] = self._order_book_snapshot_example() regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}") mock_get.get(regex_url, body=json.dumps(mock_response)) results = self.ev_loop.run_until_complete( asyncio.gather( self.data_source.get_order_book_data(self.trading_pair))) result = results[0] self.assertTrue("timestamp" in result) self.assertTrue("buys" in result) self.assertTrue("sells" in result) self.assertGreaterEqual(len(result["buys"]) + len(result["sells"]), 0) self.assertEqual(mock_response["data"]["buys"][0], result["buys"][0]) @aioresponses() def test_get_order_book_data_raises_exception_when_response_has_error_code( self, mock_get): regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}") mock_get.get(regex_url, status=100, body=json.dumps({})) with self.assertRaises(IOError) as context: self.ev_loop.run_until_complete( self.data_source.get_order_book_data(self.trading_pair)) self.assertEqual( str(context.exception), f"Error fetching OrderBook for {self.trading_pair} at {CONSTANTS.EXCHANGE_NAME}. " f"HTTP status is {100}.") @aioresponses() def test_get_new_order_book(self, mock_get): mock_response: Dict[str, Any] = self._order_book_snapshot_example() regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}") mock_get.get(regex_url, body=json.dumps(mock_response)) results = self.ev_loop.run_until_complete( asyncio.gather( self.data_source.get_new_order_book(self.trading_pair))) result: OrderBook = results[0] self.assertTrue(type(result) == OrderBook) self.assertEqual(result.snapshot_uid, mock_response["data"]["timestamp"]) def test_listen_for_snapshots_cancelled_when_fetching_snapshot(self): trades_queue = asyncio.Queue() task = asyncio.get_event_loop().create_task( self.data_source.listen_for_order_book_snapshots( ev_loop=asyncio.get_event_loop(), output=trades_queue)) with self.assertRaises(asyncio.CancelledError): task.cancel() asyncio.get_event_loop().run_until_complete(task) @aioresponses() @patch( "hummingbot.connector.exchange.bitmart.bitmart_api_order_book_data_source.BitmartAPIOrderBookDataSource._sleep", new_callable=AsyncMock) def test_listen_for_snapshots_logs_exception_when_fetching_snapshot( self, mock_get, mock_sleep): # the queue and the division by zero error are used just to synchronize the test sync_queue = deque() sync_queue.append(1) sync_queue.append(2) regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}") mock_get.get(regex_url, body=json.dumps({})) mock_sleep.side_effect = lambda delay: 1 / 0 if len( sync_queue) == 0 else sync_queue.pop() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(ZeroDivisionError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_order_book_snapshots( asyncio.get_event_loop(), msg_queue)) asyncio.get_event_loop().run_until_complete(self.listening_task) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occured listening for orderbook snapshots. Retrying in 5 secs..." )) @aioresponses() @patch( "hummingbot.connector.exchange.bitmart.bitmart_api_order_book_data_source.BitmartAPIOrderBookDataSource._sleep", new_callable=AsyncMock) def test_listen_for_snapshots_successful(self, mock_get, mock_sleep): # the queue and the division by zero error are used just to synchronize the test sync_queue = deque() sync_queue.append(1) sync_queue.append(2) mock_response: Dict[str, Any] = self._order_book_snapshot_example() regex_url = re.compile( f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_ORDER_BOOK_PATH_URL}") mock_get.get(regex_url, body=json.dumps(mock_response)) mock_sleep.side_effect = lambda delay: 1 / 0 if len( sync_queue) == 0 else sync_queue.pop() msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(ZeroDivisionError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_order_book_snapshots( asyncio.get_event_loop(), msg_queue)) asyncio.get_event_loop().run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 1) snapshot_msg: OrderBookMessage = msg_queue.get_nowait() self.assertEqual(snapshot_msg.update_id, mock_response["data"]["timestamp"]) @patch('websockets.connect', new_callable=AsyncMock) def test_listen_for_order_book_diffs_cancelled_when_listening( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) with self.assertRaises(asyncio.CancelledError): self.listening_task.cancel() asyncio.get_event_loop().run_until_complete(self.listening_task) self.assertEqual(msg_queue.qsize(), 0) @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None resp = { "table": "spot/depth500", "data": [{ "asks": [["161.96", "7.37567"]], "bids": [["161.94", "4.552355"]], "symbol": "ETH_USDT", "ms_t": 1542337219120 }] } self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, ujson.dumps(resp)) BitmartAPIOrderBookDataSource._trading_pairs = ["ETH-USDT"] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) first_msg: OrderBookMessage = self.ev_loop.run_until_complete( msg_queue.get()) self.assertTrue(first_msg.type == OrderBookMessageType.SNAPSHOT) @patch("websockets.connect", new_callable=AsyncMock) def test_websocket_connection_creation_raises_cancel_exception( self, mock_ws): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): asyncio.get_event_loop().run_until_complete( self.data_source._create_websocket_connection()) @patch("websockets.connect", new_callable=AsyncMock) def test_websocket_connection_creation_raises_exception_after_loging( self, mock_ws): mock_ws.side_effect = Exception with self.assertRaises(Exception): asyncio.get_event_loop().run_until_complete( self.data_source._create_websocket_connection()) self.assertTrue( self._is_logged( "NETWORK", "Unexpected error occurred during bitmart WebSocket Connection ()" )) def _trade_ws_messsage(self): resp = { "table": "spot/trade", "data": [{ "symbol": "ETH_USDT", "price": "162.12", "side": "buy", "size": "11.085", "s_t": 1542337219 }, { "symbol": "ETH_USDT", "price": "163.12", "side": "buy", "size": "15", "s_t": 1542337238 }] } return ujson.dumps(resp) @patch('websockets.connect', new_callable=AsyncMock) def test_listen_for_trades(self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.close.return_value = None # Add message to be processed after subscriptions, to unlock the test self.mocking_assistant.add_websocket_text_message( mock_ws.return_value, self._trade_ws_messsage()) BitmartAPIOrderBookDataSource._trading_pairs = ["ETH-USDT"] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) trade1: OrderBookMessage = self.ev_loop.run_until_complete( msg_queue.get()) trade2: OrderBookMessage = self.ev_loop.run_until_complete( msg_queue.get()) self.assertTrue(msg_queue.empty()) self.assertEqual(1542337219 * 1000, int(trade1.trade_id)) self.assertEqual(1542337238 * 1000, int(trade2.trade_id))
class TestKucoinAPIUserStreamDataSource(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.auth = KucoinAuth(self.api_key, self.api_passphrase, self.api_secret_key) self.data_source = KucoinAPIUserStreamDataSource( self.throttler, self.auth) self.mocking_assistant = NetworkMockingAssistant() def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @staticmethod def get_listen_key_mock(): listen_key = { "code": "200000", "data": { "token": "someToken", "instanceServers": [{ "endpoint": "wss://someEndpoint", "encrypt": True, "protocol": "websocket", "pingInterval": 18000, "pingTimeout": 10000, }] } } return listen_key @aioresponses() def test_get_listen_key_raises(self, mock_api): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL mock_api.post(url, status=500) with self.assertRaises(IOError): self.async_run_with_timeout(self.data_source.get_listen_key()) @aioresponses() def test_get_listen_key(self, mock_api): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) ret = self.async_run_with_timeout(self.data_source.get_listen_key()) self.assertEqual(ret, resp) # shallow comparison ok @aioresponses() @patch("websockets.connect", new_callable=AsyncMock) def test_listen_to_user_stream(self, mock_api, ws_connect_mock): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) msg = "someMsg" msg_queue = asyncio.Queue() self.mocking_assistant.add_websocket_text_message( ws_connect_mock.return_value, json.dumps(msg)) self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) self.mocking_assistant.run_until_all_text_messages_delivered( ws_connect_mock.return_value) self.assertTrue(not msg_queue.empty()) queued = msg_queue.get_nowait() self.assertEqual(msg, queued) @aioresponses() @patch("websockets.connect", new_callable=AsyncMock) def test_listen_for_user_stream_closes_ws_on_exception( self, mock_api, ws_connect_mock): url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PRIVATE_WS_DATA_PATH_URL resp = self.get_listen_key_mock() mock_api.post(url, body=json.dumps(resp)) raised_event = asyncio.Event() async def raise_exception(): raised_event.set() raise IOError ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.recv.side_effect = raise_exception self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, asyncio.Queue())) self.async_run_with_timeout(coroutine=raised_event.wait()) ws_connect_mock.return_value.close.assert_called()