Exemple #1
0
    def setUpClass(cls):
        cls.ev_loop = asyncio.get_event_loop()

        if API_MOCK_ENABLED:
            cls.web_app = MockWebServer.get_instance()
            cls.web_app.add_host_to_mock(API_HOST, ["/products", "/currencies"])
            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_HOST, "/fiat_accounts", FixtureLiquid.FIAT_ACCOUNTS)
            cls.web_app.update_response("get", API_HOST, "/crypto_accounts",
                                        FixtureLiquid.CRYPTO_ACCOUNTS)
            cls.web_app.update_response("get", API_HOST, "/orders", FixtureLiquid.ORDERS_GET)
            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.liquid.liquid_exchange.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: LiquidExchange = LiquidExchange(
            API_KEY, API_SECRET,
            poll_interval=5,
            trading_pairs=['CEL-ETH'],
        )
        # cls.ev_loop.run_until_complete(cls.market._update_balances())
        print("Initializing Liquid market... this will take about a minute.")
        cls.clock.add_iterator(cls.market)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")
    def setUp(self) -> None:
        super().setUp()

        self.log_records = []
        self.test_task: Optional[asyncio.Task] = None
        self.resume_test_event = asyncio.Event()

        self.exchange = LiquidExchange(
            liquid_api_key="testAPIKey",
            liquid_secret_key="testSecret",
            trading_pairs=[self.trading_pair],
        )

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

        self._initialize_event_loggers()
Exemple #3
0
    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        sql: SQLConnectionManager = SQLConnectionManager(
            SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market],
                                                    config_path, strategy_name)
        recorder.start()

        try:
            self.assertEqual(0, len(self.market.tracking_states))

            # Try to put limit buy order for 0.005 ETH worth of CEL, and watch for order creation event.
            current_bid_price: Decimal = self.market.get_price("CEL-ETH", True)
            bid_price: Decimal = current_bid_price * Decimal("0.8")
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                "CEL-ETH", bid_price)

            amount: Decimal = 1
            quantized_amount: Decimal = self.market.quantize_order_amount(
                "CEL-ETH", amount)

            order_id, buy_exchange_id = self.place_order(
                True, "CEL-ETH", quantized_amount, OrderType.LIMIT_MAKER,
                quantize_bid_price, 10001, FixtureLiquid.ORDER_SAVE_RESTORE,
                FixtureLiquid.ORDERS_GET_AFTER_BUY)
            [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: LiquidExchange = LiquidExchange(
                API_KEY, API_SECRET, trading_pairs=['ETH-USD', 'CEL-ETH'])

            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.
            if API_MOCK_ENABLED:
                order_cancel_resp = FixtureLiquid.ORDER_CANCEL_SAVE_RESTORE.copy(
                )
                self.web_app.update_response(
                    "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel",
                    order_cancel_resp)
            self.market.cancel("CEL-ETH", 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("CEL-ETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
class LiquidExchangeTests(TestCase):
    # the level is required to receive logs from the data source logger
    level = 0

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

    def setUp(self) -> None:
        super().setUp()

        self.log_records = []
        self.test_task: Optional[asyncio.Task] = None
        self.resume_test_event = asyncio.Event()

        self.exchange = LiquidExchange(
            liquid_api_key="testAPIKey",
            liquid_secret_key="testSecret",
            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()

        events_and_loggers = [
            (MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger),
            (MarketEvent.SellOrderCompleted, self.sell_order_completed_logger),
            (MarketEvent.OrderFilled, self.order_filled_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: float = 1):
        ret = asyncio.get_event_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 _trade_info(self, trade_id, amount, price, fee, status="live"):
        return {
            "average_price": 10000.0,
            "client_order_id": "OID1",
            "created_at": 1639429916,
            "crypto_account_id": None,
            "currency_pair_code": "BTCUSDT",
            "disc_quantity": 0.0,
            "filled_quantity": amount,
            "funding_currency": "USDT",
            "iceberg_total_quantity": 0.0,
            "id": 5821066005,
            "leverage_level": 1,
            "margin_interest": 0.0,
            "margin_type": None,
            "margin_used": 0.0,
            "order_fee": fee,
            "order_type": "limit",
            "price": price,
            "product_code": "CASH",
            "product_id": 761,
            "quantity": 1.0,
            "side": "buy",
            "source_action": "manual",
            "source_exchange": 0,
            "status": status,
            "stop_loss": None,
            "take_profit": None,
            "target": "spot",
            "trade_id": None,
            "trading_type": "spot",
            "unwound_trade_id": None,
            "unwound_trade_leverage_level": None,
            "updated_at": trade_id
        }

    def test_order_fill_event_takes_fee_from_update_event(self):
        self.exchange.start_tracking_order(
            order_id="OID1",
            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")
        order.update_exchange_order_id("5821066005")

        partial_fill = self._trade_info(1, 0.1, 10050.0, 10.0)

        message = {
            "channel": "user_account_usdt_orders",
            "data": json.dumps(partial_fill),
            "event": "updated",
        }

        mock_user_stream = AsyncMock()
        # We simulate the case when the order update arrives before the order fill
        mock_user_stream.get.side_effect = [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.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(partial_fill["funding_currency"],
                        Decimal(partial_fill["order_fee"]))
        ], fill_event.trade_fee.flat_fees)
        self.assertTrue(
            self._is_logged(
                "INFO",
                f"Filled {Decimal('0.1')} out of {order.amount} of the "
                f"{order.order_type_description} order {order.client_order_id} according to Liquid user stream."
            ))

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

        complete_fill = self._trade_info(2, 1, 10060.0, 30.0)

        message["data"] = json.dumps(complete_fill)

        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = [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.assertEqual(Decimal("30"), 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(
                complete_fill["funding_currency"],
                Decimal(complete_fill["order_fee"]) -
                Decimal(partial_fill["order_fee"]))
        ], fill_event.trade_fee.flat_fees)

        # Complete events are not produced by fill notifications, only by order updates
        self.assertFalse(
            self._is_logged(
                "INFO",
                f"The market buy order {order.client_order_id} has completed according to Liquid user stream."
            ))

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

    def test_single_complete_fill_is_processed_correctly(self):
        self.exchange.start_tracking_order(
            order_id="OID1",
            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")
        order.update_exchange_order_id("5821066005")

        complete_fill = self._trade_info(2, 1, 10060.0, 30.0)

        message = {
            "channel": "user_account_usdt_orders",
            "data": json.dumps(complete_fill),
            "event": "updated",
        }

        complete_fill = self._trade_info(1, 1, 10060.0, 30.0, "filled")

        message["data"] = json.dumps(complete_fill)

        mock_user_stream = AsyncMock()
        mock_user_stream.get.side_effect = [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.assertEqual(Decimal("30"), 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(complete_fill["funding_currency"],
                        Decimal(complete_fill["order_fee"]))
        ], fill_event.trade_fee.flat_fees)

        self.assertTrue(
            self._is_logged(
                "INFO",
                f"The market buy order {order.client_order_id} has completed according to Liquid user stream."
            ))

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

        buy_complete_event: BuyOrderCompletedEvent = self.buy_order_completed_logger.event_log[
            0]
        self.assertEqual(Decimal(30), buy_complete_event.fee_amount)
        self.assertEqual(complete_fill["funding_currency"],
                         buy_complete_event.fee_asset)