Example #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}))
Example #2
0
 def setUp(self) -> None:
     super().setUp()
     self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
     self.auth = KucoinAuth(self.api_key, self.api_passphrase,
                            self.api_secret_key)
     self.ob_data_source = KucoinAPIOrderBookDataSource(
         self.throttler, [self.trading_pair], self.auth)
Example #3
0
    def test_trade_event_symbol_transformed_correctly(self, mock_api,
                                                      ws_connect_mock):
        base_asset = "WAXP"
        quote_asset = "USDT"

        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PUBLIC_WS_DATA_PATH_URL
        resp_data = {
            "data": {
                "instanceServers": [{
                    "endpoint": self.ws_endpoint,
                }],
                "token": "someToken",
            }
        }
        mock_api.post(url, body=json.dumps(resp_data))

        ws_mock = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.__aenter__.return_value = ws_mock

        data_source = KucoinAPIOrderBookDataSource(
            self.throttler, [f"_{base_asset}-{quote_asset}"])
        received_messages = asyncio.Queue()

        response = {
            "type": "message",
            "topic": "/market/match:WAX-USDT",
            "subject": "trade.l3match",
            "data": {
                "sequence": "1545896669145",
                "type": "match",
                "symbol": "WAX-USDT",
                "side": "buy",
                "price": "0.08200000000000000000",
                "size": "0.01022222000000000000",
                "tradeId": "5c24c5da03aa673885cd67aa",
                "takerOrderId": "5c24c5d903aa6772d55b371e",
                "makerOrderId": "5c2187d003aa677bd09d5c93",
                "time": "1545913818099033203"
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_mock, message=json.dumps(response))

        self.listening_task = asyncio.get_event_loop().create_task(
            data_source.listen_for_trades(asyncio.get_event_loop(),
                                          received_messages))

        trade_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.TRADE, trade_message.type)
        self.assertEqual(-1, trade_message.update_id)
        self.assertEqual(response["data"]["tradeId"], trade_message.trade_id)
        self.assertEqual(f"{base_asset}-{quote_asset}",
                         trade_message.trading_pair)
Example #4
0
    def test_order_book_diff_symbol_transformed_correctly(
            self, mock_api, ws_connect_mock):
        base_asset = "WAXP"
        quote_asset = "USDT"

        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PUBLIC_WS_DATA_PATH_URL
        resp_data = {
            "data": {
                "instanceServers": [{
                    "endpoint": self.ws_endpoint,
                }],
                "token": "someToken",
            }
        }
        mock_api.post(url, body=json.dumps(resp_data))

        ws_mock = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.__aenter__.return_value = ws_mock

        data_source = KucoinAPIOrderBookDataSource(
            self.throttler, [f"_{base_asset}-{quote_asset}"])
        received_messages = asyncio.Queue()

        diff_response = {
            "type": "message",
            "topic": "/market/level2:WAX-USDT",
            "subject": "trade.l2update",
            "data": {
                "sequenceStart": 1545896669105,
                "sequenceEnd": 1545896669106,
                "symbol": "WAX-USDT",
                "changes": {
                    "asks": [["0.1997", "1000", "1545896669105"]],
                    "bids": [["0.1993", "2000", "1545896669106"]]
                }
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_mock, message=json.dumps(diff_response))

        self.listening_task = asyncio.get_event_loop().create_task(
            data_source.listen_for_order_book_diffs(asyncio.get_event_loop(),
                                                    received_messages))

        diff_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.DIFF, diff_message.type)
        self.assertEqual(diff_response["data"]["sequenceStart"],
                         diff_message.first_update_id)
        self.assertEqual(f"{base_asset}-{quote_asset}",
                         diff_message.trading_pair)
