예제 #1
0
    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}))
예제 #2
0
    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)
예제 #4
0
    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)
예제 #5
0
    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())
예제 #6
0
    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
예제 #8
0
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())