class CoinzoomWebsocketTests(TestCase): def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() 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 @patch("websockets.connect", new_callable=AsyncMock) def test_send_subscription_message(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() throttler = AsyncThrottler(Constants.RATE_LIMITS) websocket = CoinzoomWebsocket(throttler=throttler) message = {Constants.WS_SUB["TRADES"]: {'symbol': "BTC/USDT"}} self.async_run_with_timeout(websocket.connect()) self.async_run_with_timeout(websocket.subscribe(message)) self.async_run_with_timeout(websocket.unsubscribe(message)) sent_requests = self.mocking_assistant.text_messages_sent_through_websocket(ws_connect_mock.return_value) sent_subscribe_message = json.loads(sent_requests[0]) expected_subscribe_message = {"TradeSummaryRequest": {"action": "subscribe", "symbol": "BTC/USDT"}} self.assertEquals(expected_subscribe_message, sent_subscribe_message) sent_unsubscribe_message = json.loads(sent_requests[0]) expected_unsubscribe_message = {"TradeSummaryRequest": {"action": "subscribe", "symbol": "BTC/USDT"}} self.assertEquals(expected_unsubscribe_message, sent_unsubscribe_message)
class MexcUserStreamTrackerTests(TestCase): def setUp(self) -> None: super().setUp() self.ws_sent_messages = [] self.ws_incoming_messages = asyncio.Queue() self.listening_task = None throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = MexcAuth( api_key='testAPIKey', secret_key='testSecret', ) self.tracker = MexcUserStreamTracker(throttler=throttler, mexc_auth=auth_assistant) self.mocking_assistant = NetworkMockingAssistant() self.ev_loop = asyncio.get_event_loop() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, ujson.dumps({'channel': 'push.personal.order'})) self.listening_task = asyncio.get_event_loop().create_task( self.tracker.start()) first_received_message = self.async_run_with_timeout( self.tracker.user_stream.get()) self.assertEqual({'channel': 'push.personal.order'}, first_received_message)
class MexcAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "BTC" cls.quote_asset = "USDT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.instrument_id = 1 def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = MexcAPIOrderBookDataSource( throttler=self.throttler, trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _raise_exception(self, exception_class): raise exception_class def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @aioresponses() def test_get_last_traded_prices(self, mock_api): mock_response: Dict[Any] = { "code": 200, "data": [{ "symbol": "BTC_USDT", "volume": "1076.002782", "high": "59387.98", "low": "57009", "bid": "57920.98", "ask": "57921.03", "open": "57735.92", "last": "57902.52", "time": 1637898900000, "change_rate": "0.00288555" }] } url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_TICKERS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(mock_response)) results = self.async_run_with_timeout( asyncio.gather( self.data_source.get_last_traded_prices([self.trading_pair]))) results: Dict[str, Any] = results[0] self.assertEqual(results[self.trading_pair], 57902.52) @aioresponses() def test_fetch_trading_pairs_with_error_status_in_response(self, mock_api): mock_response = {} url = CONSTANTS.MEXC_BASE_URL + CONSTANTS.MEXC_SYMBOL_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, body=json.dumps(mock_response), status=100) result = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_api_order_book_data_source.microseconds" ) def test_get_order_book_data(self, mock_api, ms_mock): ms_mock.return_value = 1 mock_response = { "code": 200, "data": { "asks": [{ "price": "57974.06", "quantity": "0.247421" }], "bids": [{ "price": "57974.01", "quantity": "0.201635" }], "ts": 1, "version": "562370278" } } trading_pair = convert_to_exchange_trading_pair(self.trading_pair) tick_url = CONSTANTS.MEXC_DEPTH_URL.format(trading_pair=trading_pair) url = CONSTANTS.MEXC_BASE_URL + tick_url mock_api.get(url, body=json.dumps(mock_response)) results = self.async_run_with_timeout( asyncio.gather( self.data_source.get_snapshot(self.data_source._shared_client, self.trading_pair))) result = results[0] self.assertTrue("asks" in result) self.assertGreaterEqual(len(result), 0) self.assertEqual(mock_response.get("data"), result) @aioresponses() def test_get_order_book_data_raises_exception_when_response_has_error_code( self, mock_api): mock_response = "Erroneous response" trading_pair = convert_to_exchange_trading_pair(self.trading_pair) tick_url = CONSTANTS.MEXC_DEPTH_URL.format(trading_pair=trading_pair) url = CONSTANTS.MEXC_BASE_URL + tick_url mock_api.get(url, body=json.dumps(mock_response), status=100) with self.assertRaises(IOError) as context: self.async_run_with_timeout( self.data_source.get_snapshot(self.data_source._shared_client, self.trading_pair)) self.assertEqual( str(context.exception), f'Error fetching MEXC market snapshot for {self.trading_pair.replace("-", "_")}. ' f'HTTP status is {100}.') @aioresponses() def test_get_new_order_book(self, mock_api): mock_response = { "code": 200, "data": { "asks": [{ "price": "57974.06", "quantity": "0.247421" }], "bids": [{ "price": "57974.01", "quantity": "0.201635" }], "version": "562370278" } } trading_pair = convert_to_exchange_trading_pair(self.trading_pair) tick_url = CONSTANTS.MEXC_DEPTH_URL.format(trading_pair=trading_pair) url = CONSTANTS.MEXC_BASE_URL + tick_url mock_api.get(url, body=json.dumps(mock_response)) results = self.async_run_with_timeout( asyncio.gather( self.data_source.get_new_order_book(self.trading_pair))) result: OrderBook = results[0] self.assertTrue(type(result) == OrderBook) @aioresponses() def test_listen_for_snapshots_cancelled_when_fetching_snapshot( self, mock_api): trading_pair = convert_to_exchange_trading_pair(self.trading_pair) tick_url = CONSTANTS.MEXC_DEPTH_URL.format(trading_pair=trading_pair) url = CONSTANTS.MEXC_BASE_URL + tick_url mock_api.get(url, exception=asyncio.CancelledError) msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) self.assertEqual(msg_queue.qsize(), 0) @aioresponses() @patch( "hummingbot.connector.exchange.mexc.mexc_api_order_book_data_source.MexcAPIOrderBookDataSource._sleep" ) def test_listen_for_snapshots_successful(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) mock_response = { "code": 200, "data": { "asks": [{ "price": "57974.06", "quantity": "0.247421" }], "bids": [{ "price": "57974.01", "quantity": "0.201635" }], "version": "562370278" } } trading_pair = convert_to_exchange_trading_pair(self.trading_pair) tick_url = CONSTANTS.MEXC_DEPTH_URL.format(trading_pair=trading_pair) url = CONSTANTS.MEXC_BASE_URL + tick_url mock_api.get(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.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.assertEqual(msg_queue.qsize(), 1) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_cancelled_when_subscribing( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.send_str.side_effect = asyncio.CancelledError() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, {'channel': 'push.personal.order'}) with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_cancelled_when_listening( self, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() data = { 'symbol': 'MX_USDT', 'data': { 'version': '44000093', 'bids': [{ 'p': '2.9311', 'q': '0.00', 'a': '0.00000000' }], 'asks': [{ 'p': '2.9311', 'q': '22720.37', 'a': '66595.6765' }] }, 'channel': 'push.depth' } self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, ujson.dumps(data)) safe_ensure_future(self.data_source.listen_for_subscriptions()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) first_msg = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(first_msg.type == OrderBookMessageType.DIFF) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_websocket_connection_creation_raises_cancel_exception( self, mock_ws): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source._create_websocket_connection()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_websocket_connection_creation_raises_exception_after_loging( self, mock_ws): mock_ws.side_effect = Exception with self.assertRaises(Exception): self.async_run_with_timeout( self.data_source._create_websocket_connection()) self.assertTrue( self._is_logged( "NETWORK", 'Unexpected error occured connecting to mexc WebSocket API. ()' ))
class TestBybitAPIOrderBookDataSource(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 = CONSTANTS.DEFAULT_DOMAIN def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.time_synchronnizer = TimeSynchronizer() self.time_synchronnizer.add_time_offset_ms_sample(1000) self.ob_data_source = BybitAPIOrderBookDataSource( trading_pairs=[self.trading_pair], throttler=self.throttler, time_synchronizer=self.time_synchronnizer) self.ob_data_source.logger().setLevel(1) self.ob_data_source.logger().addHandler(self) BybitAPIOrderBookDataSource._trading_pair_symbol_map = { self.domain: bidict({self.ex_trading_pair: self.trading_pair}) } def tearDown(self) -> None: self.async_task and self.async_task.cancel() BybitAPIOrderBookDataSource._trading_pair_symbol_map = {} 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 # TRADING PAIRS @aioresponses() def test_fetch_trading_pairs(self, mock_api): BybitAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL) resp = { "ret_code": 0, "ret_msg": "", "ext_code": None, "ext_info": None, "result": [{ "name": "BTCUSDT", "alias": "BTCUSDT", "baseCurrency": "BTC", "quoteCurrency": "USDT", "basePrecision": "0.000001", "quotePrecision": "0.01", "minTradeQuantity": "0.0001", "minTradeAmount": "10", "minPricePrecision": "0.01", "maxTradeQuantity": "2", "maxTradeAmount": "200", "category": 1 }, { "name": "ETHUSDT", "alias": "ETHUSDT", "baseCurrency": "ETH", "quoteCurrency": "USDT", "basePrecision": "0.0001", "quotePrecision": "0.01", "minTradeQuantity": "0.0001", "minTradeAmount": "10", "minPricePrecision": "0.01", "maxTradeQuantity": "2", "maxTradeAmount": "200", "category": 1 }] } mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=BybitAPIOrderBookDataSource.fetch_trading_pairs( domain=self.domain, throttler=self.throttler, time_synchronizer=self.time_synchronnizer, )) self.assertEqual(2, len(ret)) self.assertEqual("BTC-USDT", ret[0]) self.assertEqual("ETH-USDT", ret[1]) @aioresponses() def test_fetch_trading_pairs_exception_raised(self, mock_api): BybitAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.async_run_with_timeout( self.ob_data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) # LAST TRADED PRICES @aioresponses() def test_get_last_traded_prices(self, mock_api): BybitAPIOrderBookDataSource._trading_pair_symbol_map[ CONSTANTS.DEFAULT_DOMAIN]["TKN1TKN2"] = "TKN1-TKN2" url1 = web_utils.rest_url(path_url=CONSTANTS.LAST_TRADED_PRICE_PATH, domain=CONSTANTS.DEFAULT_DOMAIN) url1 = f"{url1}?symbol={self.ex_trading_pair}" regex_url = re.compile(f"^{url1}".replace(".", r"\.").replace("?", r"\?")) resp = { "ret_code": 0, "ret_msg": None, "result": { "symbol": self.ex_trading_pair, "price": "50008" }, "ext_code": None, "ext_info": None } mock_api.get(regex_url, body=json.dumps(resp)) url2 = web_utils.rest_url(path_url=CONSTANTS.LAST_TRADED_PRICE_PATH, domain=CONSTANTS.DEFAULT_DOMAIN) url2 = f"{url2}?symbol=TKN1TKN2" regex_url = re.compile(f"^{url2}".replace(".", r"\.").replace("?", r"\?")) resp = { "ret_code": 0, "ret_msg": None, "result": { "symbol": "TKN1TKN2", "price": "2050" }, "ext_code": None, "ext_info": None } mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=BybitAPIOrderBookDataSource.get_last_traded_prices( [self.trading_pair, "TKN1-TKN2"])) ticker_requests = [(key, value) for key, value in mock_api.requests.items() if key[1].human_repr().startswith(url1) or key[1].human_repr().startswith(url2)] request_params = ticker_requests[0][1][0].kwargs["params"] self.assertEqual(self.ex_trading_pair, request_params["symbol"]) request_params = ticker_requests[1][1][0].kwargs["params"] self.assertEqual("TKN1TKN2", request_params["symbol"]) self.assertEqual(ret[self.trading_pair], 50008) self.assertEqual(ret["TKN1-TKN2"], 2050) # ORDER BOOK SNAPSHOT @staticmethod def _snapshot_response() -> Dict: snapshot = { "ret_code": 0, "ret_msg": None, "result": { "time": 1620886105740, "bids": [["50005.12", "403.0416"]], "asks": [["50006.34", "0.2297"]] }, "ext_code": None, "ext_info": None } return snapshot @staticmethod def _snapshot_response_processed() -> Dict: snapshot_processed = { "time": 1620886105740, "bids": [["50005.12", "403.0416"]], "asks": [["50006.34", "0.2297"]] } return snapshot_processed @aioresponses() def test_get_snapshot(self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) snapshot_data = self._snapshot_response() mock_api.get(regex_url, body=json.dumps(snapshot_data)) ret = self.async_run_with_timeout( coroutine=self.ob_data_source.get_snapshot(self.trading_pair)) self.assertEqual( ret, self._snapshot_response_processed()) # shallow comparison ok @aioresponses() def test_get_snapshot_raises(self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=500) with self.assertRaises(IOError): self.async_run_with_timeout( coroutine=self.ob_data_source.get_snapshot(self.trading_pair)) @aioresponses() def test_get_new_order_book(self, mock_api): url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self._snapshot_response() mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.ob_data_source.get_new_order_book( self.trading_pair)) bid_entries = list(ret.bid_entries()) ask_entries = list(ret.ask_entries()) self.assertEqual(1, len(bid_entries)) self.assertEqual(50005.12, bid_entries[0].price) self.assertEqual(403.0416, bid_entries[0].amount) self.assertEqual(int(resp["result"]["time"]), bid_entries[0].update_id) self.assertEqual(1, len(ask_entries)) self.assertEqual(50006.34, ask_entries[0].price) self.assertEqual(0.2297, ask_entries[0].amount) self.assertEqual(int(resp["result"]["time"]), ask_entries[0].update_id) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_trades_and_depth( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = { 'topic': 'trade', 'event': 'sub', 'symbol': self.ex_trading_pair, 'params': { 'binary': 'false', 'symbolName': self.ex_trading_pair }, 'code': '0', 'msg': 'Success' } result_subscribe_depth = { 'topic': 'depth', 'event': 'sub', 'symbol': self.ex_trading_pair, 'params': { 'binary': 'false', 'symbolName': self.ex_trading_pair }, 'code': '0', 'msg': 'Success' } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_depth)) self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_trade_subscription = { "topic": "trade", "event": "sub", "symbol": self.ex_trading_pair, "params": { "binary": False } } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { "topic": "diffDepth", "event": "sub", "symbol": self.ex_trading_pair, "params": { "binary": False } } self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", f"Subscribed to public order book and trade channels of {self.trading_pair}..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.bybit.bybit_api_order_book_data_source.BybitAPIOrderBookDataSource._time" ) def test_listen_for_subscriptions_sends_ping_message_before_ping_interval_finishes( self, time_mock, ws_connect_mock): time_mock.side_effect = [ 1000, 1100, 1101, 1102 ] # Simulate first ping interval is already due ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = { 'topic': 'trade', 'event': 'sub', 'symbol': self.ex_trading_pair, 'params': { 'binary': 'false', 'symbolName': self.ex_trading_pair }, 'code': '0', 'msg': 'Success' } result_subscribe_depth = { 'topic': 'depth', 'event': 'sub', 'symbol': self.ex_trading_pair, 'params': { 'binary': 'false', 'symbolName': self.ex_trading_pair }, 'code': '0', 'msg': 'Success' } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_depth)) self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) expected_ping_message = {"ping": int(1101 * 1e3)} self.assertEqual(expected_ping_message, sent_messages[-1]) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_subscriptions_raises_cancel_exception( self, _, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_subscriptions_logs_exception_details( self, sleep_mock, ws_connect_mock): sleep_mock.side_effect = asyncio.CancelledError ws_connect_mock.side_effect = Exception("TEST ERROR.") with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() self.ob_data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "topic": "trade", "params": { "symbol": self.ex_trading_pair, "binary": "false", "symbolName": self.ex_trading_pair }, "data": { "v": "564265886622695424", # "t": 1582001735462, "p": "9787.5", "q": "0.195009", "m": True } } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.ob_data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public trade updates from exchange" )) def test_listen_for_trades_successful(self): mock_queue = AsyncMock() trade_event = { "symbol": self.ex_trading_pair, "symbolName": self.ex_trading_pair, "topic": "trade", "params": { "realtimeInterval": "24h", "binary": "false" }, "data": [{ "v": "929681067596857345", "t": 1625562619577, "p": "34924.15", "q": "0.00027", "m": True }], "f": True, "sendTime": 1626249138535, "shared": False } mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(trade_event["data"][0]["t"], msg.trade_id) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_diffs_logs_exception(self): incomplete_resp = { # "symbol": self.ex_trading_pair, "symbolName": self.ex_trading_pair, "topic": "diffDepth", "params": { "realtimeInterval": "24h", "binary": "false" }, "data": [{ "e": 301, "s": self.ex_trading_pair, "t": 1565600357643, "v": "112801745_18", "b": [["11371.49", "0.0014"], ["11371.12", "0.2"], ["11369.97", "0.3523"], ["11369.96", "0.5"], ["11369.95", "0.0934"], ["11369.94", "1.6809"], ["11369.6", "0.0047"], ["11369.17", "0.3"], ["11369.16", "0.2"], ["11369.04", "1.3203"]], "a": [["11375.41", "0.0053"], ["11375.42", "0.0043"], ["11375.48", "0.0052"], ["11375.58", "0.0541"], ["11375.7", "0.0386"], ["11375.71", "2"], ["11377", "2.0691"], ["11377.01", "0.0167"], ["11377.12", "1.5"], ["11377.61", "0.3"]], "o": 0 }], "f": False, "sendTime": 1626253839401, "shared": False } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" )) def test_listen_for_order_book_diffs_successful(self): mock_queue = AsyncMock() diff_event = { "symbol": self.ex_trading_pair, "symbolName": self.ex_trading_pair, "topic": "diffDepth", "params": { "realtimeInterval": "24h", "binary": "false" }, "data": [{ "e": 301, "s": self.ex_trading_pair, "t": 1565600357643, "v": "112801745_18", "b": [["11371.49", "0.0014"], ["11371.12", "0.2"], ["11369.97", "0.3523"], ["11369.96", "0.5"], ["11369.95", "0.0934"], ["11369.94", "1.6809"], ["11369.6", "0.0047"], ["11369.17", "0.3"], ["11369.16", "0.2"], ["11369.04", "1.3203"]], "a": [["11375.41", "0.0053"], ["11375.42", "0.0043"], ["11375.48", "0.0052"], ["11375.58", "0.0541"], ["11375.7", "0.0386"], ["11375.71", "2"], ["11377", "2.0691"], ["11377.01", "0.0167"], ["11377.12", "1.5"], ["11377.61", "0.3"]], "o": 0 }], "f": False, "sendTime": 1626253839401, "shared": False } mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(diff_event["data"][0]["t"], msg.update_id) def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.ob_data_source._message_queue[ CONSTANTS.SNAPSHOT_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) @aioresponses() @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_order_book_snapshots_log_exception( self, mock_api, sleep_mock): mock_queue = AsyncMock() mock_queue.get.side_effect = ['ERROR', asyncio.CancelledError] self.ob_data_source._message_queue[ CONSTANTS.SNAPSHOT_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = [asyncio.CancelledError] url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) try: self.async_run_with_timeout( self.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" )) @aioresponses() @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_order_book_snapshots_successful_rest( self, mock_api, _): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.TimeoutError self.ob_data_source._message_queue[ CONSTANTS.SNAPSHOT_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() url = web_utils.rest_url(path_url=CONSTANTS.SNAPSHOT_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) snapshot_data = self._snapshot_response() mock_api.get(regex_url, body=json.dumps(snapshot_data)) self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(int(snapshot_data["result"]["time"]), msg.update_id) def test_listen_for_order_book_snapshots_successful_ws(self): mock_queue = AsyncMock() snapshot_event = { "symbol": self.ex_trading_pair, "symbolName": self.ex_trading_pair, "topic": "diffDepth", "params": { "realtimeInterval": "24h", "binary": "false" }, "data": [{ "e": 301, "s": self.ex_trading_pair, "t": 1565600357643, "v": "112801745_18", "b": [["11371.49", "0.0014"], ["11371.12", "0.2"], ["11369.97", "0.3523"], ["11369.96", "0.5"], ["11369.95", "0.0934"], ["11369.94", "1.6809"], ["11369.6", "0.0047"], ["11369.17", "0.3"], ["11369.16", "0.2"], ["11369.04", "1.3203"]], "a": [["11375.41", "0.0053"], ["11375.42", "0.0043"], ["11375.48", "0.0052"], ["11375.58", "0.0541"], ["11375.7", "0.0386"], ["11375.71", "2"], ["11377", "2.0691"], ["11377.01", "0.0167"], ["11377.12", "1.5"], ["11377.61", "0.3"]], "o": 0 }], "f": True, "sendTime": 1626253839401, "shared": False } mock_queue.get.side_effect = [snapshot_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.ob_data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get(), timeout=6) self.assertTrue(snapshot_event["data"][0]["t"], msg.update_id)
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.client_config_map = ClientConfigAdapter(ClientConfigMap()) self.exchange = MexcExchange( client_config_map=self.client_config_map, 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 AscendExUserStreamTrackerTests(TestCase): def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.mocking_assistant = NetworkMockingAssistant() self.listening_task = None self.api_factory = None self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.tracker = AscendExUserStreamTracker( ascend_ex_auth=AscendExAuth(api_key="testAPIKey", secret_key="testSecret"), api_factory=self.api_factory, throttler=self.throttler, ) 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 = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _accountgroup_response(self) -> Dict[str, Any]: message = {"data": {"accountGroup": 12345679}} return message def _authentication_response(self, authenticated: bool) -> Dict[str, Any]: request = { "op": "auth", "args": ["testAPIKey", "testExpires", "testSignature"] } message = { "success": authenticated, "ret_msg": "", "conn_id": "testConnectionID", "request": request } return message @aioresponses() @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_authenticates_and_subscribes_to_events( self, api_mock, ws_connect_mock): output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.tracker.data_source.listen_for_user_stream(output_queue)) # Add the account group response resp = self._accountgroup_response() api_mock.get(f"{CONSTANTS.REST_URL}/{CONSTANTS.INFO_PATH_URL}", body=json.dumps(resp)) # Create WS mock ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add the authentication response for the websocket resp = self._authentication_response(authenticated=True) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) # Add a dummy message for the websocket to read and include in the "messages" queue resp = {"data": "dummyMessage"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) ret = self.ev_loop.run_until_complete(output_queue.get()) self.assertEqual( { "success": True, "ret_msg": "", "conn_id": "testConnectionID", "request": { "op": "auth", "args": ["testAPIKey", "testExpires", "testSignature"] }, }, ret, ) ret = self.ev_loop.run_until_complete(output_queue.get()) self.assertEqual(resp, ret)
class BitmexPerpetualAPIOrderBookDataSourceUnitTests(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 = "ETH" cls.quote_asset = "USD" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}" cls.domain = "bitmex_perpetual_testnet" utils.TRADING_PAIR_SIZE_CURRENCY["ETHUSD"] = utils.TRADING_PAIR_SIZE( "USD", False, None) utils.TRADING_PAIR_INDICES["ETHUSD"] = utils.TRADING_PAIR_INDEX( 297, 0.05) def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.async_tasks: List[asyncio.Task] = [] self.data_source = BitmexPerpetualAPIOrderBookDataSource( trading_pairs=[self.trading_pair], domain=self.domain, ) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = { self.domain: bidict({self.ex_trading_pair: self.trading_pair}) } def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() for task in self.async_tasks: task.cancel() BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {} super().tearDown() def handle(self, record): self.log_records.append(record) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 60): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def resume_test_callback(self, *_, **__): self.resume_test_event.set() return None 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 _raise_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _orderbook_update_event(self): resp = { "table": "orderBookL2", "action": "insert", "data": [{ "symbol": "ETHUSD", "id": 3333377777, "size": 10, "side": "Sell" }], } return resp def _orderbook_trade_event(self): resp = { "table": "trade", "data": [{ "symbol": "ETHUSD", "side": "Sell", "price": 1000.0, "size": 10, "timestamp": "2020-02-11T9:30:02.123Z" }], } return resp @aioresponses() def test_get_last_traded_prices(self, mock_api): url = web_utils.rest_url(CONSTANTS.TICKER_PRICE_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: List[Dict[str, Any]] = [{ "symbol": "ETHUSD", "lastPrice": 100.0 }] mock_api.get(regex_url, body=json.dumps(mock_response)) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], domain=self.domain)) self.assertTrue(self.trading_pair in result) self.assertEqual(100.0, result[self.trading_pair]) def test_get_throttler_instance(self): self.assertTrue( isinstance(self.data_source._get_throttler_instance(), AsyncThrottler)) @aioresponses() def test_init_trading_pair_symbols_failure(self, mock_api): BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"])) map = self.async_run_with_timeout( self.data_source.trading_pair_symbol_map(domain=self.domain)) self.assertEqual(0, len(map)) @aioresponses() def test_init_trading_pair_symbols_successful(self, mock_api): url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: List[Dict[str, Any]] = [ { "symbol": "ETHUSD", "rootSymbol": "ETH", "quoteCurrency": "USD" }, ] mock_api.get(regex_url, status=200, body=json.dumps(mock_response)) self.async_run_with_timeout( self.data_source.init_trading_pair_symbols(domain=self.domain)) self.assertEqual(1, len(self.data_source._trading_pair_symbol_map)) @aioresponses() def test_trading_pair_symbol_map_dictionary_not_initialized( self, mock_api): BitmexPerpetualAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: List[Dict[str, Any]] = [ { "symbol": "ETHUSD", "rootSymbol": "ETH", "quoteCurrency": "USD" }, ] mock_api.get(regex_url, status=200, body=json.dumps(mock_response)) self.async_run_with_timeout( self.data_source.trading_pair_symbol_map(domain=self.domain)) self.assertEqual(1, len(self.data_source._trading_pair_symbol_map)) def test_trading_pair_symbol_map_dictionary_initialized(self): result = self.async_run_with_timeout( self.data_source.trading_pair_symbol_map(domain=self.domain)) self.assertEqual(1, len(result)) def test_convert_from_exchange_trading_pair_not_found(self): unknown_pair = "UNKNOWN-PAIR" with self.assertRaisesRegex( ValueError, f"There is no symbol mapping for exchange trading pair {unknown_pair}" ): self.async_run_with_timeout( self.data_source.convert_from_exchange_trading_pair( unknown_pair, domain=self.domain)) def test_convert_from_exchange_trading_pair_successful(self): result = self.async_run_with_timeout( self.data_source.convert_from_exchange_trading_pair( self.ex_trading_pair, domain=self.domain)) self.assertEqual(result, self.trading_pair) def test_convert_to_exchange_trading_pair_not_found(self): unknown_pair = "UNKNOWN-PAIR" with self.assertRaisesRegex( ValueError, f"There is no symbol mapping for trading pair {unknown_pair}"): self.async_run_with_timeout( self.data_source.convert_to_exchange_trading_pair( unknown_pair, domain=self.domain)) def test_convert_to_exchange_trading_pair_successful(self): result = self.async_run_with_timeout( self.data_source.convert_to_exchange_trading_pair( self.trading_pair, domain=self.domain)) self.assertEqual(result, self.ex_trading_pair) @aioresponses() def test_get_snapshot_exception_raised(self, mock_api): url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=json.dumps(["ERROR"])) with self.assertRaises(IOError) as context: self.async_run_with_timeout( self.data_source.get_snapshot(trading_pair=self.trading_pair, domain=self.domain)) self.assertEqual( str(context.exception), "Error executing request GET /orderBook/L2. HTTP status is 400. Error: [\"ERROR\"]" ) @aioresponses() def test_get_snapshot_successful(self, mock_api): url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = [{ 'symbol': 'ETHUSD', 'side': 'Sell', 'size': 348, 'price': 3127.4 }] mock_api.get(regex_url, status=200, body=json.dumps(mock_response)) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.get_snapshot(trading_pair=self.trading_pair, domain=self.domain)) self.assertEqual(mock_response, result) @aioresponses() def test_get_new_order_book(self, mock_api): url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = [{ 'symbol': 'ETHUSD', 'side': 'Sell', 'size': 348, 'price': 3127.4, 'id': 2543 }, { 'symbol': 'ETHUSD', 'side': 'Buy', 'size': 100, 'price': 3000.1, 'id': 2555 }] mock_api.get(regex_url, status=200, body=json.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_new_order_book( trading_pair=self.trading_pair)) self.assertIsInstance(result, OrderBook) @aioresponses() def test_get_funding_info_from_exchange_error_response(self, mock_api): url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400) try: self.async_run_with_timeout( self.data_source._get_funding_info_from_exchange( self.trading_pair)) except Exception: pass self._is_logged( "ERROR", f"Unable to fetch FundingInfo for {self.trading_pair}. Error: None" ) @aioresponses() def test_get_funding_info_from_exchange_successful(self, mock_api): url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = [{ "lastPrice": 1000.0, "fairPrice": 999.1, "fundingRate": 0.1, "fundingTimestamp": "2022-02-11T05:30:30.000Z" }] mock_api.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source._get_funding_info_from_exchange( self.trading_pair)) self.assertIsInstance(result, FundingInfo) self.assertEqual(result.trading_pair, self.trading_pair) self.assertEqual(result.rate, Decimal(mock_response[0]["fundingRate"])) @aioresponses() def test_get_funding_info(self, mock_api): self.assertNotIn(self.trading_pair, self.data_source._funding_info) url = web_utils.rest_url(CONSTANTS.EXCHANGE_INFO_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = [{ "lastPrice": 1000.0, "fairPrice": 999.1, "fundingRate": 0.1, "fundingTimestamp": "2022-02-11T05:30:30.000Z" }] mock_api.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_funding_info(trading_pair=self.trading_pair)) self.assertIsInstance(result, FundingInfo) self.assertEqual(result.trading_pair, self.trading_pair) self.assertEqual(result.rate, Decimal(mock_response[0]["fundingRate"])) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) def test_listen_for_subscriptions_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_subscriptions()) self.async_run_with_timeout(self.listening_task) self.assertEqual(msg_queue.qsize(), 0) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_successful(self, mock_ws): msg_queue_diffs: asyncio.Queue = asyncio.Queue() msg_queue_trades: 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_aiohttp_message( mock_ws.return_value, json.dumps(self._orderbook_update_event())) self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps(self._orderbook_trade_event())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.listening_task_diffs = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue_diffs)) self.listening_task_trades = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue_trades)) try: result: OrderBookMessage = self.async_run_with_timeout( msg_queue_diffs.get()) except Exception as e: print(e) self.assertIsInstance(result, OrderBookMessage) self.assertEqual(OrderBookMessageType.DIFF, result.type) self.assertTrue(result.has_update_id) self.assertEqual(self.trading_pair, result.content["trading_pair"]) self.assertEqual(0, len(result.content["bids"])) self.assertEqual(1, len(result.content["asks"])) result: OrderBookMessage = self.async_run_with_timeout( msg_queue_trades.get()) self.assertIsInstance(result, OrderBookMessage) self.assertEqual(OrderBookMessageType.TRADE, result.type) self.assertTrue(result.has_trade_id) self.assertEqual(self.trading_pair, result.content["trading_pair"]) self.listening_task.cancel() @aioresponses() def test_listen_for_order_book_snapshots_cancelled_error_raised( self, mock_api): url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) self.assertEqual(0, msg_queue.qsize()) @aioresponses() def test_listen_for_order_book_snapshots_logs_exception_error_with_response( self, mock_api): url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "m": 1, "i": 2, } mock_api.get(regex_url, body=json.dumps(mock_response), callback=self.resume_test_callback) msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred fetching orderbook snapshots. Retrying in 5 seconds..." )) @aioresponses() def test_listen_for_order_book_snapshots_successful(self, mock_api): url = web_utils.rest_url(CONSTANTS.SNAPSHOT_REST_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = [{ 'symbol': 'ETHUSD', 'side': 'Sell', 'size': 348, 'price': 3127.4, 'id': 33337777 }] mock_api.get(regex_url, status=200, body=json.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) result = self.async_run_with_timeout(msg_queue.get()) self.assertIsInstance(result, OrderBookMessage) self.assertEqual(OrderBookMessageType.SNAPSHOT, result.type) self.assertTrue(result.has_update_id) self.assertEqual(self.trading_pair, result.content["trading_pair"])
class NdaxWebSocketAdaptorTests(TestCase): def setUp(self) -> None: super().setUp() self.mocking_assistant = NetworkMockingAssistant() 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 @patch("aiohttp.ClientSession.ws_connect") def test_sending_messages_increment_message_number(self, mock_ws): sent_messages = [] throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.send_json.side_effect = lambda sent_message: sent_messages.append( sent_message) adaptor = NdaxWebSocketAdaptor(throttler, websocket=mock_ws.return_value) payload = {} self.async_run_with_timeout( adaptor.send_request(endpoint_name=CONSTANTS.WS_PING_REQUEST, payload=payload, limit_id=CONSTANTS.WS_PING_ID)) self.async_run_with_timeout( adaptor.send_request(endpoint_name=CONSTANTS.WS_PING_REQUEST, payload=payload, limit_id=CONSTANTS.WS_PING_ID)) self.async_run_with_timeout( adaptor.send_request(endpoint_name=CONSTANTS.WS_ORDER_BOOK_CHANNEL, payload=payload)) self.assertEqual(3, len(sent_messages)) message = sent_messages[0] self.assertEqual(1, message.get('i')) message = sent_messages[1] self.assertEqual(2, message.get('i')) message = sent_messages[2] self.assertEqual(3, message.get('i')) @patch("aiohttp.ClientSession.ws_connect") def test_request_message_structure(self, mock_ws): sent_messages = [] throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.send_json.side_effect = lambda sent_message: sent_messages.append( sent_message) adaptor = NdaxWebSocketAdaptor(throttler, websocket=mock_ws.return_value) payload = {"TestElement1": "Value1", "TestElement2": "Value2"} self.async_run_with_timeout( adaptor.send_request(endpoint_name=CONSTANTS.WS_PING_REQUEST, payload=payload, limit_id=CONSTANTS.WS_PING_ID)) self.assertEqual(1, len(sent_messages)) message = sent_messages[0] self.assertEqual(0, message.get('m')) self.assertEqual(1, message.get('i')) self.assertEqual(CONSTANTS.WS_PING_REQUEST, message.get('n')) message_payload = json.loads(message.get('o')) self.assertEqual(payload, message_payload) @patch("aiohttp.ClientSession.ws_connect") def test_receive_message(self, mock_ws): throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, 'test message') adaptor = NdaxWebSocketAdaptor(throttler, websocket=mock_ws.return_value) received_message = self.async_run_with_timeout(adaptor.receive()) self.assertEqual('test message', received_message.data) @patch("aiohttp.ClientSession.ws_connect") def test_close(self, mock_ws): throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() adaptor = NdaxWebSocketAdaptor(throttler, websocket=mock_ws.return_value) self.async_run_with_timeout(adaptor.close()) self.assertEquals(1, mock_ws.return_value.close.await_count) @patch("aiohttp.ClientSession.ws_connect") def test_get_payload_from_raw_received_message(self, mock_ws): throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() payload = {"Key1": True, "Key2": "Value2"} message = {"m": 1, "i": 1, "n": "Endpoint", "o": json.dumps(payload)} raw_message = json.dumps(message) adaptor = NdaxWebSocketAdaptor(throttler, websocket=mock_ws.return_value) extracted_payload = adaptor.payload_from_raw_message( raw_message=raw_message) self.assertEqual(payload, extracted_payload) @patch("aiohttp.ClientSession.ws_connect") def test_get_endpoint_from_raw_received_message(self, mock_ws): throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() payload = {"Key1": True, "Key2": "Value2"} message = {"m": 1, "i": 1, "n": "Endpoint", "o": json.dumps(payload)} raw_message = json.dumps(message) adaptor = NdaxWebSocketAdaptor(throttler, websocket=mock_ws.return_value) extracted_endpoint = adaptor.endpoint_from_raw_message( raw_message=raw_message) self.assertEqual("Endpoint", extracted_endpoint)
class OkxUserStreamDataSourceUnitTests(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.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.mock_time_provider = MagicMock() self.mock_time_provider.time.return_value = 1000 self.time_synchronizer = MagicMock() self.time_synchronizer.time.return_value = 1640001112.223 self.auth = OkxAuth(api_key="TEST_API_KEY", secret_key="TEST_SECRET", passphrase="TEST_PASSPHRASE", time_provider=self.time_synchronizer) client_config_map = ClientConfigAdapter(ClientConfigMap()) self.connector = OkxExchange( client_config_map=client_config_map, okx_api_key="", okx_secret_key="", okx_passphrase="", trading_pairs=[self.trading_pair], trading_required=False, ) self.connector._web_assistants_factory._auth = self.auth self.data_source = OkxAPIUserStreamDataSource( auth=self.auth, connector=self.connector, api_factory=self.connector._web_assistants_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.resume_test_event = asyncio.Event() 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 _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _create_return_value_and_unlock_test_with_event(self, value): self.resume_test_event.set() return value def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_subscribes_to_orders_and_balances_events( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) successful_login_response = {"event": "login", "code": "0", "msg": ""} result_subscribe_orders = { "event": "subscribe", "arg": { "channel": "account" } } result_subscribe_account = { "event": "subscribe", "arg": { "channel": "orders", "instType": "SPOT", } } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(successful_login_response)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_orders)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_account)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(3, len(sent_messages)) expected_login = { "op": "login", "args": [{ "apiKey": self.auth.api_key, "passphrase": self.auth.passphrase, 'timestamp': '1640001112', 'sign': 'wEhbGLkjM+fzAclpjd67vGUzbRpxPe4AlLyh6/wVwL4=', }] } self.assertEqual(expected_login, sent_messages[0]) expected_account_subscription = { "op": "subscribe", "args": [{ "channel": "account" }] } self.assertEqual(expected_account_subscription, sent_messages[1]) expected_orders_subscription = { "op": "subscribe", "args": [{ "channel": "orders", "instType": "SPOT", }] } self.assertEqual(expected_orders_subscription, sent_messages[2]) self.assertTrue( self._is_logged( "INFO", "Subscribed to private account and orders channels...")) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_authentication_failure( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) login_response = { "event": "error", "code": "60009", "msg": "Login failed." } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(login_response)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_empty_payload( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() successful_login_response = {"event": "login", "code": "0", "msg": ""} self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps(successful_login_response)) self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, "") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_connection_failed(self, mock_ws): mock_ws.side_effect = lambda *arg, **kwars: self._create_exception_and_unlock_test_with_event( Exception("TEST ERROR.")) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_sends_ping_message_before_ping_interval_finishes( self, ws_connect_mock): successful_login_response = {"event": "login", "code": "0", "msg": ""} ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = [ WSMessage(type=WSMsgType.TEXT, data=json.dumps(successful_login_response), extra=None), asyncio.TimeoutError("Test timeiout"), asyncio.CancelledError ] msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass sent_messages = self.mocking_assistant.text_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) expected_ping_message = "ping" self.assertEqual(expected_ping_message, sent_messages[0])
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.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() self.client_config_map = ClientConfigAdapter(ClientConfigMap()) self.time_synchronizer = MagicMock() self.time_synchronizer.time.return_value = 1640001112.223 self.auth = BitmartAuth(api_key="test_api_key", secret_key="test_secret_key", memo="test_memo", time_provider=self.time_synchronizer) self.connector = BitmartExchange( client_config_map=self.client_config_map, bitmart_api_key="test_api_key", bitmart_secret_key="test_secret_key", bitmart_memo="test_memo", trading_pairs=[self.trading_pair], trading_required=False, ) self.connector._web_assistants_factory._auth = self.auth self.data_source = BitmartAPIUserStreamDataSource( auth=self.auth, trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.connector._set_trading_pair_symbol_map( bidict({self.ex_trading_pair: self.trading_pair})) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def 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 _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("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_subscribes_to_orders_events( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) successful_login_response = {"event": "login"} result_subscribe_orders = { "event": "subscribe", "topic": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME, } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(successful_login_response)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_orders)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_messages)) expected_login = { "op": "login", "args": [ "test_api_key", str(int(self.time_synchronizer.time() * 1e3)), "f0f176c799346a7730c9c237a09d14742971f3ab59848dde75ef1ac95b04c4e5" ] # noqa: mock } self.assertEqual(expected_login, sent_messages[0]) expected_orders_subscription = { "op": "subscribe", "args": [ f"{CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME}:{self.ex_trading_pair}" ] } self.assertEqual(expected_orders_subscription, sent_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to private account and orders channels...")) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_logs_error_when_login_fails( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) erroneous_login_response = {"event": "login", "errorCode": "4001"} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(erroneous_login_response)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( "ERROR", "Error authenticating the private websocket connection")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_invalid_payload( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() successful_login_response = {"event": "login"} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(successful_login_response)) event_without_data = { "table": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(event_without_data)) event_without_table = { "data": [{ "symbol": self.ex_trading_pair, "side": "buy", "type": "market", "notional": "", "size": "1.0000000000", "ms_t": "1609926028000", "price": "46100.0000000000", "filled_notional": "46100.0000000000", "filled_size": "1.0000000000", "margin_trading": "0", "state": "4", "order_id": "2147857398", "order_type": "0", "last_fill_time": "1609926039226", "last_fill_price": "46100.00000", "last_fill_count": "1.00000", "exec_type": "M", "detail_id": "256348632", "client_order_id": "order4872191" }], } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(event_without_table)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep" ) def test_listen_for_user_stream_connection_failed(self, sleep_mock, mock_ws): mock_ws.side_effect = Exception("TEST ERROR") sleep_mock.side_effect = asyncio.CancelledError msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) 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(messages)) self.ev_loop.run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_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.receive.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(messages)) self.ev_loop.run_until_complete(self.listening_task) def test_subscribe_channels_raises_cancel_exception(self): ws_assistant = AsyncMock() ws_assistant.send.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(ws_assistant)) self.ev_loop.run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep" ) def test_listening_process_logs_exception_during_events_subscription( self, sleep_mock, mock_ws): self.connector._set_trading_pair_symbol_map({}) messages = asyncio.Queue() sleep_mock.side_effect = asyncio.CancelledError mock_ws.return_value = self.mocking_assistant.create_websocket_mock() # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, json.dumps({"event": "login"})) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(messages)) try: self.async_run_with_timeout(self.listening_task, timeout=3) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_processes_order_event(self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() successful_login_response = {"event": "login"} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(successful_login_response)) order_event = { "data": [{ "symbol": self.ex_trading_pair, "side": "buy", "type": "market", "notional": "", "size": "1.0000000000", "ms_t": "1609926028000", "price": "46100.0000000000", "filled_notional": "46100.0000000000", "filled_size": "1.0000000000", "margin_trading": "0", "state": "4", "order_id": "2147857398", "order_type": "0", "last_fill_time": "1609926039226", "last_fill_price": "46100.00000", "last_fill_count": "1.00000", "exec_type": "M", "detail_id": "256348632", "client_order_id": "order4872191" }], "table": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(order_event)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(1, msg_queue.qsize()) order_event_message = msg_queue.get_nowait() self.assertEqual(order_event, order_event_message) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_processes_compressed_order_event( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() successful_login_response = {"event": "login"} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(successful_login_response)) order_event = { "data": [{ "symbol": self.ex_trading_pair, "side": "buy", "type": "market", "notional": "", "size": "1.0000000000", "ms_t": "1609926028000", "price": "46100.0000000000", "filled_notional": "46100.0000000000", "filled_size": "1.0000000000", "margin_trading": "0", "state": "4", "order_id": "2147857398", "order_type": "0", "last_fill_time": "1609926039226", "last_fill_price": "46100.00000", "last_fill_count": "1.00000", "exec_type": "M", "detail_id": "256348632", "client_order_id": "order4872191" }], "table": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=bitmart_utils.compress_ws_message(json.dumps(order_event)), message_type=WSMsgType.BINARY) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(1, msg_queue.qsize()) order_event_message = msg_queue.get_nowait() self.assertEqual(order_event, order_event_message) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_logs_details_for_order_event_with_errors( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() successful_login_response = {"event": "login"} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(successful_login_response)) order_event = { "errorCode": "4001", "errorMessage": "Error", "data": [{ "symbol": self.ex_trading_pair, "side": "buy", "type": "market", "notional": "", "size": "1.0000000000", "ms_t": "1609926028000", "price": "46100.0000000000", "filled_notional": "46100.0000000000", "filled_size": "1.0000000000", "margin_trading": "0", "state": "4", "order_id": "2147857398", "order_type": "0", "last_fill_time": "1609926039226", "last_fill_price": "46100.00000", "last_fill_count": "1.00000", "exec_type": "M", "detail_id": "256348632", "client_order_id": "order4872191" }], "table": CONSTANTS.PRIVATE_ORDER_PROGRESS_CHANNEL_NAME } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(order_event)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_logs_details_for_invalid_event_message( self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() successful_login_response = {"event": "login"} self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message=json.dumps(successful_login_response)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=mock_ws.return_value, message="invalid message content") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "WARNING", "Invalid event message received through the order book data source connection (invalid message content)" ))
class CoinbaseProAPIOrderBookDataSourceTests(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.mocking_assistant = NetworkMockingAssistant() auth = CoinbaseProAuth(api_key="SomeAPIKey", secret_key="SomeSecretKey", passphrase="SomePassPhrase") web_assistants_factory = build_coinbase_pro_web_assistant_factory(auth) self.data_source = CoinbaseProAPIOrderBookDataSource( trading_pairs=[self.trading_pair], web_assistants_factory=web_assistants_factory ) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.log_records = [] self.async_tasks: List[asyncio.Task] = [] def tearDown(self) -> None: for task in self.async_tasks: task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any(record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret @staticmethod def get_products_ticker_response_mock(price: float) -> Dict: products_ticker_mock = { "trade_id": 86326522, "price": str(price), "size": "0.00698254", "time": "2020-03-20T00:22:57.833897Z", "bid": "6265.15", "ask": "6267.71", "volume": "53602.03940154" } return products_ticker_mock def get_products_response_mock(self, other_pair: str) -> List: products_mock = [ { "id": self.trading_pair, "base_currency": self.base_asset, "quote_currency": self.quote_asset, "base_min_size": "0.00100000", "base_max_size": "280.00000000", "quote_increment": "0.01000000", "base_increment": "0.00000001", "display_name": f"{self.base_asset}/{self.quote_asset}", "min_market_funds": "10", "max_market_funds": "1000000", "margin_enabled": False, "post_only": False, "limit_only": False, "cancel_only": False, "status": "online", "status_message": "", "auction_mode": True, }, { "id": other_pair, "base_currency": other_pair.split("-")[0], "quote_currency": other_pair.split("-")[1], "base_min_size": "0.00100000", "base_max_size": "280.00000000", "quote_increment": "0.01000000", "base_increment": "0.00000001", "display_name": other_pair.replace("-", "/"), "min_market_funds": "10", "max_market_funds": "1000000", "margin_enabled": False, "post_only": False, "limit_only": False, "cancel_only": False, "status": "online", "status_message": "", "auction_mode": True, } ] return products_mock @staticmethod def get_products_book_response_mock( bids: Optional[List[List[str]]] = None, asks: Optional[List[List[str]]] = None ) -> Dict: bids = bids or [["1", "2", "3"]] asks = asks or [["4", "5", "6"]] products_book_mock = { "sequence": 13051505638, "bids": bids, "asks": asks, } return products_book_mock def get_ws_open_message_mock(self) -> Dict: message = { "type": "open", "time": "2014-11-07T08:19:27.028459Z", "product_id": self.trading_pair, "sequence": 10, "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", "price": "200.2", "remaining_size": "1.00", "side": "sell" } return message def get_ws_match_message_mock(self) -> Dict: message = { "type": "match", "trade_id": 10, "sequence": 50, "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", "time": "2014-11-07T08:19:27.028459Z", "product_id": self.trading_pair, "size": "5.23512", "price": "400.23", "side": "sell" } return message def get_ws_change_message_mock(self) -> Dict: message = { "type": "change", "time": "2014-11-07T08:19:27.028459Z", "sequence": 80, "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", "product_id": self.trading_pair, "new_size": "5.23512", "old_size": "12.234412", "price": "400.23", "side": "sell" } return message def get_ws_done_message_mock(self) -> Dict: message = { "type": "done", "time": "2014-11-07T08:19:27.028459Z", "product_id": self.trading_pair, "sequence": 10, "price": "200.2", "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", "reason": "filled", "side": "sell", "remaining_size": "0" } return message @aioresponses() def test_get_last_traded_prices(self, mock_api): alt_pair = "BTC-USDT" url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/ticker" alt_url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{alt_pair}/ticker" price = 10.0 alt_price = 15.0 resp = self.get_products_ticker_response_mock(price=price) alt_resp = self.get_products_ticker_response_mock(price=alt_price) mock_api.get(url, body=json.dumps(resp)) mock_api.get(alt_url, body=json.dumps(alt_resp)) trading_pairs = [self.trading_pair, alt_pair] ret = self.async_run_with_timeout( coroutine=CoinbaseProAPIOrderBookDataSource.get_last_traded_prices(trading_pairs) ) self.assertEqual(ret[self.trading_pair], Decimal(resp["price"])) self.assertEqual(ret[alt_pair], Decimal(alt_resp["price"])) @aioresponses() def test_fetch_trading_pairs(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}" alt_pair = "BTC-USDT" resp = self.get_products_response_mock(alt_pair) mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout(coroutine=CoinbaseProAPIOrderBookDataSource.fetch_trading_pairs()) self.assertIn(self.trading_pair, ret) self.assertIn(alt_pair, ret) @aioresponses() def test_get_snapshot(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3" resp = self.get_products_book_response_mock() mock_api.get(url, body=json.dumps(resp)) rest_assistant = self.ev_loop.run_until_complete( build_coinbase_pro_web_assistant_factory().get_rest_assistant() ) ret = self.async_run_with_timeout( coroutine=CoinbaseProAPIOrderBookDataSource.get_snapshot(rest_assistant, self.trading_pair) ) self.assertEqual(resp, ret) # shallow comparison ok @aioresponses() def test_get_snapshot_raises_on_status_code(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3" resp = self.get_products_book_response_mock() mock_api.get(url, body=json.dumps(resp), status=401) rest_assistant = self.ev_loop.run_until_complete( build_coinbase_pro_web_assistant_factory().get_rest_assistant() ) with self.assertRaises(IOError): self.async_run_with_timeout( coroutine=CoinbaseProAPIOrderBookDataSource.get_snapshot(rest_assistant, self.trading_pair) ) @aioresponses() def test_get_new_order_book(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3" resp = self.get_products_book_response_mock(bids=[["1", "2", "3"]], asks=[["4", "5", "6"]]) mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout(self.data_source.get_new_order_book(self.trading_pair)) self.assertIsInstance(ret, OrderBook) bid_entries = list(ret.bid_entries()) ask_entries = list(ret.ask_entries()) self.assertEqual(1, len(bid_entries)) self.assertEqual(1, len(ask_entries)) bid_entry = bid_entries[0] ask_entry = ask_entries[0] self.assertEqual(1, bid_entry.price) self.assertEqual(4, ask_entry.price) @aioresponses() def test_get_tracking_pairs(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3" resp = self.get_products_book_response_mock(bids=[["1", "2", "3"]]) mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout(self.data_source.get_tracking_pairs()) self.assertEqual(1, len(ret)) tracker_entry = ret[self.trading_pair] self.assertIsInstance(tracker_entry, CoinbaseProOrderBookTrackerEntry) self.assertEqual(1, list(tracker_entry.order_book.bid_entries())[0].price) @aioresponses() def test_get_tracking_pairs_logs_io_error(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3" mock_api.get(url, exception=IOError) ret = self.async_run_with_timeout(self.data_source.get_tracking_pairs()) self.assertEqual(0, len(ret)) self.assertTrue(self._is_logged( log_level="NETWORK", message=f"Error getting snapshot for {self.trading_pair}.") ) @aioresponses() def test_get_tracking_pairs_logs_other_exceptions(self, mock_api): url = f"{CONSTANTS.REST_URL}{CONSTANTS.PRODUCTS_PATH_URL}/{self.trading_pair}/book?level=3" mock_api.get(url, exception=RuntimeError) ret = self.async_run_with_timeout(self.data_source.get_tracking_pairs()) self.assertEqual(0, len(ret)) self.assertTrue(self._is_logged( log_level="ERROR", message=f"Error initializing order book for {self.trading_pair}. ") ) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_processes_open_message(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = self.get_ws_open_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) ob_message = output_queue.get_nowait() self.assertEqual(resp, ob_message.content) # shallow comparison is ok @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_processes_match_message(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = self.get_ws_match_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) ob_message = output_queue.get_nowait() self.assertEqual(resp, ob_message.content) # shallow comparison is ok @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_processes_change_message(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = self.get_ws_change_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) ob_message = output_queue.get_nowait() self.assertEqual(resp, ob_message.content) # shallow comparison is ok @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_processes_done_message(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = self.get_ws_done_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) ob_message = output_queue.get_nowait() self.assertEqual(resp, ob_message.content) # shallow comparison is ok @patch( "hummingbot.connector.exchange.coinbase_pro" ".coinbase_pro_api_order_book_data_source.CoinbaseProAPIOrderBookDataSource._sleep" ) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_raises_on_no_type(self, ws_connect_mock, _): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = {} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertTrue( self._is_logged(log_level="NETWORK", message="Unexpected error with WebSocket connection.") ) self.assertTrue(output_queue.empty()) @patch( "hummingbot.connector.exchange.coinbase_pro" ".coinbase_pro_api_order_book_data_source.CoinbaseProAPIOrderBookDataSource._sleep" ) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_raises_on_error_msg(self, ws_connect_mock, _): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = {"type": "error", "message": "some error"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertTrue( self._is_logged(log_level="NETWORK", message="Unexpected error with WebSocket connection.") ) self.assertTrue(output_queue.empty()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_ignores_irrelevant_messages(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"type": "received"}) ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"type": "activate"}) ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"type": "subscriptions"}) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertTrue(output_queue.empty()) @patch( "hummingbot.connector.exchange.coinbase_pro" ".coinbase_pro_api_order_book_data_source.CoinbaseProAPIOrderBookDataSource._sleep" ) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_raises_on_unrecognized_message(self, ws_connect_mock, _): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() resp = {"type": "some-new-message-type"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp) ) output_queue = asyncio.Queue() t = self.ev_loop.create_task(self.data_source.listen_for_order_book_diffs(self.ev_loop, output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) self.assertTrue( self._is_logged(log_level="NETWORK", message="Unexpected error with WebSocket connection.") ) self.assertTrue(output_queue.empty())
class MexcAPIUserStreamDataSourceTests(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 self.ev_loop = asyncio.get_event_loop() throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = MexcAuth(api_key=self.api_key, secret_key=self.secret) self.data_source = MexcAPIUserStreamDataSource(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 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 _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("aiohttp.ClientSession.ws_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( ) self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, ujson.dumps({'channel': 'push.personal.order'})) first_received_message = self.async_run_with_timeout(messages.get()) self.assertEqual({'channel': 'push.personal.order'}, first_received_message) @patch("aiohttp.ClientSession.ws_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(messages)) self.async_run_with_timeout(self.listening_task)
class AscendExAPIOrderBookDataSourceTests(TestCase): # logging.Level required to receive logs from the data source logger level = 0 def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.base_asset = "BTC" self.quote_asset = "USDT" self.trading_pair = f"{self.base_asset}-{self.quote_asset}" self.ex_trading_pair = f"{self.base_asset}/{self.quote_asset}" self.log_records = [] self.listening_task = None self.async_task: Optional[asyncio.Task] = None self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.api_factory = build_api_factory(throttler=self.throttler) self.data_source = AscendExAPIOrderBookDataSource( api_factory=self.api_factory, throttler=self.throttler, trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() AscendExAPIOrderBookDataSource._trading_pair_symbol_map = bidict( {self.ex_trading_pair: f"{self.base_asset}-{self.quote_asset}"}) def tearDown(self) -> None: self.async_task and self.async_task.cancel() self.listening_task and self.listening_task.cancel() AscendExAPIOrderBookDataSource._trading_pair_symbol_map = None 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 _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception 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 @aioresponses() def test_fetch_trading_pairs(self, api_mock): AscendExAPIOrderBookDataSource._trading_pair_symbol_map = None mock_response = { "data": [ { "symbol": self.ex_trading_pair, "baseAsset": self.base_asset, "quoteAsset": self.quote_asset, }, { "symbol": "ETH/USDT", "baseAsset": "ETH", "quoteAsset": "USDT", }, { "symbol": "DOGE/USDT", "baseAsset": "DOGE", "quoteAsset": "USDT", }, ], } url = f"{CONSTANTS.REST_URL}/{CONSTANTS.PRODUCTS_PATH_URL}" api_mock.get(url, body=json.dumps(mock_response)) trading_pairs = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(3, len(trading_pairs)) self.assertEqual("ETH-USDT", trading_pairs[1]) @aioresponses() def test_get_last_traded_prices_requests_rest_api_price_when_subscription_price_not_available( self, api_mock): mock_response = { "code": 0, "data": { "m": "trades", "symbol": "BTC/USDT", "data": [ { "seqnum": 144115191800016553, "p": "0.06762", "q": "400", "ts": 1573165890854, "bm": False }, { "seqnum": 144115191800070421, "p": "0.06797", "q": "341", "ts": 1573166037845, "bm": True }, ], }, } self.data_source._trading_pairs = ["BTC-USDT"] url = re.escape( f"{CONSTANTS.REST_URL}/{CONSTANTS.TRADES_PATH_URL}?symbol=") regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) results = self.ev_loop.run_until_complete( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair], api_factory=self.data_source._api_factory, throttler=self.throttler)) self.assertEqual(results[self.trading_pair], float(mock_response["data"]["data"][1]["p"])) @aioresponses() def test_get_order_book_http_error_raises_exception(self, api_mock): mock_response = "ERROR WITH REQUEST" url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}" regex_url = re.compile(f"^{url}") api_mock.get(regex_url, status=400, body=mock_response) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_order_book_data( trading_pair=self.trading_pair, throttler=self.throttler)) @aioresponses() def test_get_order_book_resp_code_erro_raises_exception(self, api_mock): mock_response = { "code": 100001, "reason": "INVALID_HTTP_INPUT", "message": "Http request is invalid" } url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}" regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_order_book_data( trading_pair=self.trading_pair, throttler=self.throttler)) @aioresponses() def test_get_order_book_data_successful(self, api_mock): mock_response = { "code": 0, "data": { "m": "depth-snapshot", "symbol": self.ex_trading_pair, "data": { "seqnum": 5068757, "ts": 1573165838976, "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]], "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]], }, }, } url = f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}" regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_order_book_data( trading_pair=self.trading_pair, throttler=self.throttler)) self.assertTrue(result.get("symbol") == self.ex_trading_pair) @aioresponses() def test_get_new_order_book(self, api_mock): mock_response = { "code": 0, "data": { "m": "depth-snapshot", "symbol": "BTC/USDT", "data": { "seqnum": 5068757, "ts": 1573165838976, "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]], "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]], }, }, } self.data_source._trading_pairs = ["BTC-USDT"] # path_url = ascend_ex_utils.rest_api_path_for_endpoint(CONSTANTS.ORDER_BOOK_ENDPOINT, self.trading_pair) url = re.escape( f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}?symbol=") regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) self.listening_task = self.ev_loop.create_task( self.data_source.get_new_order_book(self.trading_pair)) order_book = self.ev_loop.run_until_complete(self.listening_task) bids = list(order_book.bid_entries()) asks = list(order_book.ask_entries()) self.assertEqual(2, len(bids)) self.assertEqual(0.06703, round(bids[0].price, 5)) self.assertEqual(13500, round(bids[0].amount, 1)) self.assertEqual(1573165838976, bids[0].update_id) self.assertEqual(2, len(asks)) self.assertEqual(0.06848, round(asks[0].price, 5)) self.assertEqual(4084.2, round(asks[0].amount, 1)) self.assertEqual(1573165838976, asks[0].update_id) @patch("aiohttp.client.ClientSession.ws_connect") def test_subscribe_to_order_book_streams_raises_exception( self, ws_connect_mock): ws_connect_mock.side_effect = Exception("TEST ERROR") with self.assertRaisesRegex(Exception, "TEST ERROR"): self.async_run_with_timeout( self.data_source._subscribe_to_order_book_streams()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) @patch("aiohttp.client.ClientSession.ws_connect") def test_subscribe_to_order_book_streams_raises_cancel_exception( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source._subscribe_to_order_book_streams()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_subscribe_to_order_book_streams_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.async_run_with_timeout( self.data_source._subscribe_to_order_book_streams()) self.assertTrue( self._is_logged( "INFO", f"Subscribed to ['{self.trading_pair}'] orderbook trading and delta streams..." )) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) stream_topics = [payload["ch"] for payload in sent_messages] self.assertEqual(2, len(stream_topics)) self.assertTrue( f"{self.data_source.DIFF_TOPIC_ID}:{self.ex_trading_pair}" in stream_topics) self.assertTrue( f"{self.data_source.TRADE_TOPIC_ID}:{self.ex_trading_pair}" in stream_topics) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_exception_raised_cancelled_when_connecting( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_exception_raised_cancelled_when_subscribing( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.send_json.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_exception_raised_cancelled_when_listening( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscription_logs_exception(self, ws_connect_mock, sleep_mock): ws_connect_mock.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_enqueues_diff_and_trade_messages( self, ws_connect_mock): diffs_queue = self.data_source._message_queue[ self.data_source.DIFF_TOPIC_ID] trade_queue = self.data_source._message_queue[ self.data_source.TRADE_TOPIC_ID] ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add diff event message be processed diff_response = { "m": "depth", "symbol": self.ex_trading_pair, "data": { "ts": 1573069021376, "seqnum": 2097965, "asks": [["0.06844", "10760"]], "bids": [["0.06777", "562.4"], ["0.05", "221760.6"]], }, } # Add trade event message be processed trade_response = { "m": "trades", "symbol": "BTC/USDT", "data": [ { "seqnum": 144115191800016553, "p": "0.06762", "q": "400", "ts": 1573165890854, "bm": False }, { "seqnum": 144115191800070421, "p": "0.06797", "q": "341", "ts": 1573166037845, "bm": True }, ], } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(trade_response)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(1, diffs_queue.qsize()) self.assertEqual(1, trade_queue.qsize()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diff_raises_cancel_exceptions( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop, output=queue)) with self.assertRaises(asyncio.CancelledError): self.listening_task.cancel() self.ev_loop.run_until_complete(self.listening_task) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_handle_ping_message( self, ws_connect_mock): # In AscendEx Ping message is sent as a aiohttp.WSMsgType.TEXT message mock_response = {"m": "ping", "hp": 3} ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(mock_response), message_type=aiohttp.WSMsgType.TEXT, ) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_json = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertTrue(any(["pong" in str(payload) for payload in sent_json])) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep" ) def test_listen_for_order_book_diff_logs_exception_parsing_message( self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add incomplete diff event message be processed diff_response = {"m": "depth", "symbol": "incomplete response"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop, output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diff_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add diff event message be processed diff_response = { "m": "depth", "symbol": self.ex_trading_pair, "data": { "ts": 1573069021376, "seqnum": 2097965, "asks": [["0.06844", "10760"]], "bids": [["0.06777", "562.4"], ["0.05", "221760.6"]], }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) diffs_queue = self.data_source._message_queue[ self.data_source.DIFF_TOPIC_ID] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(ev_loop=self.ev_loop, output=output_queue)) order_book_message = self.async_run_with_timeout(output_queue.get()) self.assertTrue(diffs_queue.empty()) self.assertEqual(1573069021376, order_book_message.update_id) self.assertEqual(1573069021376, order_book_message.timestamp) self.assertEqual(0.06777, order_book_message.bids[0].price) self.assertEqual(0.05, order_book_message.bids[1].price) self.assertEqual(0.06844, order_book_message.asks[0].price) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_raises_cancel_exceptions(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=queue)) with self.assertRaises(asyncio.CancelledError): self.listening_task.cancel() self.ev_loop.run_until_complete(self.listening_task) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.ascend_ex.ascend_ex_api_order_book_data_source.AscendExAPIOrderBookDataSource._sleep" ) def test_listen_for_trades_logs_exception_parsing_message( self, _, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add incomplete diff event message be processed diff_response = {"m": "trades", "symbol": "incomplete response"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(diff_response)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) # Add trade event message be processed trade_response = { "m": "trades", "symbol": "BTC/USDT", "data": [ { "seqnum": 144115191800016553, "p": "0.06762", "q": "400", "ts": 1573165890854, "bm": False }, { "seqnum": 144115191800070421, "p": "0.06797", "q": "341", "ts": 1573166037845, "bm": True }, ], } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(trade_response)) trades_queue = self.data_source._message_queue[ self.data_source.DIFF_TOPIC_ID] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_trades(ev_loop=self.ev_loop, output=output_queue)) first_trade_message = self.async_run_with_timeout(output_queue.get()) second_trade_message = self.async_run_with_timeout(output_queue.get()) self.assertTrue(trades_queue.empty()) self.assertEqual(1573165890854, first_trade_message.timestamp) self.assertEqual(1573166037845, second_trade_message.timestamp) @aioresponses() def test_listen_for_order_book_snapshot_event(self, api_mock): mock_response = { "code": 0, "data": { "m": "depth-snapshot", "symbol": self.ex_trading_pair, "data": { "seqnum": 5068757, "ts": 1573165838976, "asks": [["0.06848", "4084.2"], ["0.0696", "15890.6"]], "bids": [["0.06703", "13500"], ["0.06615", "24036.9"]], }, }, } self.data_source._trading_pairs = ["BTC-USDT"] # Add trade event message be processed url = re.escape( f"{CONSTANTS.REST_URL}/{CONSTANTS.DEPTH_PATH_URL}?symbol=") regex_url = re.compile(f"^{url}") api_mock.get(regex_url, body=json.dumps(mock_response)) order_book_messages = asyncio.Queue() task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( ev_loop=self.ev_loop, output=order_book_messages)) order_book_message = self.ev_loop.run_until_complete( order_book_messages.get()) try: task.cancel() self.ev_loop.run_until_complete(task) except asyncio.CancelledError: # The exception will happen when cancelling the task pass self.assertTrue(order_book_messages.empty()) self.assertEqual(1573165838976, order_book_message.update_id) self.assertEqual(1573165838976, order_book_message.timestamp) self.assertEqual(0.06703, order_book_message.bids[0].price) self.assertEqual(0.06848, order_book_message.asks[0].price)
class CoinflexAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.domain = CONSTANTS.DEFAULT_DOMAIN 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 = CoinflexAPIOrderBookDataSource(trading_pairs=[self.trading_pair], throttler=self.throttler, domain=self.domain) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.resume_test_event = asyncio.Event() CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = { "coinflex": bidict( {f"{self.ex_trading_pair}": self.trading_pair}) } def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {} 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 _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception 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 _successfully_subscribed_event(self): resp = { "result": None, "id": 1 } return resp def _login_message(self): resp = { "tag": "1234567890", "event": "login", "success": True, "timestamp": "1234567890" } return resp def _trade_update_event(self): resp = { "table": "trade", "data": [{ "timestamp": 123456789, "marketCode": self.ex_trading_pair, "tradeId": 12345, "side": "BUY", "price": "0.001", "quantity": "100", }] } return resp def _order_diff_event(self): resp = { "table": "depth", "data": [{ "timestamp": 123456789, "instrumentId": self.ex_trading_pair, "seqNum": 157, "bids": [["0.0024", "10"]], "asks": [["0.0026", "100"]] }] } return resp def _snapshot_response(self, update_id=1027024): resp = { "event": "depthL1000", "timestamp": update_id, "data": [{ "bids": [ [ "4.00000000", "431.00000000" ] ], "asks": [ [ "4.00000200", "12.00000000" ] ], "marketCode": self.ex_trading_pair, "timestamp": update_id, }] } return resp def _get_regex_url(self, endpoint, return_url=False, endpoint_api_version=None, public=True): prv_or_pub = web_utils.public_rest_url if public else web_utils.private_rest_url url = prv_or_pub(endpoint, domain=self.domain, endpoint_api_version=endpoint_api_version) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) if return_url: return url, regex_url return regex_url @aioresponses() def test_get_last_trade_prices(self, mock_api): url, regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, return_url=True) mock_response = [{ "last": "100.0", "open24h": "38719", "high24h": "38840", "low24h": "36377", "volume24h": "3622970.9407847790", "currencyVolume24h": "96.986", "openInterest": "0", "marketCode": "COINALPHA-HBOT", "timestamp": "1645546950025", "lastQty": "0.086", "markPrice": "37645", "lastMarkPrice": "37628", }] mock_api.get(regex_url, body=json.dumps(mock_response)) result: Dict[str, float] = self.async_run_with_timeout( 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]) @aioresponses() def test_get_last_trade_prices_exception_raised(self, mock_api): url, regex_url = self._get_regex_url(CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, return_url=True) mock_api.get(regex_url, body=json.dumps([{"marketCode": "COINALPHA-HBOT"}])) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_last_traded_prices(trading_pairs=[self.trading_pair], throttler=self.throttler) ) @aioresponses() def test_fetch_trading_pairs(self, mock_api): CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {} url = web_utils.public_rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL, domain=self.domain) mock_response: Dict[str, Any] = { "event": "markets", "timestamp": "1639598493658", "data": [ { "marketId": "2001000000000", "marketCode": "BTC-USD", "name": "BTC/USD", "referencePair": "BTC/USD", "base": "BTC", "counter": "USD", "type": "MARGIN", "tickSize": "1", "qtyIncrement": "0.001", "marginCurrency": "USD", "contractValCurrency": "BTC", "upperPriceBound": "39203", "lowerPriceBound": "36187", "marketPrice": "37695", "markPrice": None, "listingDate": 1593316800000, "endDate": 0, "marketPriceLastUpdated": 1645547473153, "markPriceLastUpdated": 0 }, { "marketId": "34001000000000", "marketCode": "LTC-USD", "name": "LTC/USD", "referencePair": "LTC/USD", "base": "LTC", "counter": "USD", "type": "SPOT", "tickSize": "0.1", "qtyIncrement": "0.01", "marginCurrency": "USD", "contractValCurrency": "LTC", "upperPriceBound": "114.2", "lowerPriceBound": "97.2", "marketPrice": "105.7", "markPrice": None, "listingDate": 1609765200000, "endDate": 0, "marketPriceLastUpdated": 1645547512308, "markPriceLastUpdated": 0 }, { "marketId": "4001000000000", "marketCode": "ETH-USD", "name": "ETH/USD", "referencePair": "ETH/USD", "base": "ETH", "counter": "USD", "type": "SPOT", "tickSize": "0.1", "qtyIncrement": "0.01", "marginCurrency": "USD", "contractValCurrency": "ETH", "upperPriceBound": "2704.3", "lowerPriceBound": "2496.1", "marketPrice": "2600.2", "markPrice": None, "listingDate": 0, "endDate": 0, "marketPriceLastUpdated": 1645547505166, "markPriceLastUpdated": 0 }, ] } mock_api.get(url, body=json.dumps(mock_response)) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs() ) self.assertEqual(2, len(result)) self.assertIn("ETH-USD", result) self.assertIn("LTC-USD", result) self.assertNotIn("BTC-USD", result) @aioresponses() @patch("hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time") def test_fetch_trading_pairs_exception_raised(self, mock_api, retry_sleep_time_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 CoinflexAPIOrderBookDataSource._trading_pair_symbol_map = {} url, regex_url = self._get_regex_url(CONSTANTS.EXCHANGE_INFO_PATH_URL, return_url=True) mock_api.get(regex_url, exception=Exception) result: Dict[str] = self.async_run_with_timeout( self.data_source.fetch_trading_pairs() ) self.assertEqual(0, len(result)) @aioresponses() def test_get_snapshot_successful(self, mock_api): url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) result: Dict[str, Any] = self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair) ) self.assertEqual(self._snapshot_response()["data"][0], result) @aioresponses() @patch("hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time") def test_get_snapshot_catch_exception(self, mock_api, retry_sleep_time_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, status=400) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair) ) mock_api.get(regex_url, body=json.dumps({})) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair) ) @aioresponses() def test_get_new_order_book(self, mock_api): url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response(update_id=1))) result: OrderBook = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair) ) self.assertEqual(1, result.snapshot_uid) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(self._login_message())) self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(1, len(sent_subscription_messages)) expected_subscription = { "op": "subscribe", "args": [ f"trade:{self.ex_trading_pair}", f"depth:{self.ex_trading_pair}", ], } self.assertEqual(expected_subscription, sent_subscription_messages[0]) self.assertTrue(self._is_logged( "INFO", "Subscribed to public order book and trade channels..." )) @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep") @patch("aiohttp.ClientSession.ws_connect") def test_listen_for_subscriptions_raises_cancel_exception(self, mock_ws, _: AsyncMock): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch("hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep") @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_logs_exception_details(self, mock_ws, sleep_mock): mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError()) self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds...")) def test_subscribe_channels_raises_cancel_exception(self): mock_ws = MagicMock() mock_ws.send.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task(self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) def test_subscribe_channels_raises_exception_and_logs_error(self): mock_ws = MagicMock() mock_ws.send.side_effect = Exception("Test Error") with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task(self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged("ERROR", "Unexpected error occurred subscribing to order book trading and delta streams...") ) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() 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.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "data": [{ "m": 1, "i": 2, }], } mock_queue = AsyncMock() mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()] self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue) ) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged("ERROR", "Unexpected error when processing public trade updates from exchange")) def test_listen_for_trades_successful(self): mock_queue = AsyncMock() mock_queue.get.side_effect = [self._login_message(), self._trade_update_event(), asyncio.CancelledError()] self.data_source._message_queue[CONSTANTS.TRADE_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue) ) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(-1, msg.update_id) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue 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_diffs(self.ev_loop, msg_queue) ) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_diffs_logs_exception(self): incomplete_resp = { "data": [{ "m": 1, "i": 2, }], } mock_queue = AsyncMock() mock_queue.get.side_effect = [incomplete_resp, asyncio.CancelledError()] self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue) ) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged("ERROR", "Unexpected error when processing public order book updates from exchange")) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(self._login_message())) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(self._order_diff_event())) self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) mock_queue = AsyncMock() mock_queue.get.side_effect = [self._login_message(), self._order_diff_event(), asyncio.CancelledError()] self.data_source._message_queue[CONSTANTS.DIFF_EVENT_TYPE] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() try: self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs(self.ev_loop, msg_queue) ) except asyncio.CancelledError: pass msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(123456789, msg.update_id) @aioresponses() def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(self, mock_api): url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots(self.ev_loop, asyncio.Queue()) ) @aioresponses() @patch("hummingbot.connector.exchange.coinflex.coinflex_api_order_book_data_source" ".CoinflexAPIOrderBookDataSource._sleep") @patch("hummingbot.connector.exchange.coinflex.coinflex_web_utils.retry_sleep_time") def test_listen_for_order_book_snapshots_log_exception(self, mock_api, retry_sleep_time_mock, sleep_mock): retry_sleep_time_mock.side_effect = lambda *args, **kwargs: 0 msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(asyncio.CancelledError()) url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, exception=Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue) ) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged("ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}.")) @aioresponses() @patch("hummingbot.connector.exchange.coinflex.coinflex_api_order_book_data_source" ".CoinflexAPIOrderBookDataSource._sleep") def test_listen_for_order_book_snapshots_log_outer_exception(self, mock_api, sleep_mock): msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event(Exception("Dummy")) url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(self._snapshot_response())) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots(self.ev_loop, msg_queue) ) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged("ERROR", "Unexpected error.")) @aioresponses() def test_listen_for_order_book_snapshots_successful(self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url, regex_url = self._get_regex_url(CONSTANTS.SNAPSHOT_PATH_URL.format(self.trading_pair, 1000), return_url=True) mock_api.get(regex_url, body=json.dumps(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.async_run_with_timeout(msg_queue.get()) self.assertEqual(1027024, msg.update_id)
class TestKucoinAPIUserStreamDataSource(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.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self.mock_time_provider = MagicMock() self.mock_time_provider.time.return_value = 1000 self.auth = KucoinAuth( self.api_key, self.api_passphrase, self.api_secret_key, time_provider=self.mock_time_provider) client_config_map = ClientConfigAdapter(ClientConfigMap()) self.connector = KucoinExchange( client_config_map=client_config_map, kucoin_api_key="", kucoin_passphrase="", kucoin_secret_key="", trading_pairs=[], trading_required=False) self.data_source = KucoinAPIUserStreamDataSource( auth=self.auth, trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) 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 @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() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id") def test_listen_for_user_stream_subscribes_to_orders_and_balances_events(self, mock_api, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2] url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [ { "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 50000, "pingTimeout": 10000 } ], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() result_subscribe_trades = { "type": "ack", "id": 1 } result_subscribe_diffs = { "type": "ack", "id": 2 } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_orders_subscription = { "id": 1, "type": "subscribe", "topic": "/spotMarket/tradeOrders", "privateChannel": True, "response": False } self.assertEqual(expected_orders_subscription, sent_subscription_messages[0]) expected_balances_subscription = { "id": 2, "type": "subscribe", "topic": "/account/balance", "privateChannel": True, "response": False } self.assertEqual(expected_balances_subscription, sent_subscription_messages[1]) self.assertTrue(self._is_logged( "INFO", "Subscribed to private order changes and balance updates channels..." )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_get_listen_key_successful_with_user_update_event(self, mock_api, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_api.post(regex_url, body=json.dumps(mock_response)) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() order_event = { "type": "message", "topic": "/spotMarket/tradeOrders", "subject": "orderChange", "channelType": "private", "data": { "symbol": "KCS-USDT", "orderType": "limit", "side": "buy", "orderId": "5efab07953bdea00089965d2", "type": "open", "orderTime": 1593487481683297666, "size": "0.1", "filledSize": "0", "price": "0.937", "clientOid": "1593487481000906", "remainSize": "0.1", "status": "open", "ts": 1593487481683297666 } } self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, json.dumps(order_event)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue) ) msg = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(order_event, msg) mock_ws.return_value.ping.assert_called() @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_pong_payload(self, mock_api, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_pong = { "id": "1545910590801", "type": "pong" } mock_api.post(regex_url, body=json.dumps(mock_response)) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message(mock_ws.return_value, json.dumps(mock_pong)) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue) ) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep") def test_listen_for_user_stream_connection_failed(self, mock_api, sleep_mock, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_api.post(regex_url, body=json.dumps(mock_response)) mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = asyncio.CancelledError # to finish the task execution msg_queue = asyncio.Queue() try: self.async_run_with_timeout(self.data_source.listen_for_user_stream(msg_queue)) except asyncio.CancelledError: pass self.assertTrue( self._is_logged("ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds...")) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep") def test_listen_for_user_stream_iter_message_throws_exception(self, mock_api, sleep_mock, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = self.get_listen_key_mock() mock_api.post(regex_url, body=json.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.receive.side_effect = Exception("TEST ERROR") sleep_mock.side_effect = asyncio.CancelledError # to finish the task execution try: self.async_run_with_timeout(self.data_source.listen_for_user_stream(msg_queue)) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds...")) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch("hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id") @patch("hummingbot.connector.exchange.kucoin.kucoin_api_user_stream_data_source.KucoinAPIUserStreamDataSource" "._time") def test_listen_for_user_stream_sends_ping_message_before_ping_interval_finishes( self, mock_api, time_mock, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2, 3, 4] time_mock.side_effect = [1000, 1100, 1101, 1102] # Simulate first ping interval is already due url = web_utils.private_rest_url(path_url=CONSTANTS.PRIVATE_WS_DATA_PATH_URL) resp = { "code": "200000", "data": { "instanceServers": [ { "endpoint": "wss://test.url/endpoint", "protocol": "websocket", "encrypt": True, "pingInterval": 20000, "pingTimeout": 10000 } ], "token": "testToken" } } mock_api.post(url, body=json.dumps(resp)) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock() result_subscribe_trades = { "type": "ack", "id": 1 } result_subscribe_diffs = { "type": "ack", "id": 2 } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) output_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task(self.data_source.listen_for_user_stream(output=output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered(ws_connect_mock.return_value) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) expected_ping_message = { "id": 3, "type": "ping", } self.assertEqual(expected_ping_message, sent_messages[-1])
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}" cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}" @classmethod def tearDownClass(cls) -> None: for task in asyncio.all_tasks(loop=cls.ev_loop): task.cancel() def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.mocking_assistant = NetworkMockingAssistant() self.client_config_map = ClientConfigAdapter(ClientConfigMap()) self.connector = BitmartExchange( client_config_map=self.client_config_map, bitmart_api_key="", bitmart_secret_key="", bitmart_memo="", trading_pairs=[self.trading_pair], trading_required=False, ) self.data_source = BitmartAPIOrderBookDataSource( trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.connector._set_trading_pair_symbol_map( bidict({self.ex_trading_pair: self.trading_pair})) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def 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 _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_get_new_order_book_successful(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))) order_book: OrderBook = results[0] self.assertTrue(type(order_book) == OrderBook) self.assertEqual(order_book.snapshot_uid, mock_response["data"]["timestamp"]) self.assertEqual(mock_response["data"]["timestamp"], order_book.snapshot_uid) bids = list(order_book.bid_entries()) asks = list(order_book.ask_entries()) self.assertEqual(2, len(bids)) self.assertEqual(float(mock_response["data"]["buys"][0]["price"]), bids[0].price) self.assertEqual(float(mock_response["data"]["buys"][0]["amount"]), bids[0].amount) self.assertEqual(mock_response["data"]["timestamp"], bids[0].update_id) self.assertEqual(2, len(asks)) self.assertEqual(float(mock_response["data"]["sells"][0]["price"]), asks[0].price) self.assertEqual(float(mock_response["data"]["sells"][0]["amount"]), asks[0].amount) self.assertEqual(mock_response["data"]["timestamp"], asks[0].update_id) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = { "event": "subscribe", "table": f"{CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME}:{self.ex_trading_pair}", } result_subscribe_diffs = { "event": "subscribe", "table": f"{CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME}:{self.ex_trading_pair}", } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_trade_subscription = { "op": "subscribe", "args": [f"{CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME}:{self.ex_trading_pair}"] } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { "op": "subscribe", "args": [f"{CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME}:{self.ex_trading_pair}"] } self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to public order book and trade channels...")) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect") def test_listen_for_subscriptions_raises_cancel_exception( self, mock_ws, _): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_logs_exception_details( self, mock_ws, sleep_mock): mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = asyncio.CancelledError self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) def test_subscribe_channels_raises_cancel_exception(self): mock_ws = MagicMock() mock_ws.send.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) def test_subscribe_channels_raises_exception_and_logs_error(self): mock_ws = MagicMock() mock_ws.send.side_effect = Exception("Test Error") with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_compressed_messages_are_correctly_read(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = { "event": "subscribe", "table": f"{CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME}:{self.ex_trading_pair}", } result_subscribe_diffs = { "event": "subscribe", "table": f"{CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME}:{self.ex_trading_pair}", } trade_event = { "table": CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME, "data": [{ "symbol": self.ex_trading_pair, "price": "162.12", "side": "buy", "size": "11.085", "s_t": 1542337219 }, { "symbol": self.ex_trading_pair, "price": "163.12", "side": "buy", "size": "15", "s_t": 1542337238 }] } compressed_trade_event = bitmart_utils.compress_ws_message( json.dumps(trade_event)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=compressed_trade_event, message_type=WSMsgType.BINARY) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) trade_message = self.async_run_with_timeout( self.data_source._message_queue[ self.data_source._trade_messages_queue_key].get()) self.assertEqual(trade_event, trade_message) def test_listen_for_trades(self): msg_queue: asyncio.Queue = asyncio.Queue() mock_queue = AsyncMock() trade_event = { "table": CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME, "data": [{ "symbol": self.ex_trading_pair, "price": "162.12", "side": "buy", "size": "11.085", "s_t": 1542337219 }, { "symbol": self.ex_trading_pair, "price": "163.12", "side": "buy", "size": "15", "s_t": 1542337238 }] } mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()] self.data_source._message_queue[ self.data_source._trade_messages_queue_key] = mock_queue self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) trade1: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) trade2: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(msg_queue.empty()) self.assertEqual(1542337219, int(trade1.trade_id)) self.assertEqual(1542337238, int(trade2.trade_id)) def test_listen_for_trades_raises_cancelled_exception(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError self.data_source._message_queue[ self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() 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.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "table": CONSTANTS.PUBLIC_TRADE_CHANNEL_NAME, "data": [{ "symbol": self.ex_trading_pair, }] } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[ self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public trade updates from exchange" )) def test_listen_for_order_book_diffs_successful(self): mock_queue = AsyncMock() snapshot_event = { "table": CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME, "data": [{ "asks": [["161.96", "7.37567"]], "bids": [["161.94", "4.552355"]], "symbol": self.ex_trading_pair, "ms_t": 1542337219120 }] } mock_queue.get.side_effect = [snapshot_event, asyncio.CancelledError] self.data_source._message_queue[ self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(OrderBookMessageType.SNAPSHOT, msg.type) self.assertEqual(-1, msg.trade_id) self.assertEqual( int(snapshot_event["data"][0]["ms_t"]) * 1e-3, msg.timestamp) expected_update_id = int(snapshot_event["data"][0]["ms_t"]) self.assertEqual(expected_update_id, msg.update_id) bids = msg.bids asks = msg.asks self.assertEqual(1, len(bids)) self.assertEqual(float(snapshot_event["data"][0]["bids"][0][0]), bids[0].price) self.assertEqual(float(snapshot_event["data"][0]["bids"][0][1]), bids[0].amount) self.assertEqual(expected_update_id, bids[0].update_id) self.assertEqual(1, len(asks)) self.assertEqual(float(snapshot_event["data"][0]["asks"][0][0]), asks[0].price) self.assertEqual(float(snapshot_event["data"][0]["asks"][0][1]), asks[0].amount) self.assertEqual(expected_update_id, asks[0].update_id) def test_listen_for_order_book_snapshots_raises_cancelled_exception(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[ self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_snapshots_logs_exception(self): incomplete_resp = { "table": CONSTANTS.PUBLIC_DEPTH_CHANNEL_NAME, "data": [{ "symbol": self.ex_trading_pair, "ms_t": 1542337219120 }] } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[ self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" ))
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 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 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("aiohttp.ClientSession.ws_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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_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_aiohttp_message( ws_connect_mock.return_value, json.dumps('dummyMessage')) first_received_message = self.async_run_with_timeout(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.json_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( json.dumps(authentication_request))) self.assertEqual( CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME, NdaxWebSocketAdaptor.endpoint_from_raw_message( json.dumps(subscription_request))) subscription_payload = NdaxWebSocketAdaptor.payload_from_raw_message( json.dumps(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("aiohttp.ClientSession.ws_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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(False)) try: self.async_run_with_timeout(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("aiohttp.ClientSession.ws_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(messages)) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_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_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ 'n'] else self.mocking_assistant._sent_websocket_json_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(messages)) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_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_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS. SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message[ 'n'] else self.mocking_assistant._sent_websocket_json_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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(True)) self.async_run_with_timeout(self.listening_task) @patch("aiohttp.ClientSession.ws_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()) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "NETWORK", "Unexpected error occurred during ndax WebSocket Connection ()" )) @patch("aiohttp.ClientSession.ws_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_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ 'n'] else self.mocking_assistant._sent_websocket_json_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(messages)) self.async_run_with_timeout(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("aiohttp.ClientSession.ws_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_json.side_effect = lambda sent_message: ( CONSTANTS.SUBSCRIBE_ACCOUNT_EVENTS_ENDPOINT_NAME in sent_message[ 'n'] 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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, self._authentication_response(True)) self.async_run_with_timeout(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 DydxPerpetualUserStreamDataSourceUnitTests(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.api_key = "someKey" cls.api_secret = "someSecretKey" cls.passphrase = "somePassphrase" cls.account_number = "someAccountNumber" cls.stark_private_key = "A" * 16 cls.eth_address = "someEthAddress" cls.dydx_client = DydxPerpetualClientWrapper( api_key=cls.api_key, api_secret=cls.api_secret, passphrase=cls.passphrase, account_number=cls.account_number, stark_private_key=cls.stark_private_key, ethereum_address=cls.eth_address, ) cls.dydx_auth = DydxPerpetualAuth(cls.dydx_client) def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task: Optional[asyncio.Task] = None self.data_source = DydxPerpetualUserStreamDataSource(self.dydx_auth) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: self.async_task and self.async_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 _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_user_stream_data_source.DydxPerpetualUserStreamDataSource._sleep" ) def test_listen_for_user_stream_raises_cancelled_exception( self, _, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_user_stream(asyncio.Queue())) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_auth.DydxPerpetualAuth.get_ws_auth_params" ) @patch( "hummingbot.connector.derivative.dydx_perpetual.dydx_perpetual_user_stream_data_source.DydxPerpetualUserStreamDataSource._sleep" ) def test_listen_for_user_stream_raises_logs_exception( self, mock_sleep, mock_auth, ws_connect_mock): mock_sleep.side_effect = lambda: (self.ev_loop.run_until_complete( asyncio.sleep(0.5))) mock_auth.return_value = {} ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = lambda *_: self._create_exception_and_unlock_test_with_event( Exception("TEST ERROR")) self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(asyncio.Queue())) self.async_run_with_timeout(self.resume_test_event.wait(), 1.0) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with dydx WebSocket connection. Retrying after 30 seconds..." ))
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) for task in asyncio.all_tasks(loop=cls.ev_loop): task.cancel() @classmethod def tearDownClass(cls) -> None: for task in asyncio.all_tasks(loop=cls.ev_loop): task.cancel() 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 BinancePerpetualUserStreamDataSourceUnitTests(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 = CONSTANTS.TESTNET_DOMAIN cls.api_key = "TEST_API_KEY" cls.secret_key = "TEST_SECRET_KEY" 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.emulated_time = 1640001112.223 self.auth = BinancePerpetualAuth(api_key=self.api_key, api_secret=self.secret_key, time_provider=self) self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.time_synchronizer = TimeSynchronizer() self.time_synchronizer.add_time_offset_ms_sample(0) self.data_source = BinancePerpetualUserStreamDataSource( auth=self.auth, domain=self.domain, throttler=self.throttler, time_synchronizer=self.time_synchronizer) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mock_done_event = asyncio.Event() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) 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 _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 _mock_responses_done_callback(self, *_, **__): self.mock_done_event.set() def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _successful_get_listen_key_response(self) -> str: resp = {"listenKey": self.listen_key} return ujson.dumps(resp) def _error_response(self) -> Dict[str, Any]: resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"} return resp def _simulate_user_update_event(self): # Order Trade Update resp = { "e": "ORDER_TRADE_UPDATE", "E": 1591274595442, "T": 1591274595453, "i": "SfsR", "o": { "s": "BTCUSD_200925", "c": "TEST", "S": "SELL", "o": "TRAILING_STOP_MARKET", "f": "GTC", "q": "2", "p": "0", "ap": "0", "sp": "9103.1", "x": "NEW", "X": "NEW", "i": 8888888, "l": "0", "z": "0", "L": "0", "ma": "BTC", "N": "BTC", "n": "0", "T": 1591274595442, "t": 0, "rp": "0", "b": "0", "a": "0", "m": False, "R": False, "wt": "CONTRACT_PRICE", "ot": "TRAILING_STOP_MARKET", "ps": "LONG", "cp": False, "AP": "9476.8", "cr": "5.0", "pP": False, }, } return ujson.dumps(resp) def time(self): # Implemented to emulate a TimeSynchronizer return self.emulated_time def test_last_recv_time(self): # Initial last_recv_time self.assertEqual(0, self.data_source.last_recv_time) @aioresponses() def test_get_listen_key_exception_raised(self, mock_api): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, status=400, body=ujson.dumps(self._error_response)) with self.assertRaises(IOError): self.async_run_with_timeout(self.data_source.get_listen_key()) @aioresponses() def test_get_listen_key_successful(self, mock_api): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) result: str = self.async_run_with_timeout( self.data_source.get_listen_key()) self.assertEqual(self.listen_key, result) @aioresponses() def test_ping_listen_key_failed_log_warning(self, mock_api): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.put(regex_url, status=400, body=ujson.dumps(self._error_response())) self.data_source._current_listen_key = self.listen_key result: bool = self.async_run_with_timeout( self.data_source.ping_listen_key()) self.assertTrue( self._is_logged( "WARNING", f"Failed to refresh the listen key {self.listen_key}: {self._error_response()}" )) self.assertFalse(result) @aioresponses() def test_ping_listen_key_successful(self, mock_api): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.put(regex_url, body=ujson.dumps({})) self.data_source._current_listen_key = self.listen_key result: bool = self.async_run_with_timeout( self.data_source.ping_listen_key()) self.assertTrue(result) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_create_websocket_connection_log_exception(self, mock_api, mock_ws): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) mock_ws.side_effect = Exception("TEST ERROR.") msg_queue = asyncio.Queue() try: self.async_run_with_timeout( self.data_source.listen_for_user_stream(msg_queue)) except asyncio.exceptions.TimeoutError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.", )) @aioresponses() def test_manage_listen_key_task_loop_keep_alive_failed(self, mock_api): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.put(regex_url, status=400, body=ujson.dumps(self._error_response()), callback=self._mock_responses_done_callback) self.data_source._current_listen_key = self.listen_key # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached self.data_source._last_listen_key_ping_ts = 0 self.listening_task = self.ev_loop.create_task( self.data_source._manage_listen_key_task_loop()) self.async_run_with_timeout(self.mock_done_event.wait()) self.assertTrue( self._is_logged("ERROR", "Error occurred renewing listen key... ")) self.assertIsNone(self.data_source._current_listen_key) self.assertFalse( self.data_source._listen_key_initialized_event.is_set()) @aioresponses() def test_manage_listen_key_task_loop_keep_alive_successful(self, mock_api): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.put(regex_url, body=ujson.dumps({}), callback=self._mock_responses_done_callback) self.data_source._current_listen_key = self.listen_key # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached self.data_source._last_listen_key_ping_ts = 0 self.listening_task = self.ev_loop.create_task( self.data_source._manage_listen_key_task_loop()) self.async_run_with_timeout(self.mock_done_event.wait()) self.assertTrue( self._is_logged("INFO", f"Refreshed listen key {self.listen_key}.")) self.assertGreater(self.data_source._last_listen_key_ping_ts, 0) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_create_websocket_connection_failed( self, mock_api, mock_ws): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) mock_ws.side_effect = Exception("TEST ERROR.") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) try: self.async_run_with_timeout(msg_queue.get()) except asyncio.exceptions.TimeoutError: pass self.assertTrue( self._is_logged( "INFO", f"Successfully obtained listen key {self.listen_key}")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.", )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep" ) def test_listen_for_user_stream_iter_message_throws_exception( self, mock_api, _, mock_ws): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = {"listenKey": self.listen_key} mock_api.post(regex_url, body=ujson.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.receive.side_effect = Exception("TEST ERROR") mock_ws.return_value.closed = False mock_ws.return_value.close.side_effect = Exception self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) try: self.async_run_with_timeout(msg_queue.get()) except Exception: pass self.assertTrue( self._is_logged( "INFO", f"Successfully obtained listen key {self.listen_key}")) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR", )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_successful(self, mock_api, mock_ws): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, self._simulate_user_update_event()) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) msg = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(msg, self._simulate_user_update_event) mock_ws.return_value.ping.assert_called() @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_empty_payload( self, mock_api, mock_ws): url = web_utils.rest_url( path_url=CONSTANTS.BINANCE_USER_STREAM_ENDPOINT, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.post(regex_url, body=self._successful_get_listen_key_response()) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, "") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize())
class OkxAPIOrderBookDataSourceUnitTests(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 def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task = None self.mocking_assistant = NetworkMockingAssistant() client_config_map = ClientConfigAdapter(ClientConfigMap()) self.connector = OkxExchange( client_config_map=client_config_map, okx_api_key="", okx_secret_key="", okx_passphrase="", trading_pairs=[self.trading_pair], trading_required=False, ) self.data_source = OkxAPIOrderBookDataSource( trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.resume_test_event = asyncio.Event() self.connector._set_trading_pair_symbol_map( bidict( {f"{self.base_asset}-{self.quote_asset}": self.trading_pair})) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() OkxAPIOrderBookDataSource._trading_pair_symbol_map = {} 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 _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception 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 @aioresponses() def test_get_new_order_book_successful(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = { "code": "0", "msg": "", "data": [{ "asks": [["41006.8", "0.60038921", "0", "1"]], "bids": [["41006.3", "0.30178218", "0", "2"]], "ts": "1629966436396" }] } mock_api.get(regex_url, body=json.dumps(resp)) order_book: OrderBook = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) expected_update_id = int(int(resp["data"][0]["ts"]) * 1e-3) self.assertEqual(expected_update_id, order_book.snapshot_uid) bids = list(order_book.bid_entries()) asks = list(order_book.ask_entries()) self.assertEqual(1, len(bids)) self.assertEqual(41006.3, bids[0].price) self.assertEqual(2, bids[0].amount) self.assertEqual(expected_update_id, bids[0].update_id) self.assertEqual(1, len(asks)) self.assertEqual(41006.8, asks[0].price) self.assertEqual(1, asks[0].amount) self.assertEqual(expected_update_id, asks[0].update_id) @aioresponses() def test_get_new_order_book_raises_exception(self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400) with self.assertRaises(IOError): self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) result_subscribe_trades = { "event": "subscribe", "args": { "channel": "trades", "instId": self.trading_pair } } result_subscribe_diffs = { "event": "subscribe", "arg": { "channel": "books", "instId": self.trading_pair } } self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_trades)) self.mocking_assistant.add_websocket_aiohttp_message( websocket_mock=ws_connect_mock.return_value, message=json.dumps(result_subscribe_diffs)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) sent_subscription_messages = self.mocking_assistant.json_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) self.assertEqual(2, len(sent_subscription_messages)) expected_trade_subscription = { "op": "subscribe", "args": [{ "channel": "trades", "instId": self.trading_pair }] } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { "op": "subscribe", "args": [{ "channel": "books", "instId": self.trading_pair }] } self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to public order book and trade channels...")) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_sends_ping_message_before_ping_interval_finishes( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = [ asyncio.TimeoutError("Test timeiout"), asyncio.CancelledError ] self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass sent_messages = self.mocking_assistant.text_messages_sent_through_websocket( websocket_mock=ws_connect_mock.return_value) expected_ping_message = "ping" self.assertEqual(expected_ping_message, sent_messages[0]) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect") def test_listen_for_subscriptions_raises_cancel_exception( self, mock_ws, _: AsyncMock): mock_ws.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.listening_task) @patch( "hummingbot.core.data_type.order_book_tracker_data_source.OrderBookTrackerDataSource._sleep" ) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_logs_exception_details( self, mock_ws, sleep_mock): mock_ws.side_effect = Exception("TEST ERROR.") sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_subscriptions()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) def test_subscribe_channels_raises_cancel_exception(self): mock_ws = MagicMock() mock_ws.send.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) def test_subscribe_channels_raises_exception_and_logs_error(self): mock_ws = MagicMock() mock_ws.send.side_effect = Exception("Test Error") with self.assertRaises(Exception): self.listening_task = self.ev_loop.create_task( self.data_source._subscribe_channels(mock_ws)) self.async_run_with_timeout(self.listening_task) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred subscribing to order book trading and delta streams..." )) def test_listen_for_trades_cancelled_when_listening(self): mock_queue = MagicMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[ self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() 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.async_run_with_timeout(self.listening_task) def test_listen_for_trades_logs_exception(self): incomplete_resp = { "arg": { "channel": "trades", "instId": "BTC-USDT" }, "data": [{ "instId": "BTC-USDT", }] } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[ self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public trade updates from exchange" )) def test_listen_for_trades_successful(self): mock_queue = AsyncMock() trade_event = { "arg": { "channel": "trades", "instId": self.trading_pair }, "data": [{ "instId": self.trading_pair, "tradeId": "130639474", "px": "42219.9", "sz": "0.12060306", "side": "buy", "ts": "1630048897897" }] } mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()] self.data_source._message_queue[ self.data_source._trade_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(OrderBookMessageType.TRADE, msg.type) self.assertEqual(trade_event["data"][0]["tradeId"], msg.trade_id) self.assertEqual( int(trade_event["data"][0]["ts"]) * 1e-3, msg.timestamp) def test_listen_for_order_book_diffs_cancelled(self): mock_queue = AsyncMock() mock_queue.get.side_effect = asyncio.CancelledError() self.data_source._message_queue[ self.data_source._diff_messages_queue_key] = mock_queue 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_diffs( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.listening_task) def test_listen_for_order_book_diffs_logs_exception(self): incomplete_resp = { "arg": { "channel": "books", "instId": self.trading_pair }, "action": "update", } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.data_source._message_queue[ self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) try: self.async_run_with_timeout(self.listening_task) except asyncio.CancelledError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error when processing public order book updates from exchange" )) def test_listen_for_order_book_diffs_successful(self): mock_queue = AsyncMock() diff_event = { "arg": { "channel": "books", "instId": self.trading_pair }, "action": "update", "data": [{ "asks": [ ["8476.98", "415", "0", "13"], ["8477", "7", "0", "2"], ["8477.34", "85", "0", "1"], ], "bids": [ ["8476.97", "256", "0", "12"], ["8475.55", "101", "0", "1"], ], "ts": "1597026383085", "checksum": -855196043 }] } mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()] self.data_source._message_queue[ self.data_source._diff_messages_queue_key] = mock_queue msg_queue: asyncio.Queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(OrderBookMessageType.DIFF, msg.type) self.assertEqual(-1, msg.trade_id) self.assertEqual( int(diff_event["data"][0]["ts"]) * 1e-3, msg.timestamp) expected_update_id = int(int(diff_event["data"][0]["ts"]) * 1e-3) self.assertEqual(expected_update_id, msg.update_id) bids = msg.bids asks = msg.asks self.assertEqual(2, len(bids)) self.assertEqual(8476.97, bids[0].price) self.assertEqual(12, bids[0].amount) self.assertEqual(expected_update_id, bids[0].update_id) self.assertEqual(3, len(asks)) self.assertEqual(8476.98, asks[0].price) self.assertEqual(13, asks[0].amount) self.assertEqual(expected_update_id, asks[0].update_id) @aioresponses() def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot( self, mock_api): url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=asyncio.CancelledError) with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue())) @aioresponses() @patch("hummingbot.connector.exchange.okx.okx_api_order_book_data_source" ".OkxAPIOrderBookDataSource._sleep") def test_listen_for_order_book_snapshots_log_exception( self, mock_api, sleep_mock): msg_queue: asyncio.Queue = asyncio.Queue() sleep_mock.side_effect = lambda _: self._create_exception_and_unlock_test_with_event( asyncio.CancelledError()) url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, exception=Exception) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", f"Unexpected error fetching order book snapshot for {self.trading_pair}." )) @aioresponses() def test_listen_for_order_book_snapshots_successful( self, mock_api, ): msg_queue: asyncio.Queue = asyncio.Queue() url = web_utils.public_rest_url(path_url=CONSTANTS.OKX_ORDER_BOOK_PATH) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = { "code": "0", "msg": "", "data": [{ "asks": [["41006.8", "0.60038921", "0", "1"]], "bids": [["41006.3", "0.30178218", "0", "2"]], "ts": "1629966436396" }] } mock_api.get(regex_url, body=json.dumps(resp)) self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertEqual(OrderBookMessageType.SNAPSHOT, msg.type) self.assertEqual(-1, msg.trade_id) self.assertEqual(int(resp["data"][0]["ts"]) * 1e-3, msg.timestamp) expected_update_id = int(int(resp["data"][0]["ts"]) * 1e-3) self.assertEqual(expected_update_id, msg.update_id) bids = msg.bids asks = msg.asks self.assertEqual(1, len(bids)) self.assertEqual(41006.3, bids[0].price) self.assertEqual(2, bids[0].amount) self.assertEqual(expected_update_id, bids[0].update_id) self.assertEqual(1, len(asks)) self.assertEqual(41006.8, asks[0].price) self.assertEqual(1, asks[0].amount) self.assertEqual(expected_update_id, asks[0].update_id)
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}" 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)) 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("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_trade_data_mock() self.mocking_assistant.add_websocket_aiohttp_message( 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_aiohttp_messages_delivered( websocket_mock=ws_connect_mock.return_value) self.assertTrue(not output_queue.empty()) msg = output_queue.get_nowait() self.assertTrue(isinstance(msg, OrderBookMessage)) 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 TestCoinbaseProAPIUserStreamDataSource(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() auth = CoinbaseProAuth(api_key="SomeAPIKey", secret_key="shht", passphrase="SomePassPhrase") self.mocking_assistant = NetworkMockingAssistant() web_assistants_factory = build_coinbase_pro_web_assistant_factory(auth) self.data_source = CoinbaseProAPIUserStreamDataSource( trading_pairs=[self.trading_pair], web_assistants_factory=web_assistants_factory) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.log_records = [] self.async_tasks: List[asyncio.Task] = [] def tearDown(self) -> None: for task in self.async_tasks: task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def get_ws_open_message_mock(self) -> Dict: message = { "type": "open", "time": "2014-11-07T08:19:27.028459Z", "product_id": self.trading_pair, "sequence": 10, "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", "price": "200.2", "remaining_size": "1.00", "side": "sell" } return message def get_ws_match_message_mock(self) -> Dict: message = { "type": "match", "trade_id": 10, "sequence": 50, "maker_order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", "taker_order_id": "132fb6ae-456b-4654-b4e0-d681ac05cea1", "time": "2014-11-07T08:19:27.028459Z", "product_id": self.trading_pair, "size": "5.23512", "price": "400.23", "side": "sell" } return message def get_ws_change_message_mock(self) -> Dict: message = { "type": "change", "time": "2014-11-07T08:19:27.028459Z", "sequence": 80, "order_id": "ac928c66-ca53-498f-9c13-a110027a60e8", "product_id": self.trading_pair, "new_size": "5.23512", "old_size": "12.234412", "price": "400.23", "side": "sell" } return message def get_ws_done_message_mock(self) -> Dict: message = { "type": "done", "time": "2014-11-07T08:19:27.028459Z", "product_id": self.trading_pair, "sequence": 10, "price": "200.2", "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", "reason": "filled", "side": "sell", "remaining_size": "0" } return message @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_processes_open_message( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_ws_open_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) content = output_queue.get_nowait() self.assertEqual(resp, content) # shallow comparison is ok @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_processes_match_message( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_ws_match_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) content = output_queue.get_nowait() self.assertEqual(resp, content) # shallow comparison is ok @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_processes_change_message( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_ws_change_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) content = output_queue.get_nowait() self.assertEqual(resp, content) # shallow comparison is ok @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_processes_done_message( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = self.get_ws_done_message_mock() self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) content = output_queue.get_nowait() self.assertEqual(resp, content) # shallow comparison is ok @patch( "hummingbot.connector.exchange.coinbase_pro" ".coinbase_pro_api_user_stream_data_source.CoinbaseProAPIUserStreamDataSource._sleep" ) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_raises_on_no_type(self, ws_connect_mock, _): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = {} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( log_level="NETWORK", message="Unexpected error with WebSocket connection.")) self.assertTrue(output_queue.empty()) @patch( "hummingbot.connector.exchange.coinbase_pro" ".coinbase_pro_api_user_stream_data_source.CoinbaseProAPIUserStreamDataSource._sleep" ) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_raises_on_error_message( self, ws_connect_mock, _): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) resp = {"type": "error", "message": "some error"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps(resp)) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue( self._is_logged( log_level="NETWORK", message="Unexpected error with WebSocket connection.")) self.assertTrue(output_queue.empty()) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_ignores_irrelevant_messages( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"type": "received"})) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"type": "activate"})) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, json.dumps({"type": "subscriptions"})) output_queue = asyncio.Queue() t = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.async_tasks.append(t) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue(output_queue.empty())
class BitmexUserStreamDataSourceUnitTests(unittest.TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}_{cls.quote_asset}" cls.domain = CONSTANTS.TESTNET_DOMAIN cls.api_key = "TEST_API_KEY" cls.secret_key = "TEST_SECRET_KEY" def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() self.emulated_time = 1640001112.223 self.auth = BitmexAuth(api_key=self.api_key, api_secret=self.secret_key) self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.data_source = BitmexUserStreamDataSource(auth=self.auth, domain=self.domain, throttler=self.throttler) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mock_done_event = asyncio.Event() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) 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 _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 _mock_responses_done_callback(self, *_, **__): self.mock_done_event.set() def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _error_response(self) -> Dict[str, Any]: resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"} return resp def _simulate_user_update_event(self): # Order Trade Update resp = { "table": "execution", "data": [{ "orderID": "1", "clordID": "2", "price": 20, "orderQty": 100, "symbol": "COINALPHA_HBOT", "side": "Sell", "leavesQty": "1" }], } return ujson.dumps(resp) def time(self): # Implemented to emulate a TimeSynchronizer return self.emulated_time def test_last_recv_time(self): # Initial last_recv_time self.assertEqual(0, self.data_source.last_recv_time) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_create_websocket_connection_log_exception(self, mock_ws): mock_ws.side_effect = Exception("TEST ERROR.") msg_queue = asyncio.Queue() try: self.async_run_with_timeout( self.data_source.listen_for_user_stream( self.ev_loop, msg_queue)) except asyncio.exceptions.TimeoutError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.", )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_create_websocket_connection_failed( self, mock_api, mock_ws): mock_ws.side_effect = Exception("TEST ERROR.") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(msg_queue.get()) except asyncio.exceptions.TimeoutError: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR.", )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.core.data_type.user_stream_tracker_data_source.UserStreamTrackerDataSource._sleep" ) def test_listen_for_user_stream_iter_message_throws_exception( self, _, mock_ws): msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.receive.side_effect = Exception("TEST ERROR") mock_ws.return_value.closed = False mock_ws.return_value.close.side_effect = Exception self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) try: self.async_run_with_timeout(msg_queue.get()) except Exception: pass self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds... Error: TEST ERROR", )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_successful(self, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, self._simulate_user_update_event()) 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.async_run_with_timeout(msg_queue.get()) self.assertTrue(msg, self._simulate_user_update_event) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_empty_payload( self, mock_api, mock_ws): mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, "") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(self.ev_loop, msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize())
class NdaxUserStreamTrackerTests(TestCase): def setUp(self) -> None: super().setUp() self.ws_sent_messages = [] self.ws_incoming_messages = asyncio.Queue() self.listening_task = None throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) auth_assistant = NdaxAuth(uid='001', api_key='testAPIKey', secret_key='testSecret', account_name="hbot") self.tracker = NdaxUserStreamTracker(throttler=throttler, auth_assistant=auth_assistant) 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 def _authentication_response(self, authenticated: bool) -> str: user = { "UserId": 492, "UserName": "******", "Email": "*****@*****.**", "EmailVerified": True, "AccountId": 528, "OMSId": 1, "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) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listening_process_authenticates_and_subscribes_to_events( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.listening_task = asyncio.get_event_loop().create_task( self.tracker.start()) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_aiohttp_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_aiohttp_message( ws_connect_mock.return_value, json.dumps('dummyMessage')) first_received_message = self.async_run_with_timeout( self.tracker.user_stream.get()) self.assertEqual('dummyMessage', first_received_message)
class HuobiAPIOrderBookDataSourceUnitTests(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}{cls.quote_asset}".lower() def setUp(self) -> None: super().setUp() self.log_records = [] self.async_tasks: List[asyncio.Task] = [] self.data_source = HuobiAPIOrderBookDataSource( trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.mocking_assistant = NetworkMockingAssistant() self.resume_test_event = asyncio.Event() def tearDown(self) -> None: for task in self.async_tasks: task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _compress(self, message: Dict[str, Any]) -> bytes: return gzip.compress(json.dumps(message).encode()) @aioresponses() def test_last_traded_prices(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.TICKER_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response: Dict[str, Any] = { "data": [ { "symbol": self.ex_trading_pair, "open": 1.1, "high": 2.0, "low": 0.8, "close": 1.5, "amount": 100, "vol": 100, "count": 100, "bid": 1.3, "bidSize": 10, "ask": 1.4, "askSize": 10, }, ], "status": "ok", "ts": 1637229769083, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_last_traded_prices( trading_pairs=[self.trading_pair])) self.assertEqual(1, len(result)) self.assertIn(self.trading_pair, result) self.assertEqual(1.5, result[self.trading_pair]) @aioresponses() def test_fetch_trading_pairs_failed(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.API_VERSION + CONSTANTS.SYMBOLS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=ujson.dumps({})) result = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(0, len(result)) @aioresponses() def test_fetch_trading_pairs_successful(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.API_VERSION + CONSTANTS.SYMBOLS_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "status": "ok", "data": [{ "base-currency": self.base_asset.lower(), "quote-currency": self.quote_asset.lower(), "price-precision": 4, "amount-precision": 2, "symbol-partition": "innovation", "symbol": self.ex_trading_pair, "state": "online", "value-precision": 8, "min-order-amt": 1, "max-order-amt": 10000000, "min-order-value": 0.1, "limit-order-min-order-amt": 1, "limit-order-max-order-amt": 10000000, "limit-order-max-buy-amt": 10000000, "limit-order-max-sell-amt": 10000000, "sell-market-min-order-amt": 1, "sell-market-max-order-amt": 1000000, "buy-market-max-order-value": 17000, "api-trading": "enabled", "tags": "abnormalmarket", }], } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.fetch_trading_pairs()) self.assertEqual(1, len(result)) @aioresponses() def test_get_snapshot_raises_error(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=ujson.dumps({})) expected_error_msg = f"Error fetching Huobi market snapshot for {self.trading_pair}. HTTP status is 400" with self.assertRaisesRegex(IOError, expected_error_msg): self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) @aioresponses() def test_get_snapshot_successful(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "status": "ok", "ts": 1637255180894, "tick": { "bids": [ [57069.57, 0.05], ], "asks": [ [57057.73, 0.007019], ], "version": 141982962388, "ts": 1637255180700, }, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_snapshot(self.trading_pair)) self.assertEqual(mock_response["ch"], result["ch"]) self.assertEqual(mock_response["status"], result["status"]) self.assertEqual(1, len(result["tick"]["bids"])) self.assertEqual(1, len(result["tick"]["asks"])) @aioresponses() def test_get_new_order_book(self, mock_api): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "status": "ok", "ts": 1637255180894, "tick": { "bids": [ [57069.57, 0.05], ], "asks": [ [57057.73, 0.007019], ], "version": 141982962388, "ts": 1637255180700, }, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) result = self.async_run_with_timeout( self.data_source.get_new_order_book(self.trading_pair)) self.assertIsInstance(result, OrderBook) self.assertEqual(1637255180700, result.snapshot_uid) self.assertEqual(1, len(list(result.bid_entries()))) self.assertEqual(1, len(list(result.ask_entries()))) self.assertEqual(57069.57, list(result.bid_entries())[0].price) self.assertEqual(0.05, list(result.bid_entries())[0].amount) self.assertEqual(57057.73, list(result.ask_entries())[0].price) self.assertEqual(0.007019, list(result.ask_entries())[0].amount) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_when_subscribing_raised_cancelled( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_subscriptions()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.huobi.huobi_api_order_book_data_source.HuobiAPIOrderBookDataSource._sleep" ) def test_listen_for_subscriptions_raises_logs_exception( self, sleep_mock, ws_connect_mock): sleep_mock.side_effect = lambda *_: ( # Allows listen_for_subscriptions to yield control over thread self.ev_loop.run_until_complete(asyncio.sleep(0.0))) ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ws_connect_mock.return_value.receive.side_effect = lambda *_: self._create_exception_and_unlock_test_with_event( Exception("TEST ERROR")) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error occurred when listening to order book streams. Retrying in 5 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_successful_subbed(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) subbed_message = { "id": self.ex_trading_pair, "status": "ok", "subbed": f"market.{self.ex_trading_pair}.depth.step0", "ts": 1637333566824, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(subbed_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.TRADE_CHANNEL_SUFFIX].qsize()) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_handle_ping_successful( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) ping_message = {"ping": 1637333569837} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(ping_message), message_type=aiohttp.WSMsgType.BINARY) # Adds a dummy message to ensure ping message is being handle before breaking from listening task. dummy_message = {"msg": "DUMMY MESSAGE"} self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(dummy_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.TRADE_CHANNEL_SUFFIX].qsize()) self.assertEqual( 0, self.data_source._message_queue[ self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize()) sent_json: List[Dict[ str, Any]] = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertTrue(any(["pong" in str(payload) for payload in sent_json])) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_subscriptions_successfully_append_trade_and_orderbook_messages( self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) trade_message = { "ch": f"market.{self.ex_trading_pair}.trade.detail", "ts": 1630994963175, "tick": { "id": 137005445109, "ts": 1630994963173, "data": [{ "id": 137005445109359286410323766, "ts": 1630994963173, "tradeId": 102523573486, "amount": 0.006754, "price": 52648.62, "direction": "buy", }], }, } orderbook_message = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "ts": 1630983549503, "tick": { "bids": [[52690.69, 0.36281], [52690.68, 0.2]], "asks": [[52690.7, 0.372591], [52691.26, 0.13]], "version": 136998124622, "ts": 1630983549500, }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(trade_message), message_type=aiohttp.WSMsgType.BINARY) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(orderbook_message), message_type=aiohttp.WSMsgType.BINARY, ) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual( 1, self.data_source._message_queue[ self.data_source.TRADE_CHANNEL_SUFFIX].qsize()) self.assertEqual( 1, self.data_source._message_queue[ self.data_source.ORDERBOOK_CHANNEL_SUFFIX].qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_logs_exception(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) trade_message = { "ch": f"market.{self.ex_trading_pair}.trade.detail", "err": "INCOMPLETE MESSAGE" } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(trade_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_trades_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) trade_message = { "ch": f"market.{self.ex_trading_pair}.trade.detail", "ts": 1630994963175, "tick": { "id": 137005445109, "ts": 1630994963173, "data": [{ "id": 137005445109359286410323766, "ts": 1630994963173, "tradeId": 102523573486, "amount": 0.006754, "price": 52648.62, "direction": "buy", }], }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(trade_message), message_type=aiohttp.WSMsgType.BINARY) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_trades(self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(1, msg_queue.qsize()) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_logs_exception(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) orderbook_message = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "err": "INCOMPLETE MESSAGE" } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(orderbook_message), message_type=aiohttp.WSMsgType.BINARY, ) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(0, msg_queue.qsize()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with WebSocket connection. Retrying after 30 seconds..." )) @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_order_book_diffs_successful(self, ws_connect_mock): ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) orderbook_message = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "ts": 1630983549503, "tick": { "bids": [[52690.69, 0.36281], [52690.68, 0.2]], "asks": [[52690.7, 0.372591], [52691.26, 0.13]], "version": 136998124622, "ts": 1630983549500, }, } self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=self._compress(orderbook_message), message_type=aiohttp.WSMsgType.BINARY, ) self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_subscriptions())) msg_queue = asyncio.Queue() self.async_tasks.append( self.ev_loop.create_task( self.data_source.listen_for_order_book_diffs( self.ev_loop, msg_queue))) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertEqual(1, msg_queue.qsize()) @aioresponses() @patch( "hummingbot.connector.exchange.huobi.huobi_api_order_book_data_source.HuobiAPIOrderBookDataSource._sleep" ) def test_listen_for_order_book_snapshots_successful(self, mock_api, _): url = CONSTANTS.REST_URL + CONSTANTS.DEPTH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "ch": f"market.{self.ex_trading_pair}.depth.step0", "status": "ok", "ts": 1637255180894, "tick": { "bids": [ [57069.57, 0.05], ], "asks": [ [57057.73, 0.007019], ], "version": 141982962388, "ts": 1637255180700, }, } mock_api.get(regex_url, body=ujson.dumps(mock_response)) mock_api.get(regex_url, exception=asyncio.CancelledError) msg_queue = asyncio.Queue() # Purposefully raised error to exit task loop with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_order_book_snapshots( self.ev_loop, msg_queue)) result = self.async_run_with_timeout(coroutine=msg_queue.get()) self.assertIsInstance(result, OrderBookMessage)
class ProbitAPIUserStreamDataSourceTest(unittest.TestCase): # logging.Level required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: cls.base_asset = "BTC" cls.quote_asset = "USDT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" def setUp(self) -> None: super().setUp() self.ev_loop = asyncio.get_event_loop() self.api_key = "someKey" self.api_secret = "someSecret" self.auth = ProbitAuth(self.api_key, self.api_secret) self.data_source = ProbitAPIUserStreamDataSource( self.auth, trading_pairs=[self.trading_pair]) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.log_records = [] self.mocking_assistant = NetworkMockingAssistant() self.async_task: Optional[asyncio.Task] = None def tearDown(self) -> None: self.async_task and self.async_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def check_is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.probit.probit_auth.ProbitAuth.get_ws_auth_payload", new_callable=AsyncMock, ) def test_listen_for_user_stream(self, get_ws_auth_payload_mock, ws_connect_mock): auth_msg = {"type": "authorization", "token": "someToken"} get_ws_auth_payload_mock.return_value = auth_msg ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, message={"result": "ok"} # authentication ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message=json.dumps({"my_msg": "test"}) # first message ) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.mocking_assistant.run_until_all_json_messages_delivered( ws_connect_mock.return_value) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertFalse(output_queue.empty()) sent_text_msgs = self.mocking_assistant.text_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(auth_msg, json.loads(sent_text_msgs[0])) sent_json_msgs = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) for sent_json_msg in sent_json_msgs: self.assertEqual("subscribe", sent_json_msg["type"]) self.assertIn(sent_json_msg["channel"], CONSTANTS.WS_PRIVATE_CHANNELS) CONSTANTS.WS_PRIVATE_CHANNELS.remove(sent_json_msg["channel"]) self.assertEqual(0, len(CONSTANTS.WS_PRIVATE_CHANNELS)) self.assertNotEqual(0, self.data_source.last_recv_time) @patch("aiohttp.client.ClientSession.ws_connect") @patch( "hummingbot.connector.exchange.probit.probit_api_user_stream_data_source.ProbitAPIUserStreamDataSource._sleep", new_callable=AsyncMock, ) def test_listen_for_user_stream_attempts_again_on_exception( self, sleep_mock, ws_connect_mock): called_event = asyncio.Event() async def _sleep(delay): called_event.set() await asyncio.sleep(delay) sleep_mock.side_effect = _sleep ws_connect_mock.side_effect = Exception self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(asyncio.Queue())) self.async_run_with_timeout(called_event.wait()) self.check_is_logged( log_level="ERROR", message= "Unexpected error with Probit WebSocket connection. Retrying after 30 seconds...", ) @patch("aiohttp.client.ClientSession.ws_connect") def test_listen_for_user_stream_stops_on_asyncio_cancelled_error( self, ws_connect_mock): ws_connect_mock.side_effect = asyncio.CancelledError with self.assertRaises(asyncio.CancelledError): self.async_run_with_timeout( self.data_source.listen_for_user_stream(asyncio.Queue())) @patch("aiohttp.client.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.probit.probit_auth.ProbitAuth.get_ws_auth_payload", new_callable=AsyncMock, ) def test_listen_for_user_stream_registers_ping_msg( self, get_ws_auth_payload_mock, ws_connect_mock): auth_msg = {"type": "authorization", "token": "someToken"} get_ws_auth_payload_mock.return_value = auth_msg ws_connect_mock.return_value = self.mocking_assistant.create_websocket_mock( ) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, message={"result": "ok"} # authentication ) self.mocking_assistant.add_websocket_aiohttp_message( ws_connect_mock.return_value, message="", message_type=WSMsgType.PING) output_queue = asyncio.Queue() self.async_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(output_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( ws_connect_mock.return_value) self.assertTrue(output_queue.empty()) ws_connect_mock.return_value.pong.assert_called()
class LatokenUserStreamDataSourceUnitTests(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 = "d8ae67f2-f954-4014-98c8-64b1ac334c64" cls.quote_asset = "0c3a106d-bde3-4c13-a26e-3fd2394529e5" cls.trading_pair = "ETH-USDT" cls.trading_pairs = [cls.trading_pair] cls.ex_trading_pair = cls.base_asset + '/' + cls.quote_asset cls.domain = "com" cls.listen_key = 'ffffffff-ffff-ffff-ffff-ffffffffff' def setUp(self) -> None: super().setUp() self.log_records = [] self.listening_task: Optional[asyncio.Task] = None self.mocking_assistant = NetworkMockingAssistant() self.throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self.mock_time_provider = MagicMock() self.mock_time_provider.time.return_value = 1000 self.auth = LatokenAuth(api_key="TEST_API_KEY", secret_key="TEST_SECRET", time_provider=self.mock_time_provider) self.time_synchronizer = TimeSynchronizer() self.time_synchronizer.add_time_offset_ms_sample(0) client_config_map = ClientConfigAdapter(ClientConfigMap()) self.connector = LatokenExchange(client_config_map=client_config_map, latoken_api_key="", latoken_api_secret="", trading_pairs=[], trading_required=False, domain=self.domain) self.connector._web_assistants_factory._auth = self.auth self.data_source = LatokenAPIUserStreamDataSource( auth=self.auth, trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory, domain=self.domain) self.data_source.logger().setLevel(1) self.data_source.logger().addHandler(self) self.resume_test_event = asyncio.Event() self.connector._set_trading_pair_symbol_map( bidict({self.ex_trading_pair: self.trading_pair})) def tearDown(self) -> None: self.listening_task and self.listening_task.cancel() super().tearDown() def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def _raise_exception(self, exception_class): raise exception_class def _create_exception_and_unlock_test_with_event(self, exception): self.resume_test_event.set() raise exception def _create_return_value_and_unlock_test_with_event(self, value): self.resume_test_event.set() return value 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 _error_response(self) -> Dict[str, Any]: resp = {"code": "ERROR CODE", "msg": "ERROR MESSAGE"} return resp def _user_update_event(self): # Balance Update, so not the initial balance return b'MESSAGE\ndestination:/user/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/v1/account\nmessage-id:9e8188c8-682c-41cd-9a14-722bf6dfd99e\ncontent-length:346\nsubscription:2\n\n{"payload":[{"id":"44d36460-46dc-4828-a17c-63b1a047b054","status":"ACCOUNT_STATUS_ACTIVE","type":"ACCOUNT_TYPE_SPOT","timestamp":1650120265819,"currency":"620f2019-33c0-423b-8a9d-cde4d7f8ef7f","available":"34.001000000000000000","blocked":"0.999000000000000000","user":"******"}],"nonce":1,"timestamp":1650120265830}\x00' def _successfully_subscribed_event(self): return b'CONNECTED\nserver:vertx-stomp/3.9.6\nheart-beat:1000,1000\nsession:37a8e962-7fa7-4eab-b163-146eeafdef63\nversion:1.1\n\n\x00 ' @aioresponses() def test_get_listen_key_log_exception(self, mock_api): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=json.dumps(self._error_response())) with self.assertRaises(IOError): self.async_run_with_timeout(self.data_source._get_listen_key()) @aioresponses() def test_get_listen_key_successful(self, mock_api): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { 'id': 'ffffffff-ffff-ffff-ffff-ffffffffff', 'status': 'ACTIVE', 'role': 'INVESTOR', 'email': '*****@*****.**', 'phone': '', 'authorities': [], 'forceChangePassword': None, 'authType': 'API_KEY', 'socials': [] } mock_api.get(regex_url, body=json.dumps(mock_response)) result: str = self.async_run_with_timeout( self.data_source._get_listen_key()) self.assertEqual(self.listen_key, result) @aioresponses() def test_ping_listen_key_log_exception(self, mock_api): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_api.get(regex_url, status=400, body=json.dumps(self._error_response())) self.data_source._current_listen_key = self.listen_key result: bool = self.async_run_with_timeout( self.data_source._ping_listen_key()) self.assertTrue( self._is_logged( "WARNING", f"Failed to refresh the listen key {self.listen_key}: {self._error_response()}" )) self.assertFalse(result) @aioresponses() def test_ping_listen_key_successful(self, mock_api): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { 'id': 'ffffffff-ffff-ffff-ffff-ffffffffff', 'status': 'ACTIVE', 'role': 'INVESTOR', 'email': '*****@*****.**', 'phone': '', 'authorities': [], 'forceChangePassword': None, 'authType': 'API_KEY', 'socials': [] } mock_api.get(regex_url, body=json.dumps(mock_response)) self.data_source._current_listen_key = self.listen_key result: bool = self.async_run_with_timeout( self.data_source._ping_listen_key()) self.assertTrue(result) @patch( "hummingbot.connector.exchange.latoken.latoken_api_user_stream_data_source.LatokenAPIUserStreamDataSource" "._ping_listen_key", new_callable=AsyncMock) def test_manage_listen_key_task_loop_keep_alive_failed( self, mock_ping_listen_key): mock_ping_listen_key.side_effect = ( lambda *args, **kwargs: self. _create_return_value_and_unlock_test_with_event(False)) self.data_source._current_listen_key = self.listen_key # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached self.data_source._last_listen_key_ping_ts = 0 self.listening_task = self.ev_loop.create_task( self.data_source._manage_listen_key_task_loop()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged("ERROR", "Error occurred renewing listen key ...")) self.assertIsNone(self.data_source._current_listen_key) self.assertFalse( self.data_source._listen_key_initialized_event.is_set()) @patch( "hummingbot.connector.exchange.latoken.latoken_api_user_stream_data_source.LatokenAPIUserStreamDataSource." "_ping_listen_key", new_callable=AsyncMock) def test_manage_listen_key_task_loop_keep_alive_successful( self, mock_ping_listen_key): mock_ping_listen_key.side_effect = ( lambda *args, **kwargs: self. _create_return_value_and_unlock_test_with_event(True)) # Simulate LISTEN_KEY_KEEP_ALIVE_INTERVAL reached self.data_source._current_listen_key = self.listen_key self.data_source._listen_key_initialized_event.set() self.data_source._last_listen_key_ping_ts = 0 self.listening_task = self.ev_loop.create_task( self.data_source._manage_listen_key_task_loop()) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged("INFO", f"Refreshed listen key {self.listen_key}.")) self.assertGreater(self.data_source._last_listen_key_ping_ts, 0) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_get_listen_key_successful_with_user_update_event( self, mock_api, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { 'id': 'ffffffff-ffff-ffff-ffff-ffffffffff', 'status': 'ACTIVE', 'role': 'INVESTOR', 'email': '*****@*****.**', 'phone': '', 'authorities': [], 'forceChangePassword': None, 'authType': 'API_KEY', 'socials': [] } mock_api.get(regex_url, body=json.dumps(mock_response)) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, self._successfully_subscribed_event(), message_type=aiohttp.WSMsgType.BINARY) self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, self._user_update_event(), message_type=aiohttp.WSMsgType.BINARY) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) msg = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(msg, self._user_update_event) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_does_not_queue_empty_payload( self, mock_api, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { 'id': 'ffffffff-ffff-ffff-ffff-ffffffffff', 'status': 'ACTIVE', 'role': 'INVESTOR', 'email': '*****@*****.**', 'phone': '', 'authorities': [], 'forceChangePassword': None, 'authType': 'API_KEY', 'socials': [] } mock_api.get(regex_url, body=json.dumps(mock_response)) mock_ws.return_value = self.mocking_assistant.create_websocket_mock() self.mocking_assistant.add_websocket_aiohttp_message( mock_ws.return_value, "") msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.mocking_assistant.run_until_all_aiohttp_messages_delivered( mock_ws.return_value) self.assertEqual(0, msg_queue.qsize()) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_connection_failed(self, mock_api, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { 'id': 'ffffffff-ffff-ffff-ffff-ffffffffff', 'status': 'ACTIVE', 'role': 'INVESTOR', 'email': '*****@*****.**', 'phone': '', 'authorities': [], 'forceChangePassword': None, 'authType': 'API_KEY', 'socials': [] } mock_api.get(regex_url, body=json.dumps(mock_response)) mock_ws.side_effect = lambda *arg, **kwars: self._create_exception_and_unlock_test_with_event( Exception("TEST ERROR.")) msg_queue = asyncio.Queue() self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." )) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) def test_listen_for_user_stream_iter_message_throws_exception( self, mock_api, mock_ws): url = web_utils.private_rest_url(path_url=CONSTANTS.USER_ID_PATH_URL, domain=self.domain) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { 'id': 'ffffffff-ffff-ffff-ffff-ffffffffff', 'status': 'ACTIVE', 'role': 'INVESTOR', 'email': '*****@*****.**', 'phone': '', 'authorities': [], 'forceChangePassword': None, 'authType': 'API_KEY', 'socials': [] } mock_api.get(regex_url, body=json.dumps(mock_response)) msg_queue: asyncio.Queue = asyncio.Queue() mock_ws.return_value = self.mocking_assistant.create_websocket_mock() mock_ws.return_value.receive.side_effect = ( lambda *args, **kwargs: self. _create_exception_and_unlock_test_with_event( Exception("TEST ERROR"))) mock_ws.close.return_value = None self.listening_task = self.ev_loop.create_task( self.data_source.listen_for_user_stream(msg_queue)) self.async_run_with_timeout(self.resume_test_event.wait()) self.assertTrue( self._is_logged( "ERROR", "Unexpected error while listening to user stream. Retrying after 5 seconds..." ))
class BybitPerpetualUserStreamDataSourceTests(TestCase): # the level is required to receive logs from the data source loger level = 0 def setUp(self) -> None: super().setUp() self.api_key = 'testAPIKey' self.secret = 'testSecret' self.log_records = [] self.listening_task = None self.data_source = BybitPerpetualUserStreamDataSource( auth_assistant=BybitPerpetualAuth(api_key=self.api_key, secret_key=self.secret)) 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() if self.data_source._session is not None: asyncio.get_event_loop().run_until_complete( self.data_source._session.close()) 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: request = { "op": "auth", "args": ['testAPIKey', 'testExpires', 'testSignature'] } message = { "success": authenticated, "ret_msg": "", "conn_id": "testConnectionID", "request": request } return message def _subscription_response(self, subscribed: bool, subscription: str) -> str: request = {"op": "subscribe", "args": [subscription]} message = { "success": subscribed, "ret_msg": "", "conn_id": "testConnectionID", "request": request } return message def _raise_exception(self, exception_class): raise exception_class @patch('aiohttp.ClientSession.ws_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_on_url( "test_url", messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._subscription_response( True, CONSTANTS.WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME)) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._subscription_response( True, CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME)) self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._subscription_response( True, CONSTANTS.WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME)) # Add a dummy message for the websocket to read and include in the "messages" queue self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, json.dumps('dummyMessage')) asyncio.get_event_loop().run_until_complete(messages.get()) 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', "Successful subscription to the topic ['position'] on test_url" )) self.assertTrue( self._is_logged( "INFO", "Successful subscription to the topic ['order'] on test_url")) self.assertTrue( self._is_logged( "INFO", "Successful subscription to the topic ['execution'] on test_url" )) sent_messages = self.mocking_assistant.json_messages_sent_through_websocket( ws_connect_mock.return_value) self.assertEqual(4, len(sent_messages)) authentication_request = sent_messages[0] subscription_positions_request = sent_messages[1] subscription_orders_request = sent_messages[2] subscription_executions_request = sent_messages[3] self.assertEqual( CONSTANTS.WS_AUTHENTICATE_USER_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( authentication_request)) self.assertEqual( CONSTANTS.WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( subscription_positions_request)) self.assertEqual( CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( subscription_orders_request)) self.assertEqual( CONSTANTS.WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME, BybitPerpetualWebSocketAdaptor.endpoint_from_message( subscription_executions_request)) subscription_positions_payload = BybitPerpetualWebSocketAdaptor.payload_from_message( subscription_positions_request) expected_payload = {"op": "subscribe", "args": ["position"]} self.assertEqual(expected_payload, subscription_positions_payload) subscription_orders_payload = BybitPerpetualWebSocketAdaptor.payload_from_message( subscription_orders_request) expected_payload = {"op": "subscribe", "args": ["order"]} self.assertEqual(expected_payload, subscription_orders_payload) subscription_executions_payload = BybitPerpetualWebSocketAdaptor.payload_from_message( subscription_executions_request) expected_payload = {"op": "subscribe", "args": ["execution"]} self.assertEqual(expected_payload, subscription_executions_payload) self.assertGreater(self.data_source.last_recv_time, initial_last_recv_time) @patch('aiohttp.ClientSession.ws_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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_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 Bybit Perpetual)" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. " "(Could not authenticate websocket connection with Bybit Perpetual)" )) @patch('aiohttp.ClientSession.ws_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(messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_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_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.WS_AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ "op"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_positions_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_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS. WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_orders_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_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_canceled_when_cancel_exception_during_executions_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_json.side_effect = lambda sent_message: ( self._raise_exception(asyncio.CancelledError) if CONSTANTS. WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, sent_message)) with self.assertRaises(asyncio.CancelledError): self.listening_task = asyncio.get_event_loop().create_task( self.data_source.listen_for_user_stream(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, self._authentication_response(True)) asyncio.get_event_loop().run_until_complete(self.listening_task) @patch('aiohttp.ClientSession.ws_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._create_websocket_connection("test_url")) asyncio.get_event_loop().run_until_complete(self.listening_task) self.assertTrue( self._is_logged( "NETWORK", "Unexpected error occurred during bybit_perpetual WebSocket Connection on test_url ()" )) @patch('aiohttp.ClientSession.ws_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_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.WS_AUTHENTICATE_USER_ENDPOINT_NAME in sent_message[ "op"] else self.mocking_assistant.add_websocket_json_message( 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(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 Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_positions_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_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS. WS_SUBSCRIPTION_POSITIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, 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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_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 bybit_perpetual private channels ()" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_orders_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_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS.WS_SUBSCRIPTION_ORDERS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, 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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_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 bybit_perpetual private channels ()" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" )) @patch('aiohttp.ClientSession.ws_connect', new_callable=AsyncMock) def test_listening_process_logs_exception_during_executions_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_json.side_effect = lambda sent_message: ( self._raise_exception(Exception) if CONSTANTS. WS_SUBSCRIPTION_EXECUTIONS_ENDPOINT_NAME in sent_message[ "args"] else self.mocking_assistant.add_websocket_json_message( ws_connect_mock.return_value, 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(messages)) # Add the authentication response for the websocket self.mocking_assistant.add_websocket_json_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 bybit_perpetual private channels ()" )) self.assertTrue( self._is_logged( "ERROR", "Unexpected error with Bybit Perpetual WebSocket connection on" " wss://stream.bybit.com/realtime_private. Retrying in 30 seconds. ()" ))