Example #5
0
 def _create_order_book_data_source(self) -> OrderBookTrackerDataSource:
     return KucoinAPIOrderBookDataSource(
         trading_pairs=self._trading_pairs,
         connector=self,
         api_factory=self._web_assistants_factory,
         domain=self.domain,
     )
    def test_fetch_trading_pairs(self, mock_api):
        KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {}
        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL)

        resp = {
            "data": [{
                "symbol": self.trading_pair,
                "name": self.trading_pair,
                "baseCurrency": self.base_asset,
                "quoteCurrency": self.quote_asset,
                "enableTrading": True,
            }, {
                "symbol": "SOME-PAIR",
                "name": "SOME-PAIR",
                "baseCurrency": "SOME",
                "quoteCurrency": "PAIR",
                "enableTrading": False,
            }]
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.fetch_trading_pairs(
                throttler=self.throttler,
                time_synchronizer=self.time_synchronnizer,
            ))

        self.assertEqual(1, len(ret))
        self.assertEqual(self.trading_pair, ret[0])
 def test_api_get_last_traded_prices(self):
     prices = self.ev_loop.run_until_complete(
         KucoinAPIOrderBookDataSource.get_last_traded_prices(["BTC-USDT", "LTC-USDT"]))
     for key, value in prices.items():
         print(f"{key} last_trade_price: {value}")
     self.assertGreater(prices["BTC-USDT"], 1000)
     self.assertLess(prices["LTC-USDT"], 1000)
Example #8
0
 def status_dict(self) -> Dict[str, bool]:
     return {
         "symbols_mapping_initialized": KucoinAPIOrderBookDataSource.trading_pair_symbol_map_ready(
             domain=self._domain),
         "order_books_initialized": self._order_book_tracker.ready,
         "account_balance": not self._trading_required or len(self._account_balances) > 0,
         "trading_rule_initialized": len(self._trading_rules) > 0,
         "user_stream_initialized":
             self._user_stream_tracker.data_source.last_recv_time > 0 if self._trading_required else True,
     }
Example #9
0
 def __init__(self,
              throttler: Optional[AsyncThrottler] = None,
              trading_pairs: Optional[List[str]] = None,
              auth: Optional[KucoinAuth] = None):
     super().__init__(KucoinAPIOrderBookDataSource(throttler, trading_pairs, auth), trading_pairs)
     self._auth = auth
     self._order_book_diff_stream: asyncio.Queue = asyncio.Queue()
     self._order_book_snapshot_stream: asyncio.Queue = asyncio.Queue()
     self._ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
     self._saved_message_queues: Dict[str, Deque[KucoinOrderBookMessage]] = defaultdict(lambda: deque(maxlen=1000))
     self._active_order_trackers: Dict[str, KucoinActiveOrderTracker] = defaultdict(KucoinActiveOrderTracker)
Example #10
0
 def __init__(self, trading_pairs: List[str]):
     super().__init__(KucoinAPIOrderBookDataSource(trading_pairs),
                      trading_pairs)
     self._order_book_diff_stream: asyncio.Queue = asyncio.Queue()
     self._order_book_snapshot_stream: asyncio.Queue = asyncio.Queue()
     self._ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
     self._saved_message_queues: Dict[
         str, Deque[KucoinOrderBookMessage]] = defaultdict(
             lambda: deque(maxlen=1000))
     self._active_order_trackers: Dict[
         str,
         KucoinActiveOrderTracker] = defaultdict(KucoinActiveOrderTracker)
    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.async_task = None

        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.time_synchronnizer = TimeSynchronizer()
        self.time_synchronnizer.add_time_offset_ms_sample(1000)
        self.ob_data_source = KucoinAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            throttler=self.throttler,
            time_synchronizer=self.time_synchronnizer)

        self.ob_data_source.logger().setLevel(1)
        self.ob_data_source.logger().addHandler(self)

        KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {
            CONSTANTS.DEFAULT_DOMAIN:
            bidict({self.trading_pair: self.trading_pair})
        }
Example #12
0
    def test_get_snapshot_raises(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, status=500)

        client = self.ev_loop.run_until_complete(
            aiohttp.ClientSession().__aenter__())
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                coroutine=KucoinAPIOrderBookDataSource.get_snapshot(
                    client, self.trading_pair))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))
Example #13
0
    def test_get_snapshot_no_auth(self, mock_api):
        url = CONSTANTS.BASE_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))

        client = aiohttp.ClientSession()
        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_snapshot(
                client, self.trading_pair))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

        self.assertEqual(ret, resp)  # shallow comparison ok
