def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task = None self.mocking_assistant = NetworkMockingAssistant() 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.ob_data_source = KucoinAPIOrderBookDataSource( trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory) self.ob_data_source.logger().setLevel(1) self.ob_data_source.logger().addHandler(self) self.connector._set_trading_pair_symbol_map( bidict({self.trading_pair: self.trading_pair}))
def setUp(self) -> None: super().setUp() self.exchange = KucoinExchange( self.api_key, self.api_passphrase, self.api_secret_key, trading_pairs=[self.trading_pair] ) self.order_filled_logger = EventLogger() self.buy_order_completed_logger = EventLogger() self.exchange.add_listener(MarketEvent.OrderFilled, self.order_filled_logger) self.exchange.add_listener(MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger)
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 _kucoin_connector_without_private_keys(cls) -> 'KucoinExchange': from hummingbot.connector.exchange.kucoin.kucoin_exchange import KucoinExchange client_config_map = cls._get_client_config_map() return KucoinExchange(client_config_map=client_config_map, kucoin_api_key="", kucoin_passphrase="", kucoin_secret_key="", trading_pairs=[], trading_required=False)
def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, [ "/api/v1/timestamp", "/api/v1/symbols", "/api/v1/bullet-public", "/api/v2/market/orderbook/level2" ]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/api/v1/accounts", FixtureKucoin.BALANCES) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.kucoin.kucoin_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls._exch_order_id = 20001 cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"]) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"]) cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready())
def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: float = self.market.get_price(trading_pair, True) bid_price: Decimal = Decimal(current_bid_price * Decimal(0.8)) quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) amount: Decimal = Decimal(0.04) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_BUY_LIMIT_ORDER) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) if API_MOCK_ENABLED: resp = FixtureKucoin.CANCEL_ORDER.copy() resp["data"]["cancelledOrderIds"] = exch_order_id self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = FixtureKucoin.GET_CANCELLED_ORDER.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) self.market_logger.clear()
class TestKucoinExchange(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.exchange = KucoinExchange(self.api_key, self.api_passphrase, self.api_secret_key, trading_pairs=[self.trading_pair]) self.order_filled_logger = EventLogger() self.buy_order_completed_logger = EventLogger() self.exchange.add_listener(MarketEvent.OrderFilled, self.order_filled_logger) self.exchange.add_listener(MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger) 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_accounts_data_mock(self) -> Dict: acc_data = { "code": "200000", "data": [ { "id": "someId1", "currency": self.base_asset, "type": "trade", "balance": "81.8446241", "available": "81.8446241", "holds": "0", }, { "id": "someId2", "currency": self.quote_asset, "type": "trade", "balance": "41.3713", "available": "41.3713", "holds": "0", }, ], } return acc_data def get_exchange_rules_mock(self) -> Dict: exchange_rules = { "code": "200000", "data": [ { "symbol": self.trading_pair, "name": self.trading_pair, "baseCurrency": self.base_asset, "quoteCurrency": self.quote_asset, "feeCurrency": self.quote_asset, "market": "ALTS", "baseMinSize": "1", "quoteMinSize": "0.1", "baseMaxSize": "10000000000", "quoteMaxSize": "99999999", "baseIncrement": "0.1", "quoteIncrement": "0.01", "priceIncrement": "0.01", "priceLimitRate": "0.1", "isMarginEnabled": False, "enableTrading": True, }, ], } return exchange_rules def get_in_flight_order_mock(self, order_id: str, exchange_id: str) -> KucoinInFlightOrder: order = KucoinInFlightOrder( client_order_id=order_id, exchange_order_id=exchange_id, trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal("10.0"), amount=Decimal("1"), ) return order def get_order_response_mock(self, size: float, filled: float) -> Dict[str, Any]: order_response = { "data": { "id": "5c35c02703aa673ceec2a168", "symbol": self.trading_pair, "opType": "DEAL", "type": "limit", "side": "buy", "price": "10", "size": str(size), "funds": "0", "dealFunds": "0.166", "dealSize": str(filled), "fee": "0", "feeCurrency": "USDT", "stp": "", "stop": "", "stopTriggered": False, "stopPrice": "0", "timeInForce": "GTC", "postOnly": False, "hidden": False, "iceberg": False, "visibleSize": "0", "cancelAfter": 0, "channel": "IOS", "clientOid": "", "remark": "", "tags": "", "isActive": False, "cancelExist": False, "createdAt": 1547026471000, "tradeType": "TRADE" } } return order_response @staticmethod def get_cancel_response(exchange_id: str) -> Dict: cancel_response = { "code": "200000", "data": { "cancelledOrderIds": [exchange_id], } } return cancel_response @aioresponses() def test_check_network_success(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.SERVER_TIME_PATH_URL resp = time.time() mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.exchange.check_network()) self.assertEqual(ret, NetworkStatus.CONNECTED) @aioresponses() def test_check_network_failure(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.SERVER_TIME_PATH_URL mock_api.get(url, status=500) ret = self.async_run_with_timeout( coroutine=self.exchange.check_network()) self.assertEqual(ret, NetworkStatus.NOT_CONNECTED) @aioresponses() def test_update_balances(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ACCOUNTS_PATH_URL resp = self.get_accounts_data_mock() mock_api.get(url, body=json.dumps(resp)) self.async_run_with_timeout(self.exchange._update_balances()) self.assertTrue(self.quote_asset in self.exchange.available_balances) @aioresponses() def test_update_trading_rules(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.SYMBOLS_PATH_URL resp = self.get_exchange_rules_mock() mock_api.get(url, body=json.dumps(resp)) self.async_run_with_timeout( coroutine=self.exchange._update_trading_rules()) self.assertTrue(self.trading_pair in self.exchange.trading_rules) @aioresponses() def test_get_order_status(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = "someStatus" mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.exchange.get_order_status( exchange_order_id="someId")) self.assertEqual(resp, ret) @aioresponses() def test_place_order(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL resp = { "code": "200000", "data": { "orderId": "someId", } } call_inputs = [] def callback(*args, **kwargs): call_inputs.append((args, kwargs)) mock_api.post(url, body=json.dumps(resp), callback=callback) amount = Decimal("1") price = Decimal("10.0") ret = self.async_run_with_timeout(coroutine=self.exchange.place_order( order_id="internalId", trading_pair=self.trading_pair, amount=amount, is_buy=True, order_type=OrderType.LIMIT, price=price, )) self.assertEqual(ret, resp["data"]["orderId"]) call_kwargs = call_inputs[0][1] call_data = call_kwargs["data"] expected_data = json.dumps({ "size": str(amount), "clientOid": "internalId", "side": "buy", "symbol": self.trading_pair, "type": "limit", "price": str(price), }) self.assertEqual(call_data, expected_data) @aioresponses() def test_execute_cancel(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) called = asyncio.Event() def callback(*args, **kwargs): called.set() exchange_id = "someId" resp = self.get_cancel_response(exchange_id=exchange_id) mock_api.delete(regex_url, body=json.dumps(resp), callback=callback) order_id = "internalId" order = self.get_in_flight_order_mock(order_id, exchange_id=exchange_id) order.last_state = "DEAL" self.exchange.in_flight_orders[order_id] = order self.async_run_with_timeout(coroutine=self.exchange.execute_cancel( self.trading_pair, order_id)) self.assertTrue(called.is_set()) @aioresponses() def test_cancel_all(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) called1 = asyncio.Event() called2 = asyncio.Event() def callback(ev, *args, **kwargs): ev.set() order_id1 = "internalId1" order_id2 = "internalId2" exchange_id1 = "someId1" exchange_id2 = "someId2" resp1 = self.get_cancel_response(exchange_id1) resp2 = self.get_cancel_response(exchange_id2) mock_api.delete(regex_url, body=json.dumps(resp1), callback=partial(callback, called1)) mock_api.delete(regex_url, body=json.dumps(resp2), callback=partial(callback, called2)) self.exchange.in_flight_orders[ order_id1] = self.get_in_flight_order_mock( order_id1, exchange_id=exchange_id1) self.exchange.in_flight_orders[ order_id2] = self.get_in_flight_order_mock( order_id2, exchange_id=exchange_id2) self.async_run_with_timeout(coroutine=self.exchange.cancel_all( timeout_seconds=1)) self.assertTrue(called1.is_set()) self.assertTrue(called2.is_set()) @aioresponses() def test_update_order_status_notifies_on_order_filled(self, mocked_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}") resp = self.get_order_response_mock(size=2, filled=2) mocked_api.get(regex_url, body=json.dumps(resp)) clock = Clock( ClockMode.BACKTEST, start_time=self.exchange.UPDATE_ORDERS_INTERVAL, end_time=self.exchange.UPDATE_ORDERS_INTERVAL * 2, ) TimeIterator.start(self.exchange, clock) order_id = "someId" exchange_id = "someExchangeId" self.exchange.in_flight_orders[ order_id] = self.get_in_flight_order_mock(order_id, exchange_id) order = self.exchange.in_flight_orders[order_id] self.async_run_with_timeout(self.exchange._update_order_status()) orders_filled_events = self.order_filled_logger.event_log order_completed_events = self.buy_order_completed_logger.event_log self.assertTrue(order.is_done) self.assertEqual(1, len(order_completed_events)) self.assertEqual(1, len(orders_filled_events)) self.assertEqual(order_id, order_completed_events[0].order_id) self.assertEqual(order_id, orders_filled_events[0].order_id) @aioresponses() def test_update_order_status_skips_if_order_no_longer_tracked( self, mocked_api): order_id = "someId" exchange_id = "someExchangeId" url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}") resp = self.get_order_response_mock(size=2, filled=2) mocked_api.get( regex_url, body=json.dumps(resp), callback=lambda *_, **__: self.exchange.stop_tracking_order( order_id), ) clock = Clock( ClockMode.BACKTEST, start_time=self.exchange.UPDATE_ORDERS_INTERVAL, end_time=self.exchange.UPDATE_ORDERS_INTERVAL * 2, ) TimeIterator.start(self.exchange, clock) self.exchange.in_flight_orders[ order_id] = self.get_in_flight_order_mock(order_id, exchange_id) self.async_run_with_timeout(self.exchange._update_order_status()) orders_filled_events = self.order_filled_logger.event_log order_completed_events = self.buy_order_completed_logger.event_log self.assertEqual(0, len(order_completed_events)) self.assertEqual(0, len(orders_filled_events)) @aioresponses() def test_get_fee_defaults_on_not_found(self, mocked_api): url = KUCOIN_ROOT_API + CONSTANTS.FEE_PATH_URL regex_url = re.compile(f"^{url}") resp = {"data": [{"makerFeeRate": "0.002", "takerFeeRate": "0.002"}]} mocked_api.get(regex_url, body=json.dumps(resp)) self.async_run_with_timeout(self.exchange._update_trading_fees()) fee = self.exchange.get_fee( base_currency=self.base_asset, quote_currency=self.quote_asset, order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal("10"), price=Decimal("20"), ) self.assertEqual(Decimal("0.002"), fee.percent) fee = self.exchange.get_fee( base_currency="SOME", quote_currency="OTHER", order_type=OrderType.LIMIT, order_side=TradeType.BUY, amount=Decimal("10"), price=Decimal("20"), ) self.assertEqual(Decimal("0.001"), fee.percent) # default fee
class TestKucoinAPIOrderBookDataSource(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.ws_endpoint = "ws://someEndpoint" def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task = None self.mocking_assistant = NetworkMockingAssistant() 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.ob_data_source = KucoinAPIOrderBookDataSource( trading_pairs=[self.trading_pair], connector=self.connector, api_factory=self.connector._web_assistants_factory) self.ob_data_source.logger().setLevel(1) self.ob_data_source.logger().addHandler(self) self.connector._set_trading_pair_symbol_map( bidict({self.trading_pair: self.trading_pair})) 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 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_snapshot_mock() -> Dict: snapshot = { "code": "200000", "data": { "time": 1630556205455, "sequence": "1630556205456", "bids": [["0.3003", "4146.5645"]], "asks": [["0.3004", "1553.6412"]] } } return snapshot @aioresponses() def test_get_new_order_book(self, mock_api): url = web_utils.public_rest_url( path_url=CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = self.get_snapshot_mock() 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(0.3003, bid_entries[0].price) self.assertEqual(4146.5645, bid_entries[0].amount) self.assertEqual(int(resp["data"]["sequence"]), bid_entries[0].update_id) self.assertEqual(1, len(ask_entries)) self.assertEqual(0.3004, ask_entries[0].price) self.assertEqual(1553.6412, ask_entries[0].amount) self.assertEqual(int(resp["data"]["sequence"]), ask_entries[0].update_id) @aioresponses() @patch("aiohttp.ClientSession.ws_connect", new_callable=AsyncMock) @patch( "hummingbot.connector.exchange.kucoin.kucoin_web_utils.next_message_id" ) def test_listen_for_subscriptions_subscribes_to_trades_and_order_diffs( self, mock_api, id_mock, ws_connect_mock): id_mock.side_effect = [1, 2] url = web_utils.public_rest_url( path_url=CONSTANTS.PUBLIC_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)) 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 = { "id": 1, "type": "subscribe", "topic": f"/market/match:{self.trading_pair}", "privateChannel": False, "response": False } self.assertEqual(expected_trade_subscription, sent_subscription_messages[0]) expected_diff_subscription = { "id": 2, "type": "subscribe", "topic": f"/market/level2:{self.trading_pair}", "privateChannel": False, "response": False } self.assertEqual(expected_diff_subscription, sent_subscription_messages[1]) self.assertTrue( self._is_logged( "INFO", "Subscribed to public order book and trade channels...")) @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_order_book_data_source.KucoinAPIOrderBookDataSource._time" ) def test_listen_for_subscriptions_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.public_rest_url( path_url=CONSTANTS.PUBLIC_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)) 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 = { "id": 3, "type": "ping", } self.assertEqual(expected_ping_message, sent_messages[-1]) @aioresponses() @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, mock_api, _, ws_connect_mock): url = web_utils.public_rest_url( path_url=CONSTANTS.PUBLIC_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.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) @aioresponses() @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, mock_api, sleep_mock, ws_connect_mock): url = web_utils.public_rest_url( path_url=CONSTANTS.PUBLIC_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)) 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[ self.ob_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.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 = { "type": "message", "topic": f"/market/match:{self.trading_pair}", } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.ob_data_source._message_queue[ self.ob_data_source._trade_messages_queue_key] = 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 = { "type": "message", "topic": f"/market/match:{self.trading_pair}", "subject": "trade.l3match", "data": { "sequence": "1545896669145", "type": "match", "symbol": self.trading_pair, "side": "buy", "price": "0.08200000000000000000", "size": "0.01022222000000000000", "tradeId": "5c24c5da03aa673885cd67aa", "takerOrderId": "5c24c5d903aa6772d55b371e", "makerOrderId": "5c2187d003aa677bd09d5c93", "time": "1545913818099033203" } } mock_queue.get.side_effect = [trade_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ self.ob_data_source._trade_messages_queue_key] = 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)) msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get()) self.assertTrue(trade_event["data"]["tradeId"], 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[ self.ob_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.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 = { "type": "message", "topic": f"/market/level2:{self.trading_pair}", } mock_queue = AsyncMock() mock_queue.get.side_effect = [ incomplete_resp, asyncio.CancelledError() ] self.ob_data_source._message_queue[ self.ob_data_source._diff_messages_queue_key] = 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 = { "type": "message", "topic": "/market/level2:BTC-USDT", "subject": "trade.l2update", "data": { "sequenceStart": 1545896669105, "sequenceEnd": 1545896669106, "symbol": f"{self.trading_pair}", "changes": { "asks": [["6", "1", "1545896669105"]], "bids": [["4", "1", "1545896669106"]] } } } mock_queue.get.side_effect = [diff_event, asyncio.CancelledError()] self.ob_data_source._message_queue[ self.ob_data_source._diff_messages_queue_key] = 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"]["sequenceEnd"], msg.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.SNAPSHOT_NO_AUTH_PATH_URL) 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.ob_data_source.listen_for_order_book_snapshots( self.ev_loop, asyncio.Queue())) @aioresponses() @patch( "hummingbot.connector.exchange.kucoin.kucoin_api_order_book_data_source" ".KucoinAPIOrderBookDataSource._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 = asyncio.CancelledError url = web_utils.public_rest_url( path_url=CONSTANTS.SNAPSHOT_NO_AUTH_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", 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.SNAPSHOT_NO_AUTH_PATH_URL) regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) snapshot_data = { "code": "200000", "data": { "sequence": "3262786978", "time": 1550653727731, "bids": [["6500.12", "0.45054140"], ["6500.11", "0.45054140"]], "asks": [["6500.16", "0.57753524"], ["6500.15", "0.57753524"]] } } 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["data"]["sequence"]), msg.update_id)
def setUp(self) -> None: super().setUp() self.exchange = KucoinExchange(self.api_key, self.api_passphrase, self.api_secret_key, trading_pairs=[self.trading_pair])
class TestKucoinExchange(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "someKey" cls.api_passphrase = "somePassPhrase" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.exchange = KucoinExchange(self.api_key, self.api_passphrase, self.api_secret_key, 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_accounts_data_mock(self) -> Dict: acc_data = { "code": "200000", "data": [ { "id": "someId1", "currency": self.base_asset, "type": "trade", "balance": "81.8446241", "available": "81.8446241", "holds": "0", }, { "id": "someId2", "currency": self.quote_asset, "type": "trade", "balance": "41.3713", "available": "41.3713", "holds": "0", }, ], } return acc_data def get_exchange_rules_mock(self) -> Dict: exchange_rules = { "code": "200000", "data": [ { "symbol": self.trading_pair, "name": self.trading_pair, "baseCurrency": self.base_asset, "quoteCurrency": self.quote_asset, "feeCurrency": self.quote_asset, "market": "ALTS", "baseMinSize": "1", "quoteMinSize": "0.1", "baseMaxSize": "10000000000", "quoteMaxSize": "99999999", "baseIncrement": "0.1", "quoteIncrement": "0.01", "priceIncrement": "0.01", "priceLimitRate": "0.1", "isMarginEnabled": False, "enableTrading": True, }, ], } return exchange_rules def get_in_flight_order_mock(self, order_id: str, exchange_id: str) -> KucoinInFlightOrder: order = KucoinInFlightOrder( client_order_id=order_id, exchange_order_id=exchange_id, trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal("10.0"), amount=Decimal("1"), ) return order @staticmethod def get_cancel_response(exchange_id: str) -> Dict: cancel_response = { "code": "200000", "data": { "cancelledOrderIds": [exchange_id], } } return cancel_response @aioresponses() def test_check_network_success(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.SERVER_TIME_PATH_URL resp = time.time() mock_api.get(url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.exchange.check_network()) self.assertEqual(ret, NetworkStatus.CONNECTED) @aioresponses() def test_check_network_failure(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.SERVER_TIME_PATH_URL mock_api.get(url, status=500) ret = self.async_run_with_timeout( coroutine=self.exchange.check_network()) self.assertEqual(ret, NetworkStatus.NOT_CONNECTED) @aioresponses() def test_update_balances(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ACCOUNTS_PATH_URL resp = self.get_accounts_data_mock() mock_api.get(url, body=json.dumps(resp)) self.async_run_with_timeout(self.exchange._update_balances()) self.assertTrue(self.quote_asset in self.exchange.available_balances) @aioresponses() def test_update_trading_rules(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.SYMBOLS_PATH_URL resp = self.get_exchange_rules_mock() mock_api.get(url, body=json.dumps(resp)) self.async_run_with_timeout( coroutine=self.exchange._update_trading_rules()) self.assertTrue(self.trading_pair in self.exchange.trading_rules) @aioresponses() def test_get_order_status(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) resp = "someStatus" mock_api.get(regex_url, body=json.dumps(resp)) ret = self.async_run_with_timeout( coroutine=self.exchange.get_order_status( exchange_order_id="someId")) self.assertEqual(resp, ret) @aioresponses() def test_place_order(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL resp = { "code": "200000", "data": { "orderId": "someId", } } call_inputs = [] def callback(*args, **kwargs): call_inputs.append((args, kwargs)) mock_api.post(url, body=json.dumps(resp), callback=callback) amount = Decimal("1") price = Decimal("10.0") ret = self.async_run_with_timeout(coroutine=self.exchange.place_order( order_id="internalId", trading_pair=self.trading_pair, amount=amount, is_buy=True, order_type=OrderType.LIMIT, price=price, )) self.assertEqual(ret, resp["data"]["orderId"]) call_kwargs = call_inputs[0][1] call_data = call_kwargs["data"] expected_data = json.dumps({ "size": str(amount), "clientOid": "internalId", "side": "buy", "symbol": self.trading_pair, "type": "limit", "price": str(price), }) self.assertEqual(call_data, expected_data) @aioresponses() def test_execute_cancel(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) called = asyncio.Event() def callback(*args, **kwargs): called.set() exchange_id = "someId" resp = self.get_cancel_response(exchange_id=exchange_id) mock_api.delete(regex_url, body=json.dumps(resp), callback=callback) order_id = "internalId" order = self.get_in_flight_order_mock(order_id, exchange_id=exchange_id) order.last_state = "DEAL" self.exchange.in_flight_orders[order_id] = order self.async_run_with_timeout(coroutine=self.exchange.execute_cancel( self.trading_pair, order_id)) self.assertTrue(called.is_set()) @aioresponses() def test_cancel_all(self, mock_api): url = KUCOIN_ROOT_API + CONSTANTS.ORDERS_PATH_URL regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) called1 = asyncio.Event() called2 = asyncio.Event() def callback(ev, *args, **kwargs): ev.set() order_id1 = "internalId1" order_id2 = "internalId2" exchange_id1 = "someId1" exchange_id2 = "someId2" resp1 = self.get_cancel_response(exchange_id1) resp2 = self.get_cancel_response(exchange_id2) mock_api.delete(regex_url, body=json.dumps(resp1), callback=partial(callback, called1)) mock_api.delete(regex_url, body=json.dumps(resp2), callback=partial(callback, called2)) self.exchange.in_flight_orders[ order_id1] = self.get_in_flight_order_mock( order_id1, exchange_id=exchange_id1) self.exchange.in_flight_orders[ order_id2] = self.get_in_flight_order_mock( order_id2, exchange_id=exchange_id2) self.async_run_with_timeout(coroutine=self.exchange.cancel_all( timeout_seconds=1)) self.assertTrue(called1.is_set()) self.assertTrue(called2.is_set())