Esempio n. 1
0
class BittrexExchangeTest(unittest.TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.api_key = "someKey"
        cls.secret_key = "someSecret"
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.symbol = f"{cls.base_asset}{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.ev_loop = asyncio.get_event_loop()
        self.log_records = []
        self.test_task: Optional[asyncio.Task] = None
        self.resume_test_event = asyncio.Event()

        self.exchange = BittrexExchange(self.api_key,
                                        self.secret_key,
                                        trading_pairs=[self.trading_pair])

        self.exchange.logger().setLevel(1)
        self.exchange.logger().addHandler(self)
        self._initialize_event_loggers()

    def tearDown(self) -> None:
        self.test_task and self.test_task.cancel()
        super().tearDown()

    def _initialize_event_loggers(self):
        self.buy_order_completed_logger = EventLogger()
        self.sell_order_completed_logger = EventLogger()
        self.order_filled_logger = EventLogger()
        self.order_cancelled_logger = EventLogger()

        events_and_loggers = [
            (MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger),
            (MarketEvent.SellOrderCompleted, self.sell_order_completed_logger),
            (MarketEvent.OrderFilled, self.order_filled_logger),
            (MarketEvent.OrderCancelled, self.order_cancelled_logger)
        ]

        for event, logger in events_and_loggers:
            self.exchange.add_listener(event, logger)

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

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

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

    def _return_calculation_and_set_done_event(self, calculation: Callable,
                                               *args, **kwargs):
        if self.resume_test_event.is_set():
            raise asyncio.CancelledError
        self.resume_test_event.set()
        return calculation(*args, **kwargs)

    def get_filled_response(self) -> Dict:
        filled_resp = {
            "id": "87076200-79bc-4f97-82b1-ad8fa3e630cf",
            "marketSymbol": self.trading_pair,
            "direction": "BUY",
            "type": "LIMIT",
            "quantity": "1",
            "limit": "10",
            "timeInForce": "POST_ONLY_GOOD_TIL_CANCELLED",
            "fillQuantity": "1",
            "commission": "0.11805420",
            "proceeds": "23.61084196",
            "status": "CLOSED",
            "createdAt": "2021-09-08T10:00:34.83Z",
            "updatedAt": "2021-09-08T10:00:35.05Z",
            "closedAt": "2021-09-08T10:00:35.05Z",
        }
        return filled_resp

    @aioresponses()
    def test_execute_cancel(self, mocked_api):
        url = f"{self.exchange.BITTREX_API_ENDPOINT}/orders/"
        regex_url = re.compile(f"^{url}")
        resp = {"status": "CLOSED"}
        mocked_api.delete(regex_url, body=json.dumps(resp))

        order_id = "someId"
        self.exchange.start_tracking_order(
            order_id=order_id,
            exchange_order_id="someExchangeId",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT_MAKER,
            trade_type=TradeType.BUY,
            price=Decimal("10.0"),
            amount=Decimal("1.0"),
        )

        self.async_run_with_timeout(coroutine=self.exchange.execute_cancel(
            self.trading_pair, order_id))

        self.assertEqual(1, len(self.order_cancelled_logger.event_log))

        event = self.order_cancelled_logger.event_log[0]

        self.assertEqual(order_id, event.order_id)
        self.assertTrue(order_id not in self.exchange.in_flight_orders)

    @aioresponses()
    def test_execute_cancel_already_filled(self, mocked_api):
        url = f"{self.exchange.BITTREX_API_ENDPOINT}/orders/"
        regex_url = re.compile(f"^{url}")
        del_resp = {"code": "ORDER_NOT_OPEN"}
        mocked_api.delete(regex_url, status=409, body=json.dumps(del_resp))
        get_resp = self.get_filled_response()
        mocked_api.get(regex_url, body=json.dumps(get_resp))

        order_id = "someId"
        self.exchange.start_tracking_order(
            order_id=order_id,
            exchange_order_id="someExchangeId",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT_MAKER,
            trade_type=TradeType.BUY,
            price=Decimal("10.0"),
            amount=Decimal("1.0"),
        )

        self.async_run_with_timeout(coroutine=self.exchange.execute_cancel(
            self.trading_pair, order_id))

        self.assertEqual(1, len(self.buy_order_completed_logger.event_log))

        event = self.buy_order_completed_logger.event_log[0]

        self.assertEqual(order_id, event.order_id)
        self.assertTrue(order_id not in self.exchange.in_flight_orders)

    def test_order_fill_event_takes_fee_from_update_event(self):
        self.exchange.start_tracking_order(
            order_id="OID1",
            exchange_order_id="EOID1",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal("10000"),
            amount=Decimal("1"),
        )

        order = self.exchange.in_flight_orders.get("OID1")

        partial_fill = {
            "accountId":
            "testAccount",
            "sequence":
            "1001",
            "deltas": [{
                "id": "1",
                "marketSymbol": f"{self.base_asset}{self.quote_asset}",
                "executedAt": "12-03-2021 6:17:16",
                "quantity": "0.1",
                "rate": "10050",
                "orderId": "EOID1",
                "commission": "10",
                "isTaker": False
            }]
        }

        message = {
            "event_type": "execution",
            "content": partial_fill,
        }

        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: message)

        self.exchange.user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertEqual(Decimal("10"), order.fee_paid)
        self.assertEqual(1, len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0]
        self.assertEqual(Decimal("0"), fill_event.trade_fee.percent)
        self.assertEqual([
            TokenAmount(order.quote_asset,
                        Decimal(partial_fill["deltas"][0]["commission"]))
        ], fill_event.trade_fee.flat_fees)
        self.assertTrue(
            self._is_logged(
                "INFO",
                f"Filled {Decimal(partial_fill['deltas'][0]['quantity'])} out of {order.amount} of the "
                f"{order.order_type_description} order {order.client_order_id}. - ws"
            ))

        self.assertEqual(0, len(self.buy_order_completed_logger.event_log))

        complete_fill = {
            "accountId":
            "testAccount",
            "sequence":
            "1001",
            "deltas": [{
                "id": "2",
                "marketSymbol": f"{self.base_asset}{self.quote_asset}",
                "executedAt": "12-03-2021 6:17:16",
                "quantity": "0.9",
                "rate": "10060",
                "orderId": "EOID1",
                "commission": "30",
                "isTaker": False
            }]
        }

        message["content"] = complete_fill

        self.resume_test_event = asyncio.Event()
        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = functools.partial(
            self._return_calculation_and_set_done_event, lambda: message)

        self.exchange.user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        self.async_run_with_timeout(self.resume_test_event.wait())

        self.assertEqual(Decimal("40"), order.fee_paid)

        self.assertEqual(2, len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1]
        self.assertEqual(Decimal("0"), fill_event.trade_fee.percent)
        self.assertEqual([
            TokenAmount(order.quote_asset,
                        Decimal(complete_fill["deltas"][0]["commission"]))
        ], fill_event.trade_fee.flat_fees)

        # The order should be marked as complete only when the "done" event arrives, not with the fill event
        self.assertFalse(
            self._is_logged(
                "INFO",
                f"The market buy order {order.client_order_id} has completed according to Coinbase Pro user stream."
            ))

        self.assertEqual(0, len(self.buy_order_completed_logger.event_log))

    def test_order_fill_event_processed_before_order_complete_event(self):
        self.exchange.start_tracking_order(
            order_id="OID1",
            exchange_order_id="EOID1",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT,
            trade_type=TradeType.BUY,
            price=Decimal("10000"),
            amount=Decimal("1"),
        )

        order = self.exchange.in_flight_orders.get("OID1")

        complete_fill = {
            "id": "1",
            "marketSymbol": f"{self.base_asset}{self.quote_asset}",
            "executedAt": "12-03-2021 6:17:16",
            "quantity": "1",
            "rate": "10050",
            "orderId": "EOID1",
            "commission": "10",
            "isTaker": False
        }

        fill_message = {
            "event_type": "execution",
            "content": {
                "accountId": "testAccount",
                "sequence": "1001",
                "deltas": [complete_fill]
            }
        }

        update_data = {
            "id": "EOID1",
            "marketSymbol": f"{self.base_asset}{self.quote_asset}",
            "direction": "BUY",
            "type": "LIMIT",
            "quantity": "1",
            "limit": "10000",
            "ceiling": "10000",
            "timeInForce": "GOOD_TIL_CANCELLED",
            "clientOrderId": "OID1",
            "fillQuantity": "1",
            "commission": "10",
            "proceeds": "10050",
            "status": "CLOSED",
            "createdAt": "12-03-2021 6:17:16",
            "updatedAt": "12-03-2021 6:17:16",
            "closedAt": "12-03-2021 6:17:16",
            "orderToCancel": {
                "type": "LIMIT",
                "id": "string (uuid)"
            }
        }

        update_message = {
            "event_type": "order",
            "content": {
                "accountId": "testAccount",
                "sequence": "1001",
                "delta": update_data
            }
        }

        mock_user_stream = AsyncMock()
        # We simulate the case when the order update arrives before the order fill
        mock_user_stream.get.side_effect = [
            update_message, fill_message,
            asyncio.CancelledError()
        ]
        self.exchange.user_stream_tracker._user_stream = mock_user_stream

        self.test_task = asyncio.get_event_loop().create_task(
            self.exchange._user_stream_event_listener())
        try:
            self.async_run_with_timeout(self.test_task)
        except asyncio.CancelledError:
            pass

        self.async_run_with_timeout(order.wait_until_completely_filled())

        self.assertEqual(Decimal("10"), order.fee_paid)
        self.assertEqual(1, len(self.order_filled_logger.event_log))
        fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0]
        self.assertEqual(Decimal("0"), fill_event.trade_fee.percent)
        self.assertEqual([
            TokenAmount(order.quote_asset, Decimal(
                complete_fill["commission"]))
        ], fill_event.trade_fee.flat_fees)
        self.assertTrue(
            self._is_logged(
                "INFO",
                f"Filled {Decimal(complete_fill['quantity'])} out of {order.amount} of the "
                f"{order.order_type_description} order {order.client_order_id}. - ws"
            ))

        self.assertTrue(
            self._is_logged(
                "INFO",
                f"The BUY order {order.client_order_id} has completed according to order delta websocket API."
            ))

        self.assertEqual(1, len(self.buy_order_completed_logger.event_log))
    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, [])
            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, "/v3/ping",
                                        FixtureBittrex.PING)
            cls.web_app.update_response("get", API_BASE_URL, "/v3/markets",
                                        FixtureBittrex.MARKETS)
            cls.web_app.update_response("get", API_BASE_URL,
                                        "/v3/markets/tickers",
                                        FixtureBittrex.MARKETS_TICKERS)
            cls.web_app.update_response("get", API_BASE_URL, "/v3/balances",
                                        FixtureBittrex.BALANCES)
            cls.web_app.update_response("get", API_BASE_URL, "/v3/orders/open",
                                        FixtureBittrex.ORDERS_OPEN)
            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.bittrex.bittrex_exchange.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()

            cls._us_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.bittrex.bittrex_api_user_stream_data_source."
                "BittrexAPIUserStreamDataSource._transform_raw_message",
                autospec=True)
            cls._us_mock = cls._us_patcher.start()
            cls._us_mock.side_effect = _transform_raw_message_patch

            cls._ob_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.bittrex.bittrex_api_order_book_data_source."
                "BittrexAPIOrderBookDataSource._transform_raw_message",
                autospec=True)
            cls._ob_mock = cls._ob_patcher.start()
            cls._ob_mock.side_effect = _transform_raw_message_patch

            MockWebSocketServerFactory.url_host_only = True
            ws_server = MockWebSocketServerFactory.start_new_server(
                WS_BASE_URL)
            cls._ws_patcher = unittest.mock.patch("websockets.connect",
                                                  autospec=True)
            cls._ws_mock = cls._ws_patcher.start()
            cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect
            ws_server.add_stock_response(
                "queryExchangeState",
                FixtureBittrex.WS_ORDER_BOOK_SNAPSHOT.copy())

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BittrexExchange = BittrexExchange(
            bittrex_api_key=API_KEY,
            bittrex_secret_key=API_SECRET,
            trading_pairs=["ETH-USDT"])

        print("Initializing Bittrex market... this will take about a minute. ")
        cls.clock.add_iterator(cls.market)
        cls.stack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")
class BittrexExchangeTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        super().setUpClass()
        cls.api_key = "someKey"
        cls.secret_key = "someSecret"
        cls.base_asset = "COINALPHA"
        cls.quote_asset = "HBOT"
        cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}"
        cls.symbol = f"{cls.base_asset}{cls.quote_asset}"

    def setUp(self) -> None:
        super().setUp()
        self.ev_loop = asyncio.get_event_loop()
        self.event_listener = MockEventListener()

        self.exchange = BittrexExchange(self.api_key,
                                        self.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_filled_response(self) -> Dict:
        filled_resp = {
            "id": "87076200-79bc-4f97-82b1-ad8fa3e630cf",
            "marketSymbol": self.trading_pair,
            "direction": "BUY",
            "type": "LIMIT",
            "quantity": "1",
            "limit": "10",
            "timeInForce": "POST_ONLY_GOOD_TIL_CANCELLED",
            "fillQuantity": "1",
            "commission": "0.11805420",
            "proceeds": "23.61084196",
            "status": "CLOSED",
            "createdAt": "2021-09-08T10:00:34.83Z",
            "updatedAt": "2021-09-08T10:00:35.05Z",
            "closedAt": "2021-09-08T10:00:35.05Z",
        }
        return filled_resp

    @aioresponses()
    def test_execute_cancel(self, mocked_api):
        url = f"{self.exchange.BITTREX_API_ENDPOINT}/orders/"
        regex_url = re.compile(f"^{url}")
        resp = {"status": "CLOSED"}
        mocked_api.delete(regex_url, body=json.dumps(resp))

        order_id = "someId"
        self.exchange.start_tracking_order(
            order_id=order_id,
            exchange_order_id="someExchangeId",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT_MAKER,
            trade_type=TradeType.BUY,
            price=Decimal("10.0"),
            amount=Decimal("1.0"),
        )
        self.exchange.add_listener(MarketEvent.OrderCancelled,
                                   self.event_listener)

        self.async_run_with_timeout(coroutine=self.exchange.execute_cancel(
            self.trading_pair, order_id))

        self.assertEqual(self.event_listener.events_count, 1)
        self.assertTrue(order_id not in self.exchange.in_flight_orders)

    @aioresponses()
    def test_execute_cancel_already_filled(self, mocked_api):
        url = f"{self.exchange.BITTREX_API_ENDPOINT}/orders/"
        regex_url = re.compile(f"^{url}")
        del_resp = {"code": "ORDER_NOT_OPEN"}
        mocked_api.delete(regex_url, status=409, body=json.dumps(del_resp))
        get_resp = self.get_filled_response()
        mocked_api.get(regex_url, body=json.dumps(get_resp))

        order_id = "someId"
        self.exchange.start_tracking_order(
            order_id=order_id,
            exchange_order_id="someExchangeId",
            trading_pair=self.trading_pair,
            order_type=OrderType.LIMIT_MAKER,
            trade_type=TradeType.BUY,
            price=Decimal("10.0"),
            amount=Decimal("1.0"),
        )
        self.exchange.add_listener(MarketEvent.BuyOrderCompleted,
                                   self.event_listener)

        self.async_run_with_timeout(coroutine=self.exchange.execute_cancel(
            self.trading_pair, order_id))

        self.assertEqual(self.event_listener.events_count, 1)
        self.assertTrue(order_id not in self.exchange.in_flight_orders)
    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))
            current_bid_price: Decimal = self.market.get_price(
                trading_pair, True) * Decimal('0.80')
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                trading_pair, current_bid_price)
            bid_amount: Decimal = Decimal('0.06')
            quantized_bid_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, bid_amount)

            order_id, exch_order_id = self.place_order(
                True, trading_pair, quantized_bid_amount, OrderType.LIMIT,
                quantize_bid_price, 10001, FixtureBittrex.OPEN_BUY_LIMIT_ORDER,
                FixtureBittrex.WS_AFTER_BUY_1)
            [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: BittrexExchange = BittrexExchange(
                bittrex_api_key=API_KEY,
                bittrex_secret_key=API_SECRET,
                trading_pairs=["XRP-BTC"])
            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))

            # Cancel the order and verify that the change is saved.
            self.cancel_order(trading_pair, order_id, exch_order_id)
            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)