Example #14
0
    def __init__(self,
                 kucoin_api_key: str,
                 kucoin_passphrase: str,
                 kucoin_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True,
                 domain: str = CONSTANTS.DEFAULT_DOMAIN):

        self._domain = domain
        self._time_synchronizer = TimeSynchronizer()
        super().__init__()
        self._auth = KucoinAuth(
            api_key=kucoin_api_key,
            passphrase=kucoin_passphrase,
            secret_key=kucoin_secret_key,
            time_provider=self._time_synchronizer)
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._api_factory = web_utils.build_api_factory(
            throttler=self._throttler,
            time_synchronizer=self._time_synchronizer,
            domain=self._domain,
            auth=self._auth)
        self._last_poll_timestamp = 0
        self._last_timestamp = 0
        self._trading_pairs = trading_pairs
        self._order_book_tracker = OrderBookTracker(
            data_source=KucoinAPIOrderBookDataSource(
                trading_pairs=trading_pairs,
                domain=self._domain,
                api_factory=self._api_factory,
                throttler=self._throttler,
                time_synchronizer=self._time_synchronizer),
            trading_pairs=trading_pairs,
            domain=self._domain)
        self._user_stream_tracker = UserStreamTracker(
            data_source=KucoinAPIUserStreamDataSource(
                domain=self._domain,
                api_factory=self._api_factory,
                throttler=self._throttler))
        self._poll_notifier = asyncio.Event()
        self._status_polling_task = None
        self._user_stream_tracker_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._trading_fees_polling_task = None
        self._trading_required = trading_required
        self._trading_rules = {}
        self._trading_fees = {}
        self._order_tracker: ClientOrderTracker = ClientOrderTracker(connector=self)
Example #15
0
    def test_get_last_traded_prices(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL
        resp = {
            "data": {
                "ticker": [{
                    "symbol": self.trading_pair,
                    "last": 100.0,
                }]
            }
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_last_traded_prices(
                [self.trading_pair]))

        self.assertEqual(ret[self.trading_pair], 100)
Example #16
0
    def test_fetch_trading_pairs(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.EXCHANGE_INFO_PATH_URL
        resp = {
            "data": [{
                "symbol": self.trading_pair,
                "enableTrading": True,
            }, {
                "symbol": "SOME-PAIR",
                "enableTrading": False,
            }]
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.fetch_trading_pairs())

        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0], self.trading_pair)
    def test_get_last_traded_prices(self, mock_api):
        KucoinAPIOrderBookDataSource._trading_pair_symbol_map[
            CONSTANTS.DEFAULT_DOMAIN]["TKN1-TKN2"] = "TKN1-TKN2"

        url1 = web_utils.rest_url(
            path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL,
            domain=CONSTANTS.DEFAULT_DOMAIN)
        url1 = f"{url1}?symbol={self.trading_pair}"
        regex_url = re.compile(f"^{url1}".replace(".",
                                                  r"\.").replace("?", r"\?"))
        resp = {
            "code": "200000",
            "data": {
                "sequence": "1550467636704",
                "bestAsk": "0.03715004",
                "size": "0.17",
                "price": "100",
                "bestBidSize": "3.803",
                "bestBid": "0.03710768",
                "bestAskSize": "1.788",
                "time": 1550653727731
            }
        }
        mock_api.get(regex_url, body=json.dumps(resp))

        url2 = web_utils.rest_url(
            path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL,
            domain=CONSTANTS.DEFAULT_DOMAIN)
        url2 = f"{url2}?symbol=TKN1-TKN2"
        regex_url = re.compile(f"^{url2}".replace(".",
                                                  r"\.").replace("?", r"\?"))
        resp = {
            "code": "200000",
            "data": {
                "sequence": "1550467636704",
                "bestAsk": "0.03715004",
                "size": "0.17",
                "price": "200",
                "bestBidSize": "3.803",
                "bestBid": "0.03710768",
                "bestAskSize": "1.788",
                "time": 1550653727731
            }
        }
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_last_traded_prices(
                [self.trading_pair, "TKN1-TKN2"]))

        ticker_requests = [(key, value)
                           for key, value in mock_api.requests.items()
                           if key[1].human_repr().startswith(url1)
                           or key[1].human_repr().startswith(url2)]

        request_params = ticker_requests[0][1][0].kwargs["params"]
        self.assertEqual(f"{self.base_asset}-{self.quote_asset}",
                         request_params["symbol"])
        request_params = ticker_requests[1][1][0].kwargs["params"]
        self.assertEqual("TKN1-TKN2", request_params["symbol"])

        self.assertEqual(ret[self.trading_pair], 100)
        self.assertEqual(ret["TKN1-TKN2"], 200)
Example #18
0
class TestKucoinAPIOrderBookDataSource(KucoinTestProviders, unittest.TestCase):
    def setUp(self) -> None:
        super().setUp()
        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.auth = KucoinAuth(self.api_key, self.api_passphrase,
                               self.api_secret_key)
        self.ob_data_source = KucoinAPIOrderBookDataSource(
            self.throttler, [self.trading_pair], self.auth)

    @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_last_traded_prices(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL
        resp = {
            "data": {
                "ticker": [{
                    "symbol": self.trading_pair,
                    "last": 100.0,
                }]
            }
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_last_traded_prices(
                [self.trading_pair]))

        self.assertEqual(ret[self.trading_pair], 100)

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.EXCHANGE_INFO_PATH_URL
        resp = {
            "data": [{
                "symbol": self.trading_pair,
                "enableTrading": True,
            }, {
                "symbol": "SOME-PAIR",
                "enableTrading": False,
            }]
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.fetch_trading_pairs())

        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0], self.trading_pair)

    @aioresponses()
    def test_get_snapshot_raises(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, status=500)

        client = self.ev_loop.run_until_complete(
            aiohttp.ClientSession().__aenter__())
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                coroutine=KucoinAPIOrderBookDataSource.get_snapshot(
                    client, self.trading_pair))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

    @aioresponses()
    def test_get_snapshot_no_auth(self, mock_api):
        url = CONSTANTS.BASE_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))

        client = aiohttp.ClientSession()
        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_snapshot(
                client, self.trading_pair))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

        self.assertEqual(ret, resp)  # shallow comparison ok

    @aioresponses()
    def test_get_snapshot_with_auth(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_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))

        client = self.ev_loop.run_until_complete(
            aiohttp.ClientSession().__aenter__())
        ret = self.async_run_with_timeout(
            coroutine=self.ob_data_source.get_snapshot(
                client, self.trading_pair, self.auth, self.throttler))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

        self.assertEqual(ret, resp)  # shallow comparison ok

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_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))

        self.assertTrue(isinstance(ret, OrderBook))
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"
        cls.api_key = "someKey"
        cls.api_passphrase = "somePassPhrase"
        cls.api_secret_key = "someSecretKey"

    def setUp(self) -> None:
        super().setUp()
        self.log_records = []
        self.async_task = None

        self.mocking_assistant = NetworkMockingAssistant()
        self.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self.time_synchronnizer = TimeSynchronizer()
        self.time_synchronnizer.add_time_offset_ms_sample(1000)
        self.ob_data_source = KucoinAPIOrderBookDataSource(
            trading_pairs=[self.trading_pair],
            throttler=self.throttler,
            time_synchronizer=self.time_synchronnizer)

        self.ob_data_source.logger().setLevel(1)
        self.ob_data_source.logger().addHandler(self)

        KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {
            CONSTANTS.DEFAULT_DOMAIN:
            bidict({self.trading_pair: self.trading_pair})
        }

    def tearDown(self) -> None:
        self.async_task and self.async_task.cancel()
        KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {}
        super().tearDown()

    def handle(self, record):
        self.log_records.append(record)

    def _is_logged(self, log_level: str, message: str) -> bool:
        return any(
            record.levelname == log_level and record.getMessage() == message
            for record in self.log_records)

    def async_run_with_timeout(self, coroutine: Awaitable, timeout: int = 1):
        ret = self.ev_loop.run_until_complete(
            asyncio.wait_for(coroutine, timeout))
        return ret

    @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_last_traded_prices(self, mock_api):
        KucoinAPIOrderBookDataSource._trading_pair_symbol_map[
            CONSTANTS.DEFAULT_DOMAIN]["TKN1-TKN2"] = "TKN1-TKN2"

        url1 = web_utils.rest_url(
            path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL,
            domain=CONSTANTS.DEFAULT_DOMAIN)
        url1 = f"{url1}?symbol={self.trading_pair}"
        regex_url = re.compile(f"^{url1}".replace(".",
                                                  r"\.").replace("?", r"\?"))
        resp = {
            "code": "200000",
            "data": {
                "sequence": "1550467636704",
                "bestAsk": "0.03715004",
                "size": "0.17",
                "price": "100",
                "bestBidSize": "3.803",
                "bestBid": "0.03710768",
                "bestAskSize": "1.788",
                "time": 1550653727731
            }
        }
        mock_api.get(regex_url, body=json.dumps(resp))

        url2 = web_utils.rest_url(
            path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL,
            domain=CONSTANTS.DEFAULT_DOMAIN)
        url2 = f"{url2}?symbol=TKN1-TKN2"
        regex_url = re.compile(f"^{url2}".replace(".",
                                                  r"\.").replace("?", r"\?"))
        resp = {
            "code": "200000",
            "data": {
                "sequence": "1550467636704",
                "bestAsk": "0.03715004",
                "size": "0.17",
                "price": "200",
                "bestBidSize": "3.803",
                "bestBid": "0.03710768",
                "bestAskSize": "1.788",
                "time": 1550653727731
            }
        }
        mock_api.get(regex_url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_last_traded_prices(
                [self.trading_pair, "TKN1-TKN2"]))

        ticker_requests = [(key, value)
                           for key, value in mock_api.requests.items()
                           if key[1].human_repr().startswith(url1)
                           or key[1].human_repr().startswith(url2)]

        request_params = ticker_requests[0][1][0].kwargs["params"]
        self.assertEqual(f"{self.base_asset}-{self.quote_asset}",
                         request_params["symbol"])
        request_params = ticker_requests[1][1][0].kwargs["params"]
        self.assertEqual("TKN1-TKN2", request_params["symbol"])

        self.assertEqual(ret[self.trading_pair], 100)
        self.assertEqual(ret["TKN1-TKN2"], 200)

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {}
        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL)

        resp = {
            "data": [{
                "symbol": self.trading_pair,
                "name": self.trading_pair,
                "baseCurrency": self.base_asset,
                "quoteCurrency": self.quote_asset,
                "enableTrading": True,
            }, {
                "symbol": "SOME-PAIR",
                "name": "SOME-PAIR",
                "baseCurrency": "SOME",
                "quoteCurrency": "PAIR",
                "enableTrading": False,
            }]
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.fetch_trading_pairs(
                throttler=self.throttler,
                time_synchronizer=self.time_synchronnizer,
            ))

        self.assertEqual(1, len(ret))
        self.assertEqual(self.trading_pair, ret[0])

    @aioresponses()
    def test_fetch_trading_pairs_exception_raised(self, mock_api):
        KucoinAPIOrderBookDataSource._trading_pair_symbol_map = {}

        url = web_utils.rest_url(path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL)
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))

        mock_api.get(regex_url, exception=Exception)

        result: Dict[str] = self.async_run_with_timeout(
            self.ob_data_source.fetch_trading_pairs())

        self.assertEqual(0, len(result))

    @aioresponses()
    def test_get_snapshot_raises(self, mock_api):
        url = web_utils.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, status=500)

        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                coroutine=self.ob_data_source.get_snapshot(self.trading_pair))

    @aioresponses()
    def test_get_snapshot(self, mock_api):
        url = web_utils.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_snapshot(self.trading_pair))

        self.assertEqual(ret, resp)  # shallow comparison ok

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = web_utils.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.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.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.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.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[
            CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_trades_logs_exception(self):
        incomplete_resp = {
            "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[
            CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public trade updates from exchange"
            ))

    def test_listen_for_trades_successful(self):
        mock_queue = AsyncMock()
        trade_event = {
            "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[
            CONSTANTS.TRADE_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_trades(self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(trade_event["data"]["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[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        with self.assertRaises(asyncio.CancelledError):
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
            self.async_run_with_timeout(self.listening_task)

    def test_listen_for_order_book_diffs_logs_exception(self):
        incomplete_resp = {
            "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[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        self.listening_task = self.ev_loop.create_task(
            self.ob_data_source.listen_for_order_book_diffs(
                self.ev_loop, msg_queue))

        try:
            self.async_run_with_timeout(self.listening_task)
        except asyncio.CancelledError:
            pass

        self.assertTrue(
            self._is_logged(
                "ERROR",
                "Unexpected error when processing public order book updates from exchange"
            ))

    def test_listen_for_order_book_diffs_successful(self):
        mock_queue = AsyncMock()
        diff_event = {
            "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[
            CONSTANTS.DIFF_EVENT_TYPE] = mock_queue

        msg_queue: asyncio.Queue = asyncio.Queue()

        try:
            self.listening_task = self.ev_loop.create_task(
                self.ob_data_source.listen_for_order_book_diffs(
                    self.ev_loop, msg_queue))
        except asyncio.CancelledError:
            pass

        msg: OrderBookMessage = self.async_run_with_timeout(msg_queue.get())

        self.assertTrue(diff_event["data"]["sequenceEnd"], msg.update_id)

    @aioresponses()
    def test_listen_for_order_book_snapshots_cancelled_when_fetching_snapshot(
            self, mock_api):
        url = web_utils.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.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.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)
Example #20
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)
Example #21
0
class TestKucoinAPIOrderBookDataSource(KucoinTestProviders, unittest.TestCase):
    def setUp(self) -> None:
        super().setUp()
        self.auth = KucoinAuth(self.api_key, self.api_passphrase,
                               self.api_secret_key)
        self.ob_data_source = KucoinAPIOrderBookDataSource(
            self.throttler, [self.trading_pair], self.auth)

    @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_last_traded_prices(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL
        resp = {
            "data": {
                "ticker": [{
                    "symbol": self.trading_pair,
                    "last": 100.0,
                }]
            }
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_last_traded_prices(
                [self.trading_pair]))

        self.assertEqual(ret[self.trading_pair], 100)

    @aioresponses()
    def test_fetch_trading_pairs(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.EXCHANGE_INFO_PATH_URL
        resp = {
            "data": [{
                "symbol": self.trading_pair,
                "enableTrading": True,
            }, {
                "symbol": "SOME-PAIR",
                "enableTrading": False,
            }]
        }
        mock_api.get(url, body=json.dumps(resp))

        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.fetch_trading_pairs())

        self.assertEqual(len(ret), 1)
        self.assertEqual(ret[0], self.trading_pair)

    @aioresponses()
    def test_get_snapshot_raises(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_NO_AUTH_PATH_URL
        regex_url = re.compile(f"^{url}".replace(".",
                                                 r"\.").replace("?", r"\?"))
        mock_api.get(regex_url, status=500)

        client = self.ev_loop.run_until_complete(
            aiohttp.ClientSession().__aenter__())
        with self.assertRaises(IOError):
            self.async_run_with_timeout(
                coroutine=KucoinAPIOrderBookDataSource.get_snapshot(
                    client, self.trading_pair))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

    @aioresponses()
    def test_get_snapshot_no_auth(self, mock_api):
        url = CONSTANTS.BASE_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))

        client = aiohttp.ClientSession()
        ret = self.async_run_with_timeout(
            coroutine=KucoinAPIOrderBookDataSource.get_snapshot(
                client, self.trading_pair))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

        self.assertEqual(ret, resp)  # shallow comparison ok

    @aioresponses()
    def test_get_snapshot_with_auth(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_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))

        client = self.ev_loop.run_until_complete(
            aiohttp.ClientSession().__aenter__())
        ret = self.async_run_with_timeout(
            coroutine=self.ob_data_source.get_snapshot(
                client, self.trading_pair, self.auth, self.throttler))
        self.ev_loop.run_until_complete(client.__aexit__(None, None, None))

        self.assertEqual(ret, resp)  # shallow comparison ok

    @aioresponses()
    def test_get_new_order_book(self, mock_api):
        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.SNAPSHOT_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))

        self.assertTrue(isinstance(ret, OrderBook))

    @aioresponses()
    @patch("aiohttp.client.ClientSession.ws_connect")
    def test_order_book_diff_symbol_transformed_correctly(
            self, mock_api, ws_connect_mock):
        base_asset = "WAXP"
        quote_asset = "USDT"

        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PUBLIC_WS_DATA_PATH_URL
        resp_data = {
            "data": {
                "instanceServers": [{
                    "endpoint": self.ws_endpoint,
                }],
                "token": "someToken",
            }
        }
        mock_api.post(url, body=json.dumps(resp_data))

        ws_mock = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.__aenter__.return_value = ws_mock

        data_source = KucoinAPIOrderBookDataSource(
            self.throttler, [f"_{base_asset}-{quote_asset}"])
        received_messages = asyncio.Queue()

        diff_response = {
            "type": "message",
            "topic": "/market/level2:WAX-USDT",
            "subject": "trade.l2update",
            "data": {
                "sequenceStart": 1545896669105,
                "sequenceEnd": 1545896669106,
                "symbol": "WAX-USDT",
                "changes": {
                    "asks": [["0.1997", "1000", "1545896669105"]],
                    "bids": [["0.1993", "2000", "1545896669106"]]
                }
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_mock, message=json.dumps(diff_response))

        self.listening_task = asyncio.get_event_loop().create_task(
            data_source.listen_for_order_book_diffs(asyncio.get_event_loop(),
                                                    received_messages))

        diff_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.DIFF, diff_message.type)
        self.assertEqual(diff_response["data"]["sequenceStart"],
                         diff_message.first_update_id)
        self.assertEqual(f"{base_asset}-{quote_asset}",
                         diff_message.trading_pair)

    @aioresponses()
    @patch("aiohttp.client.ClientSession.ws_connect")
    def test_trade_event_symbol_transformed_correctly(self, mock_api,
                                                      ws_connect_mock):
        base_asset = "WAXP"
        quote_asset = "USDT"

        url = CONSTANTS.BASE_PATH_URL + CONSTANTS.PUBLIC_WS_DATA_PATH_URL
        resp_data = {
            "data": {
                "instanceServers": [{
                    "endpoint": self.ws_endpoint,
                }],
                "token": "someToken",
            }
        }
        mock_api.post(url, body=json.dumps(resp_data))

        ws_mock = self.mocking_assistant.create_websocket_mock()
        ws_connect_mock.return_value.__aenter__.return_value = ws_mock

        data_source = KucoinAPIOrderBookDataSource(
            self.throttler, [f"_{base_asset}-{quote_asset}"])
        received_messages = asyncio.Queue()

        response = {
            "type": "message",
            "topic": "/market/match:WAX-USDT",
            "subject": "trade.l3match",
            "data": {
                "sequence": "1545896669145",
                "type": "match",
                "symbol": "WAX-USDT",
                "side": "buy",
                "price": "0.08200000000000000000",
                "size": "0.01022222000000000000",
                "tradeId": "5c24c5da03aa673885cd67aa",
                "takerOrderId": "5c24c5d903aa6772d55b371e",
                "makerOrderId": "5c2187d003aa677bd09d5c93",
                "time": "1545913818099033203"
            }
        }

        self.mocking_assistant.add_websocket_aiohttp_message(
            websocket_mock=ws_mock, message=json.dumps(response))

        self.listening_task = asyncio.get_event_loop().create_task(
            data_source.listen_for_trades(asyncio.get_event_loop(),
                                          received_messages))

        trade_message = self.async_run_with_timeout(received_messages.get())

        self.assertEqual(OrderBookMessageType.TRADE, trade_message.type)
        self.assertEqual(-1, trade_message.update_id)
        self.assertEqual(response["data"]["tradeId"], trade_message.trade_id)
        self.assertEqual(f"{base_asset}-{quote_asset}",
                         trade_message.trading_pair)