Esempio n. 1
0
class CoinbaseProExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled,
        MarketEvent.OrderCancelled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]

    market: CoinbaseProExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.ev_loop = asyncio.get_event_loop()
        trading_pair = "ETH-USDC"
        if API_MOCK_ENABLED:
            cls.web_app = MockWebServer.get_instance()
            cls.web_app.add_host_to_mock(
                API_BASE_URL,
                ["/time", "/products", f"/products/{trading_pair}/book"])
            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, "/accounts",
                                        FixtureCoinbasePro.BALANCES)
            cls.web_app.update_response("get", API_BASE_URL, "/fees",
                                        FixtureCoinbasePro.TRADE_FEES)
            cls.web_app.update_response("get", API_BASE_URL, "/orders",
                                        FixtureCoinbasePro.ORDERS_STATUS)

            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

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.coinbase_pro.coinbase_pro_exchange.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: CoinbaseProExchange = CoinbaseProExchange(
            API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=[trading_pair])
        print(
            "Initializing Coinbase Pro 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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()
        if API_MOCK_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._t_nonce_patcher.stop()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(
            join(__file__, "../coinbase_pro_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDC", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 1)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_fee_overrides_config(self):
        fee_overrides_config_map["coinbase_pro_taker_fee"].value = None
        taker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.005"), taker_fee.percent)
        fee_overrides_config_map["coinbase_pro_taker_fee"].value = Decimal(
            '0.2')
        taker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["coinbase_pro_maker_fee"].value = None
        maker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent)
        fee_overrides_config_map["coinbase_pro_maker_fee"].value = Decimal(
            '0.75')
        maker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.0075"), maker_fee.percent)

    def place_order(self, is_buy, trading_pair, amount, order_type, price,
                    nonce, fixture_resp, fixture_ws):
        order_id, exch_order_id = None, None
        if API_MOCK_ENABLED:
            self._t_nonce_mock.return_value = nonce
            side = 'buy' if is_buy else 'sell'
            resp = fixture_resp.copy()
            exch_order_id = resp["id"]
            resp["side"] = side
            self.web_app.update_response("post", API_BASE_URL, "/orders", resp)
        if is_buy:
            order_id = self.market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = self.market.sell(trading_pair, amount, order_type,
                                        price)
        if API_MOCK_ENABLED:
            resp = fixture_ws.copy()
            resp["order_id"] = exch_order_id
            resp["side"] = side
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.1)
        return order_id, exch_order_id

    def cancel_order(self, trading_pair, order_id, exchange_order_id,
                     fixture_ws):
        if API_MOCK_ENABLED:
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/orders/{exchange_order_id}",
                                         exchange_order_id)
        self.market.cancel(trading_pair, order_id)
        if API_MOCK_ENABLED:
            resp = fixture_ws.copy()
            resp["order_id"] = exchange_order_id
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.1)

    def test_limit_maker_rejections(self):
        if API_MOCK_ENABLED:
            return
        trading_pair = "ETH-USDC"

        # Try to put a buy limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('1.02')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair,
                                                   Decimal("0.02"))
        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER,
                                   price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

        self.market_logger.clear()

        # Try to put a sell limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('0.98')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair,
                                                   Decimal("0.02"))

        order_id = self.market.sell(trading_pair, amount,
                                    OrderType.LIMIT_MAKER, price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

    def test_limit_makers_unfilled(self):
        if API_MOCK_ENABLED:
            return
        trading_pair = "ETH-USDC"
        bid_price: Decimal = self.market.get_price(trading_pair,
                                                   True) * Decimal("0.5")
        ask_price: Decimal = self.market.get_price(trading_pair, False) * 2
        amount: Decimal = 10 / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price * Decimal("0.7"))
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price * Decimal("1.5"))

        order_id, exch_order_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER,
            FixtureCoinbasePro.WS_ORDER_OPEN)
        [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)

        order_id_2, exch_order_id_2 = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002,
            FixtureCoinbasePro.OPEN_SELL_LIMIT_ORDER,
            FixtureCoinbasePro.WS_ORDER_OPEN)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id_2, order_created_event.order_id)

        self.run_parallel(asyncio.sleep(1))

        if API_MOCK_ENABLED:
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/orders/{exch_order_id}",
                                         exch_order_id)
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/orders/{exch_order_id_2}",
                                         exch_order_id_2)
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        if API_MOCK_ENABLED:
            resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy()
            resp["order_id"] = exch_order_id
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.1)
            resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy()
            resp["order_id"] = exch_order_id_2
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.11)
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_limit_taker_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))
        trading_pair = "ETH-USDC"
        price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("0.02")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT, price,
            10001, FixtureCoinbasePro.BUY_MARKET_ORDER,
            FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT_MAKER for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_limit_taker_sell(self):
        trading_pair = "ETH-USDC"
        price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal("0.02")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT, price,
            10001, FixtureCoinbasePro.BUY_MARKET_ORDER,
            FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT_MAKER for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):
        trading_pair = "ETH-USDC"

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("0.2")
        self.assertGreater(self.market.get_balance("ETH"), amount)

        bid_price: Decimal = current_bid_price - Decimal(
            "0.1") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, exch_order_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER,
            FixtureCoinbasePro.WS_ORDER_OPEN)

        self.cancel_order(trading_pair, order_id, exch_order_id,
                          FixtureCoinbasePro.WS_ORDER_CANCELED)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, order_id)

    def test_cancel_all(self):
        trading_pair = "ETH-USDC"
        bid_price: Decimal = self.market.get_price(trading_pair,
                                                   True) * Decimal("0.5")
        ask_price: Decimal = self.market.get_price(trading_pair, False) * 2
        amount: Decimal = 10 / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price * Decimal("0.7"))
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price * Decimal("1.5"))

        _, exch_order_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER,
            FixtureCoinbasePro.WS_ORDER_OPEN)
        _, exch_order_id_2 = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002,
            FixtureCoinbasePro.OPEN_SELL_LIMIT_ORDER,
            FixtureCoinbasePro.WS_ORDER_OPEN)
        self.run_parallel(asyncio.sleep(1))

        if API_MOCK_ENABLED:
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/orders/{exch_order_id}",
                                         exch_order_id)
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/orders/{exch_order_id_2}",
                                         exch_order_id_2)
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        if API_MOCK_ENABLED:
            resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy()
            resp["order_id"] = exch_order_id
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.1)
            resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy()
            resp["order_id"] = exch_order_id_2
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.11)
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv),
                         "List order test requires manual action.")
    def test_list_orders(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))
        trading_pair = "ETH-USDC"
        amount: Decimal = Decimal("0.02")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        bid_price: Decimal = current_bid_price + Decimal(
            "0.05") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)

        self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
                        quantize_bid_price)
        self.run_parallel(asyncio.sleep(1))
        [order_details] = self.run_parallel(self.market.list_orders())
        self.assertGreaterEqual(len(order_details), 1)

        self.market_logger.clear()

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ETH-USDC"
        sql: SQLConnectionManager = SQLConnectionManager(
            SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market],
                                                    config_path, strategy_name)
        recorder.start()

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

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

            amount: Decimal = Decimal("0.02")
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, amount)

            order_id, exch_order_id = self.place_order(
                True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
                quantize_bid_price, 10001,
                FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER,
                FixtureCoinbasePro.WS_ORDER_OPEN)
            [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: CoinbaseProExchange = CoinbaseProExchange(
                coinbase_pro_api_key=API_KEY,
                coinbase_pro_secret_key=API_SECRET,
                coinbase_pro_passphrase=API_PASSPHRASE,
                trading_pairs=["ETH-USDC"])
            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,
                              FixtureCoinbasePro.WS_ORDER_CANCELED)
            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.cancel_order(trading_pair, order_id, exch_order_id,
                                  FixtureCoinbasePro.WS_ORDER_CANCELED)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_update_last_prices(self):
        # This is basic test to see if order_book last_trade_price is initiated and updated.
        for order_book in self.market.order_books.values():
            for _ in range(5):
                self.ev_loop.run_until_complete(asyncio.sleep(1))
                print(order_book.last_trade_price)
                self.assertFalse(math.isnan(order_book.last_trade_price))

    def test_order_fill_record(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ETH-USDC"
        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:
            # Try to buy 0.04 ETH from the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal("0.02")
            order_id, exch_order_id = self.place_order(
                True, trading_pair, amount, OrderType.LIMIT, price, 10001,
                FixtureCoinbasePro.BUY_MARKET_ORDER,
                FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of ETH to the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(trading_pair, False)
            amount = buy_order_completed_event.base_asset_amount
            order_id, exch_order_id = self.place_order(
                False, trading_pair, amount, OrderType.LIMIT, price, 10002,
                FixtureCoinbasePro.SELL_MARKET_ORDER,
                FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        finally:
            if order_id is not None:
                self.cancel_order(trading_pair, order_id, exch_order_id,
                                  FixtureCoinbasePro.WS_ORDER_CANCELED)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
Esempio n. 2
0
class BinancePerpetualOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[BinancePerpetualOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["BTC-USDT", "ETH-USDT"]

    @classmethod
    def setUpClass(cls) -> None:
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: BinancePerpetualOrderBookTracker = BinancePerpetualOrderBookTracker(
            trading_pairs=cls.trading_pairs,
            base_url="https://testnet.binancefuture.com",
            stream_url="wss://stream.binancefuture.com")
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(
            cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        while True:
            if len(cls.order_book_tracker.order_books) > 0:
                print("Initialized real-time order books.")
                return
            await asyncio.sleep(1)

    def setUp(self) -> None:
        self.event_logger = EventLogger()
        for event_tag in self.events:
            for trading_pair, order_book in self.order_book_tracker.order_books.items(
            ):
                order_book.add_listener(event_tag, self.event_logger)

    @staticmethod
    async def run_parallel_async(*tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            await asyncio.sleep(1)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_order_book_trade_occurs(self):
        self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent))
        for ob_trade_event in self.event_logger.event_log:
            self.assertEqual(type(ob_trade_event), OrderBookTradeEvent)
            self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs)
            self.assertEqual(type(ob_trade_event.timestamp), float)
            self.assertEqual(type(ob_trade_event.amount), float)
            self.assertEqual(type(ob_trade_event.price), float)
            self.assertEqual(type(ob_trade_event.type), TradeType)
            self.assertTrue(ob_trade_event.amount > 0)
            self.assertTrue(ob_trade_event.price > 0)

    def test_tracker_adv(self):
        self.ev_loop.run_until_complete(asyncio.sleep(10))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        btcusdt_book: OrderBook = order_books[self.trading_pairs[0]]
        ethusdt_book: OrderBook = order_books[self.trading_pairs[1]]

        print("BTC-USDT SNAPSHOT: ")
        print(btcusdt_book.snapshot)
        print("ETH-USDT SNAPSHOT: ")
        print(ethusdt_book.snapshot)

        self.assertGreaterEqual(
            btcusdt_book.get_price_for_volume(True, 10).result_price,
            btcusdt_book.get_price(True))
        self.assertLessEqual(
            btcusdt_book.get_price_for_volume(False, 10).result_price,
            btcusdt_book.get_price(False))
        self.assertGreaterEqual(
            ethusdt_book.get_price_for_volume(True, 10).result_price,
            ethusdt_book.get_price(True))
        self.assertLessEqual(
            ethusdt_book.get_price_for_volume(False, 10).result_price,
            ethusdt_book.get_price(False))
 def setUp(self):
     self.event_logger = EventLogger()
     for event_tag in self.events:
         for trading_pair, order_book in self.order_book_tracker.order_books.items(
         ):
             order_book.add_listener(event_tag, self.event_logger)
class BambooRelayMarketCoordinatedUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled,
        MarketEvent.OrderExpired, MarketEvent.OrderFilled,
        MarketEvent.WithdrawAsset
    ]

    wallet_events: List[WalletEvent] = [
        WalletEvent.WrappedEth, WalletEvent.UnwrappedEth
    ]

    wallet: Web3Wallet
    market: BambooRelayMarket
    market_logger: EventLogger
    wallet_logger: EventLogger

    @classmethod
    def setUpClass(cls):
        if conf.test_bamboo_relay_chain_id == 3:
            chain = EthereumChain.ROPSTEN
        elif conf.test_bamboo_relay_chain_id == 4:
            chain = EthereumChain.RINKEBY
        elif conf.test_bamboo_relay_chain_id == 42:
            chain = EthereumChain.KOVAN
        elif conf.test_bamboo_relay_chain_id == 1337:
            chain = EthereumChain.ZEROEX_TEST
        else:
            chain = EthereumChain.MAIN_NET
        cls.chain = chain
        cls.base_token_asset = conf.test_bamboo_relay_base_token_symbol
        cls.quote_token_asset = conf.test_bamboo_relay_quote_token_symbol
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.wallet = Web3Wallet(private_key=conf.web3_private_key_bamboo,
                                backend_urls=conf.test_web3_provider_list,
                                erc20_token_addresses=[
                                    conf.test_bamboo_relay_base_token_address,
                                    conf.test_bamboo_relay_quote_token_address
                                ],
                                chain=chain)
        cls.market: BambooRelayMarket = BambooRelayMarket(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            trading_pairs=[
                conf.test_bamboo_relay_base_token_symbol + "-" +
                conf.test_bamboo_relay_quote_token_symbol
            ],
            use_coordinator=True,
            pre_emptive_soft_cancels=True)
        print("Initializing Bamboo Relay market... ")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.wallet)
        cls.clock.add_iterator(cls.market)
        stack = contextlib.ExitStack()
        cls._clock = stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(
            join(__file__, "../bamboo_relay_uncordinated_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        self.wallet_logger = EventLogger()
        for event_tag in self.market_events:
            self.market.add_listener(event_tag, self.market_logger)
        for event_tag in self.wallet_events:
            self.wallet.add_listener(event_tag, self.wallet_logger)

    def tearDown(self):
        for event_tag in self.market_events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None
        for event_tag in self.wallet_events:
            self.wallet.remove_listener(event_tag, self.wallet_logger)
        self.wallet_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        maker_buy_trade_fee: TradeFee = self.market.get_fee(
            "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, Decimal(20),
            Decimal(0.01))
        self.assertEqual(maker_buy_trade_fee.percent, 0)
        self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0)
        taker_buy_trade_fee: TradeFee = self.market.get_fee(
            "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, Decimal(20))
        self.assertEqual(taker_buy_trade_fee.percent, 0)
        self.assertEqual(len(taker_buy_trade_fee.flat_fees), 2)
        self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH")
        self.assertEqual(taker_buy_trade_fee.flat_fees[1][0], "ETH")

    def test_get_wallet_balances(self):
        balances = self.market.get_all_balances()
        self.assertGreaterEqual((balances["ETH"]), s_decimal_0)
        self.assertGreaterEqual((balances[self.quote_token_asset]),
                                s_decimal_0)

    def test_limit_order_amount_modified(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount = Decimal("0.001")
        expires = int(time.time() + 60 * 3)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        buy_order_id = self.market.buy(trading_pair=trading_pair,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       Decimal("0.2") * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset,
                         buy_order_opened_event.trading_pair)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        self.assertEqual(float(quantized_amount),
                         float(buy_order_opened_event.amount))

        [cancellation_results, buy_order_cancelled_event] = self.run_parallel(
            self.market.cancel_order(buy_order_id),
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_cancelled_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_single_limit_order_cancel(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount = Decimal("0.001")
        expires = int(time.time() + 60 * 3)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        buy_order_id = self.market.buy(trading_pair=trading_pair,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       Decimal("0.2") * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset,
                         buy_order_opened_event.trading_pair)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        self.assertEqual(float(quantized_amount),
                         float(buy_order_opened_event.amount))
        [cancellation_results, buy_order_cancelled_event] = self.run_parallel(
            self.market.cancel_order(buy_order_id),
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_cancelled_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_limit_buy_and_sell_and_cancel_all(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount = Decimal("0.001")
        expires = int(time.time() + 60 * 3)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        buy_order_id = self.market.buy(trading_pair=trading_pair,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       Decimal("0.2") * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(buy_order_id, buy_order_opened_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(buy_order_opened_event.amount))
        self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset,
                         buy_order_opened_event.trading_pair)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)

        # Reset the logs
        self.market_logger.clear()

        current_price: Decimal = self.market.get_price(trading_pair, False)
        sell_order_id = self.market.sell(trading_pair=trading_pair,
                                         amount=amount,
                                         order_type=OrderType.LIMIT,
                                         price=current_price +
                                         Decimal("0.2") * current_price,
                                         expiration_ts=expires)
        [sell_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        self.assertEqual(sell_order_id, sell_order_opened_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(sell_order_opened_event.amount))
        self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset,
                         sell_order_opened_event.trading_pair)
        self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type)

        [cancellation_results, order_cancelled_event] = self.run_parallel(
            self.market.cancel_all(60 * 3),
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(cancellation_results[0],
                         CancellationResult(buy_order_id, True))
        self.assertEqual(cancellation_results[1],
                         CancellationResult(sell_order_id, True))

        # Wait for the order book source to also register the cancellation
        self.assertTrue(
            (buy_order_opened_event.order_id == order_cancelled_event.order_id
             or sell_order_opened_event.order_id
             == order_cancelled_event.order_id))
        # Reset the logs
        self.market_logger.clear()

    def test_order_pre_emptive_cancel(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount = Decimal("0.003")
        expires = int(time.time() + 60)  # expires in 1 min
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        buy_order_id = self.market.buy(trading_pair=trading_pair,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       Decimal("0.2") * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))

        self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset,
                         buy_order_opened_event.trading_pair)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        [buy_order_expired_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent, 75))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_expired_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_market_buy(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        amount = Decimal("0.002")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        order_id = self.market.buy(
            self.base_token_asset + "-" + self.quote_token_asset, amount,
            OrderType.MARKET)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual(self.base_token_asset,
                         order_completed_event.base_asset)
        self.assertEqual(self.quote_token_asset,
                         order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_batch_market_buy(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        amount = Decimal("0.002")
        current_price: Decimal = self.market.get_price(trading_pair, True)
        expires = int(time.time() + 60 * 3)
        sell_order_id = self.market.sell(trading_pair=trading_pair,
                                         amount=amount,
                                         order_type=OrderType.LIMIT,
                                         price=current_price -
                                         Decimal("0.2") * current_price,
                                         expiration_ts=expires)
        self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent))

        amount = Decimal("0.004")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        order_id = self.market.buy(
            self.base_token_asset + "-" + self.quote_token_asset, amount,
            OrderType.MARKET)

        [order_completed_event, _] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent),
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual(self.base_token_asset,
                         order_completed_event.base_asset)
        self.assertEqual(self.quote_token_asset,
                         order_completed_event.quote_asset)

        self.market_logger.clear()

    def test_market_sell(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        amount = Decimal("0.001")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        order_id = self.market.sell(trading_pair, amount, OrderType.MARKET)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual(self.base_token_asset,
                         order_completed_event.base_asset)
        self.assertEqual(self.quote_token_asset,
                         order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_batch_market_sell(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        amount = Decimal("0.002")
        current_price: Decimal = self.market.get_price(trading_pair, False)
        expires = int(time.time() + 60 * 3)
        buy_order_id = self.market.buy(trading_pair=trading_pair,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price +
                                       Decimal("0.2") * current_price,
                                       expiration_ts=expires)
        self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))

        amount = Decimal("0.005")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        order_id = self.market.sell(
            self.base_token_asset + "-" + self.quote_token_asset, amount,
            OrderType.MARKET)

        [order_completed_event, _] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent),
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual(self.base_token_asset,
                         order_completed_event.base_asset)
        self.assertEqual(self.quote_token_asset,
                         order_completed_event.quote_asset)

        self.market_logger.clear()

    def test_wrap_eth(self):
        amount_to_wrap = Decimal("0.01")
        tx_hash = self.wallet.wrap_eth(amount_to_wrap)
        [tx_completed_event] = self.run_parallel(
            self.wallet_logger.wait_for(WalletWrappedEthEvent))
        tx_completed_event: WalletWrappedEthEvent = tx_completed_event

        self.assertEqual(tx_hash, tx_completed_event.tx_hash)
        self.assertEqual(float(amount_to_wrap),
                         float(tx_completed_event.amount))
        self.assertEqual(self.wallet.address, tx_completed_event.address)

    def test_unwrap_eth(self):
        amount_to_unwrap = Decimal("0.01")
        tx_hash = self.wallet.unwrap_eth(amount_to_unwrap)
        [tx_completed_event] = self.run_parallel(
            self.wallet_logger.wait_for(WalletUnwrappedEthEvent))
        tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event

        self.assertEqual(tx_hash, tx_completed_event.tx_hash)
        self.assertEqual(float(amount_to_unwrap),
                         float(tx_completed_event.amount))
        self.assertEqual(self.wallet.address, tx_completed_event.address)

    def test_z_orders_saving_and_restoration(self):
        self.market.reset_state()

        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        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["limit_orders"]))

            # Try to put limit buy order for 0.05 Quote Token worth of Base Token, and watch for order creation event.
            current_bid_price: Decimal = self.market.get_price(
                trading_pair, True)
            bid_price: Decimal = current_bid_price * Decimal("0.8")
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                trading_pair, bid_price)

            amount: Decimal = Decimal("0.005") / bid_price
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, amount)

            expires = int(time.time() + 60 * 3)
            order_id = self.market.buy(trading_pair,
                                       quantized_amount,
                                       OrderType.LIMIT,
                                       quantize_bid_price,
                                       expiration_ts=expires)
            [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["limit_orders"]))
            self.assertEqual(
                order_id,
                list(self.market.tracking_states["limit_orders"].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.assertIsInstance(
                saved_market_states.saved_state["limit_orders"], dict)
            self.assertGreater(
                len(saved_market_states.saved_state["limit_orders"]), 0)

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.market_events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: BambooRelayMarket = BambooRelayMarket(
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_web3_provider_list[0],
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                trading_pairs=[
                    self.base_token_asset + "-" + self.quote_token_asset
                ],
                use_coordinator=True,
                pre_emptive_soft_cancels=True)
            for event_tag in self.market_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["limit_orders"]))
            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["limit_orders"]))

            # Cancel the order and verify that the change is saved.
            self.run_parallel(self.market.cancel(trading_pair, order_id),
                              self.market_logger.wait_for(OrderCancelledEvent))
            order_id = None
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(1,
                             len(self.market.tracking_states["limit_orders"]))
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.assertEqual(
                1, len(saved_market_states.saved_state["limit_orders"]))
        finally:
            if order_id is not None:
                self.run_parallel(
                    self.market.cancel(trading_pair, order_id),
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_order_fill_record(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        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:
            # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal("0.005") / current_price
            order_id = self.market.buy(trading_pair, amount)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of ZRX to the exchange, and watch for completion event.
            amount = buy_order_completed_event.base_asset_amount
            order_id = self.market.sell(trading_pair, amount)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        finally:
            if order_id is not None:
                self.run_parallel(
                    self.market.cancel(trading_pair, order_id),
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
Esempio n. 5
0
class DydxOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[DydxOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["WETH-USDC", "WETH-DAI"]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: DydxOrderBookTracker = DydxOrderBookTracker(
            trading_pairs=cls.trading_pairs, )
        cls.order_book_tracker.start()
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        while True:
            if len(cls.order_book_tracker.order_books) > 0:
                print("Initialized real-time order books.")
                return
            await asyncio.sleep(1)

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def setUp(self):
        self.event_logger = EventLogger()
        for event_tag in self.events:
            for trading_pair, order_book in self.order_book_tracker.order_books.items(
            ):
                order_book.add_listener(event_tag, self.event_logger)

    def test_order_book_trade_event_emission(self):
        """
        Test if order book tracker is able to retrieve order book trade message
        from exchange and emit order book trade events after correctly parsing
        the trade messages
        """
        self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent))
        for ob_trade_event in self.event_logger.event_log:
            self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent)
            self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs)
            self.assertTrue(type(ob_trade_event.timestamp) == float)
            self.assertTrue(type(ob_trade_event.amount) == float)
            self.assertTrue(type(ob_trade_event.price) == float)
            self.assertTrue(type(ob_trade_event.type) == TradeType)
            self.assertTrue(
                math.ceil(math.log10(ob_trade_event.timestamp)) == 10)
            self.assertTrue(ob_trade_event.amount > 0)
            self.assertTrue(ob_trade_event.price > 0)

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        lrc_eth_book: OrderBook = order_books["WETH-DAI"]
        self.assertGreaterEqual(
            lrc_eth_book.get_price_for_volume(True, 0.1).result_price,
            lrc_eth_book.get_price(True))

    def test_mid_price(self):
        data_source = self.order_book_tracker.data_source
        mid_price = data_source.get_mid_price(self.trading_pairs[0])
        self.assertGreater(mid_price, 100)
class PaperTradeExchangeTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled
    ]

    market: PaperTradeExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: PaperTradeExchange = PaperTradeExchange(
            order_book_tracker=OrderBookTracker(
                data_source=BinanceAPIOrderBookDataSource(
                    trading_pairs=["ETH-USDT", "BTC-USDT"]),
                trading_pairs=["ETH-USDT", "BTC-USDT"]),
            target_market=BinanceExchange,
            exchange_name="binance",
        )
        print(
            "Initializing PaperTrade execute orders market... this will take about a minute."
        )
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../binance_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

        for trading_pair, orderbook in self.market.order_books.items():
            orderbook.clear_traded_order_book()

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_place_market_orders(self):
        self.market.sell("ETH-USDT", 30, OrderType.MARKET)
        list_queued_orders: List[QueuedOrder] = self.market.queued_orders
        first_queued_order: QueuedOrder = list_queued_orders[0]
        self.assertFalse(first_queued_order.is_buy,
                         msg="Market order is not sell")
        self.assertEqual(first_queued_order.trading_pair,
                         "ETH-USDT",
                         msg="Trading pair is incorrect")
        self.assertEqual(first_queued_order.amount,
                         30,
                         msg="Quantity is incorrect")
        self.assertEqual(len(list_queued_orders),
                         1,
                         msg="First market order did not get added")

        # Figure out why this test is failing
        self.market.buy("BTC-USDT", 30, OrderType.MARKET)
        list_queued_orders: List[QueuedOrder] = self.market.queued_orders
        second_queued_order: QueuedOrder = list_queued_orders[1]
        self.assertTrue(second_queued_order.is_buy,
                        msg="Market order is not buy")
        self.assertEqual(second_queued_order.trading_pair,
                         "BTC-USDT",
                         msg="Trading pair is incorrect")
        self.assertEqual(second_queued_order.amount,
                         30,
                         msg="Quantity is incorrect")
        self.assertEqual(second_queued_order.amount,
                         30,
                         msg="Quantity is incorrect")
        self.assertEqual(len(list_queued_orders),
                         2,
                         msg="Second market order did not get added")

    def test_market_order_simulation(self):
        self.market.set_balance("ETH", 20)
        self.market.set_balance("USDT", 100)
        self.market.sell("ETH-USDT", 10, OrderType.MARKET)
        self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))

        # Get diff between composite bid entries and original bid entries
        compare_df = OrderBookUtils.get_compare_df(
            self.market.order_books['ETH-USDT'].original_bid_entries(),
            self.market.order_books['ETH-USDT'].bid_entries(),
            diffs_only=True).sort_index().round(10)
        filled_bids = OrderBookUtils.ob_rows_data_frame(
            list(self.market.order_books['ETH-USDT'].traded_order_book.
                 bid_entries())).sort_index().round(10)

        # assert filled orders matches diff
        diff_bid = compare_df["diff"] - filled_bids["amount"]

        self.assertFalse(diff_bid.to_numpy().any())

        self.assertEquals(10,
                          self.market.get_balance("ETH"),
                          msg="Balance was not updated.")
        self.market.buy("ETH-USDT", 5, OrderType.MARKET)
        self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))

        # Get diff between composite bid entries and original bid entries
        compare_df = OrderBookUtils.get_compare_df(
            self.market.order_books['ETH-USDT'].original_ask_entries(),
            self.market.order_books['ETH-USDT'].ask_entries(),
            diffs_only=True).sort_index().round(10)
        filled_asks = OrderBookUtils.ob_rows_data_frame(
            list(self.market.order_books['ETH-USDT'].traded_order_book.
                 ask_entries())).sort_index().round(10)

        # assert filled orders matches diff
        diff_ask = compare_df["diff"] - filled_asks["amount"]

        self.assertFalse(diff_ask.to_numpy().any())
        self.assertEquals(15,
                          self.market.get_balance("ETH"),
                          msg="Balance was not updated.")

    def test_limit_order_crossed(self):
        starting_base_balance = 20
        starting_quote_balance = 1000
        self.market.set_balance("ETH", starting_base_balance)
        self.market.set_balance("USDT", starting_quote_balance)
        self.market.sell("ETH-USDT", 10, OrderType.LIMIT, 100)
        self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))
        self.assertEquals(starting_base_balance - 10,
                          self.market.get_balance("ETH"),
                          msg="ETH Balance was not updated.")
        self.assertEquals(starting_quote_balance + 1000,
                          self.market.get_balance("USDT"),
                          msg="USDT Balance was not updated.")
        self.market.buy("ETH-USDT", 1, OrderType.LIMIT, 500)
        self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))
        self.assertEquals(11,
                          self.market.get_balance("ETH"),
                          msg="ETH Balance was not updated.")
        self.assertEquals(1500,
                          self.market.get_balance("USDT"),
                          msg="USDT Balance was not updated.")

    def test_bid_limit_order_trade_match(self):
        """
        Test bid limit order fill and balance simulation, and market events emission
        """
        trading_pair = TradingPair("ETH-USDT", "ETH", "USDT")
        base_quantity = 2.0
        starting_base_balance = 200
        starting_quote_balance = 2000
        self.market.set_balance(trading_pair.base_asset, starting_base_balance)
        self.market.set_balance(trading_pair.quote_asset,
                                starting_quote_balance)

        best_bid_price = self.market.order_books[
            trading_pair.trading_pair].get_price(True)
        client_order_id = self.market.buy(trading_pair.trading_pair,
                                          base_quantity, OrderType.LIMIT,
                                          best_bid_price)

        matched_limit_orders = TestUtils.get_match_limit_orders(
            self.market.limit_orders, {
                "client_order_id": client_order_id,
                "trading_pair": trading_pair.trading_pair,
                "is_buy": True,
                "base_currency": trading_pair.base_asset,
                "quote_currency": trading_pair.quote_asset,
                "price": best_bid_price,
                "quantity": base_quantity
            })
        # Market should track limit orders
        self.assertEqual(1, len(matched_limit_orders))

        # Market should on hold balance for the created order
        self.assertAlmostEqual(
            float(self.market.on_hold_balances[trading_pair.quote_asset]),
            base_quantity * best_bid_price)
        # Market should reflect on hold balance in available balance
        self.assertAlmostEqual(
            float(self.market.get_available_balance(trading_pair.quote_asset)),
            starting_quote_balance - base_quantity * best_bid_price)

        matched_order_create_events = TestUtils.get_match_events(
            self.market_logger.event_log, BuyOrderCreatedEvent, {
                "type": OrderType.LIMIT,
                "amount": base_quantity,
                "price": best_bid_price,
                "order_id": client_order_id
            })
        # Market should emit BuyOrderCreatedEvent
        self.assertEqual(1, len(matched_order_create_events))

        async def delay_trigger_event1():
            await asyncio.sleep(1)
            trade_event1 = OrderBookTradeEvent(trading_pair="ETH-USDT",
                                               timestamp=time.time(),
                                               type=TradeType.SELL,
                                               price=best_bid_price + 1,
                                               amount=1.0)
            self.market.order_books['ETH-USDT'].apply_trade(trade_event1)

        safe_ensure_future(delay_trigger_event1())
        self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))

        placed_bid_orders: List[LimitOrder] = [
            o for o in self.market.limit_orders if o.is_buy
        ]
        # Market should delete limit order when it is filled
        self.assertEqual(0, len(placed_bid_orders))

        matched_order_complete_events = TestUtils.get_match_events(
            self.market_logger.event_log, BuyOrderCompletedEvent, {
                "order_type": OrderType.LIMIT,
                "quote_asset_amount": base_quantity * best_bid_price,
                "order_id": client_order_id
            })
        # Market should emit BuyOrderCompletedEvent
        self.assertEqual(1, len(matched_order_complete_events))

        matched_order_fill_events = TestUtils.get_match_events(
            self.market_logger.event_log, OrderFilledEvent, {
                "order_type": OrderType.LIMIT,
                "trade_type": TradeType.BUY,
                "trading_pair": trading_pair.trading_pair,
                "order_id": client_order_id
            })
        # Market should emit OrderFilledEvent
        self.assertEqual(1, len(matched_order_fill_events))

        # Market should have no more on hold balance
        self.assertAlmostEqual(
            float(self.market.on_hold_balances[trading_pair.quote_asset]), 0)
        # Market should update balance for the filled order
        self.assertAlmostEqual(
            float(self.market.get_available_balance(trading_pair.quote_asset)),
            starting_quote_balance - base_quantity * best_bid_price)

    def test_ask_limit_order_trade_match(self):
        """
        Test ask limit order fill and balance simulation, and market events emission
        """
        trading_pair = TradingPair("ETH-USDT", "ETH", "USDT")
        base_quantity = 2.0
        starting_base_balance = 200
        starting_quote_balance = 2000
        self.market.set_balance(trading_pair.base_asset, starting_base_balance)
        self.market.set_balance(trading_pair.quote_asset,
                                starting_quote_balance)

        best_ask_price = self.market.order_books[
            trading_pair.trading_pair].get_price(False)
        client_order_id = self.market.sell(trading_pair.trading_pair,
                                           base_quantity, OrderType.LIMIT,
                                           best_ask_price)

        matched_limit_orders = TestUtils.get_match_limit_orders(
            self.market.limit_orders, {
                "client_order_id": client_order_id,
                "trading_pair": trading_pair.trading_pair,
                "is_buy": False,
                "base_currency": trading_pair.base_asset,
                "quote_currency": trading_pair.quote_asset,
                "price": best_ask_price,
                "quantity": base_quantity
            })
        # Market should track limit orders
        self.assertEqual(1, len(matched_limit_orders))

        # Market should on hold balance for the created order
        self.assertAlmostEqual(
            float(self.market.on_hold_balances[trading_pair.base_asset]),
            base_quantity)
        # Market should reflect on hold balance in available balance
        self.assertAlmostEqual(
            self.market.get_available_balance(trading_pair.base_asset),
            starting_base_balance - base_quantity)

        matched_order_create_events = TestUtils.get_match_events(
            self.market_logger.event_log, SellOrderCreatedEvent, {
                "type": OrderType.LIMIT,
                "amount": base_quantity,
                "price": best_ask_price,
                "order_id": client_order_id
            })
        # Market should emit BuyOrderCreatedEvent
        self.assertEqual(1, len(matched_order_create_events))

        async def delay_trigger_event2():
            await asyncio.sleep(1)
            trade_event = OrderBookTradeEvent(
                trading_pair=trading_pair.trading_pair,
                timestamp=time.time(),
                type=TradeType.BUY,
                price=best_ask_price - 1,
                amount=base_quantity)
            self.market.order_books[trading_pair.trading_pair].apply_trade(
                trade_event)

        safe_ensure_future(delay_trigger_event2())

        self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))

        placed_ask_orders: List[LimitOrder] = [
            o for o in self.market.limit_orders if not o.is_buy
        ]

        # Market should delete limit order when it is filled
        self.assertEqual(0, len(placed_ask_orders))

        matched_order_complete_events = TestUtils.get_match_events(
            self.market_logger.event_log, SellOrderCompletedEvent, {
                "order_type": OrderType.LIMIT,
                "quote_asset_amount": base_quantity * base_quantity,
                "order_id": client_order_id
            })
        # Market should emit BuyOrderCompletedEvent
        self.assertEqual(1, len(matched_order_complete_events))

        matched_order_fill_events = TestUtils.get_match_events(
            self.market_logger.event_log, OrderFilledEvent, {
                "order_type": OrderType.LIMIT,
                "trade_type": TradeType.SELL,
                "trading_pair": trading_pair.trading_pair,
                "order_id": client_order_id
            })
        # Market should emit OrderFilledEvent
        self.assertEqual(1, len(matched_order_fill_events))

        # Market should have no more on hold balance
        self.assertAlmostEqual(
            float(self.market.on_hold_balances[trading_pair.base_asset]), 0)
        # Market should update balance for the filled order
        self.assertAlmostEqual(
            self.market.get_available_balance(trading_pair.base_asset),
            starting_base_balance - base_quantity)

    def test_order_cancellation(self):
        trading_pair = TradingPair("ETH-USDT", "ETH", "USDT")
        base_quantity = 2.0
        starting_base_balance = 200
        starting_quote_balance = 2000
        self.market.set_balance(trading_pair.base_asset, starting_base_balance)
        self.market.set_balance(trading_pair.quote_asset,
                                starting_quote_balance)
        best_ask_price = self.market.order_books[
            trading_pair.trading_pair].get_price(False)
        ask_client_order_id = self.market.sell(trading_pair.trading_pair,
                                               base_quantity, OrderType.LIMIT,
                                               best_ask_price)
        best_bid_price = self.market.order_books[
            trading_pair.trading_pair].get_price(True)
        self.market.buy(trading_pair.trading_pair, base_quantity,
                        OrderType.LIMIT, best_bid_price)

        # Market should track limit orders
        self.assertEqual(2, len(self.market.limit_orders))
        self.market.cancel(trading_pair.trading_pair, ask_client_order_id)

        matched_limit_orders = TestUtils.get_match_limit_orders(
            self.market.limit_orders, {
                "client_order_id": ask_client_order_id,
                "trading_pair": trading_pair.trading_pair,
                "is_buy": False,
                "base_currency": trading_pair.base_asset,
                "quote_currency": trading_pair.quote_asset,
                "price": best_ask_price,
                "quantity": base_quantity
            })

        # Market should remove canceled orders
        self.assertEqual(0, len(matched_limit_orders))

        matched_order_cancel_events = TestUtils.get_match_events(
            self.market_logger.event_log, OrderCancelledEvent,
            {"order_id": ask_client_order_id})
        # Market should emit cancel event
        self.assertEqual(1, len(matched_order_cancel_events))

    def test_order_cancel_all(self):
        trading_pair = TradingPair("ETH-USDT", "ETH", "USDT")
        base_quantity = 2.0
        starting_base_balance = 200
        starting_quote_balance = 2000
        self.market.set_balance(trading_pair.base_asset, starting_base_balance)
        self.market.set_balance(trading_pair.quote_asset,
                                starting_quote_balance)
        best_ask_price = self.market.order_books[
            trading_pair.trading_pair].get_price(False)
        self.market.sell(trading_pair.trading_pair, base_quantity,
                         OrderType.LIMIT, best_ask_price)
        best_bid_price = self.market.order_books[
            trading_pair.trading_pair].get_price(True)
        self.market.buy(trading_pair.trading_pair, base_quantity,
                        OrderType.LIMIT, best_bid_price)

        # Market should track limit orders
        self.assertEqual(2, len(self.market.limit_orders))

        asyncio.get_event_loop().run_until_complete(self.market.cancel_all(0))

        # Market should remove all canceled orders
        self.assertEqual(0, len(self.market.limit_orders))

        matched_order_cancel_events = TestUtils.get_match_events(
            self.market_logger.event_log, OrderCancelledEvent, {})
        # Market should emit cancel event
        self.assertEqual(2, len(matched_order_cancel_events))
class KucoinExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled,
        MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure
    ]

    market: KucoinExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        if API_MOCK_ENABLED:
            cls.web_app = MockWebServer.get_instance()
            cls.web_app.add_host_to_mock(API_BASE_URL, ["/api/v1/timestamp", "/api/v1/symbols",
                                                        "/api/v1/bullet-public",
                                                        "/api/v2/market/orderbook/level2"])
            cls.web_app.start()
            cls.ev_loop.run_until_complete(cls.web_app.wait_til_started())
            cls._patcher = mock.patch("aiohttp.client.URL")
            cls._url_mock = cls._patcher.start()
            cls._url_mock.side_effect = cls.web_app.reroute_local
            cls.web_app.update_response("get", API_BASE_URL, "/api/v1/accounts", FixtureKucoin.BALANCES)

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.kucoin.kucoin_exchange.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
            cls._exch_order_id = 20001
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: KucoinExchange = KucoinExchange(
            kucoin_api_key=API_KEY,
            kucoin_passphrase=API_PASSPHRASE,
            kucoin_secret_key=API_SECRET,
            trading_pairs=["ETH-USDT"]
        )
        # Need 2nd instance of market to prevent events mixing up across tests
        cls.market_2: KucoinExchange = KucoinExchange(
            kucoin_api_key=API_KEY,
            kucoin_passphrase=API_PASSPHRASE,
            kucoin_secret_key=API_SECRET,
            trading_pairs=["ETH-USDT"]
        )
        cls.clock.add_iterator(cls.market)
        cls.clock.add_iterator(cls.market_2)
        cls.stack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()
        if API_MOCK_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._t_nonce_patcher.stop()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready and cls.market_2.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../kucoin_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        self.market_2_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)
            self.market_2.add_listener(event_tag, self.market_2_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
            self.market_2.remove_listener(event_tag, self.market_2_logger)
        self.market_logger = None
        self.market_2_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(0.5)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: AddedToCostTradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: AddedToCostTradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)
        sell_trade_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10
        )
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

    def order_response(self, fixture_data, nonce):
        self._t_nonce_mock.return_value = nonce
        order_resp = fixture_data.copy()
        return order_resp

    def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, post_resp, get_resp):
        global EXCHANGE_ORDER_ID
        order_id, exch_order_id = None, None
        if API_MOCK_ENABLED:
            exch_order_id = f"KUCOIN_{EXCHANGE_ORDER_ID}"
            EXCHANGE_ORDER_ID += 1
            resp = self.order_response(post_resp, nonce)
            resp["data"]["orderId"] = exch_order_id
            self.web_app.update_response("post", API_BASE_URL, "/api/v1/orders", resp)
        if is_buy:
            order_id = self.market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = self.market.sell(trading_pair, amount, order_type, price)
        if API_MOCK_ENABLED:
            resp = get_resp.copy()
            resp["data"]["id"] = exch_order_id
            resp["data"]["clientOid"] = order_id
            self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp)
        return order_id, exch_order_id

    def test_fee_overrides_config(self):
        fee_overrides_config_map["kucoin_taker_fee"].value = None
        taker_fee: AddedToCostTradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
                                                             Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent)
        fee_overrides_config_map["kucoin_taker_fee"].value = Decimal('0.2')
        taker_fee: AddedToCostTradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
                                                             Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["kucoin_maker_fee"].value = None
        maker_fee: AddedToCostTradeFee = self.market.get_fee("LINK",
                                                             "ETH",
                                                             OrderType.LIMIT_MAKER,
                                                             TradeType.BUY,
                                                             Decimal(1),
                                                             Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent)
        fee_overrides_config_map["kucoin_maker_fee"].value = Decimal('0.5')
        maker_fee: AddedToCostTradeFee = self.market.get_fee("LINK",
                                                             "ETH",
                                                             OrderType.LIMIT_MAKER,
                                                             TradeType.BUY,
                                                             Decimal(1),
                                                             Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent)

    def test_limit_maker_rejections(self):
        if API_MOCK_ENABLED:
            return
        trading_pair = "ETH-USDT"

        # Try to put a buy limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01))

        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price)
        [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

        self.market_logger.clear()

        # Try to put a sell limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01))

        order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price)
        [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

    def test_limit_makers_unfilled(self):
        if API_MOCK_ENABLED:
            return
        trading_pair = "ETH-USDT"
        bid_price = self.market.get_price(trading_pair, True) * Decimal("0.8")
        quantized_bid_price = self.market.quantize_order_price(trading_pair, bid_price)
        quantized_bid_amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01))

        order_id, _ = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER,
                                       quantized_bid_price, 10001,
                                       FixtureKucoin.ORDER_PLACE, FixtureKucoin.ORDER_GET_BUY_UNMATCHED)
        [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)

        ask_price = self.market.get_price(trading_pair, True) * Decimal("1.2")
        quatized_ask_price = self.market.quantize_order_price(trading_pair, ask_price)
        quatized_ask_amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01))

        order_id, _ = self.place_order(False, trading_pair, quatized_ask_amount, OrderType.LIMIT_MAKER,
                                       quatized_ask_price, 10002,
                                       FixtureKucoin.ORDER_PLACE, FixtureKucoin.ORDER_GET_SELL_UNMATCHED)
        [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id, order_created_event.order_id)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_limit_taker_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05"))
        trading_pair = "ETH-USDT"
        price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal(0.01)
        quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount)

        order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001,
                                       FixtureKucoin.ORDER_PLACE, FixtureKucoin.BUY_MARKET_ORDER)
        [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))
        buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event
        trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log
                                                if isinstance(t, OrderFilledEvent)]
        base_amount_traded: float = sum(t.amount for t in trade_events)
        quote_amount_traded: float = sum(t.amount * t.price for t in trade_events)

        self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, buy_order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount), buy_order_completed_event.base_asset_amount, places=4)
        self.assertEqual("ETH", buy_order_completed_event.base_asset)
        self.assertEqual("USDT", buy_order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded, float(buy_order_completed_event.base_asset_amount), places=4)
        self.assertAlmostEqual(quote_amount_traded, float(buy_order_completed_event.quote_asset_amount), places=4)
        self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0))
        self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id
                             for event in self.market_logger.event_log]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_taker_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05"))
        trading_pair = "ETH-USDT"
        price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal(0.011)
        quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount)
        order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10001,
                                       FixtureKucoin.ORDER_PLACE, FixtureKucoin.SELL_MARKET_ORDER)
        [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))
        sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event
        trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log
                                                if isinstance(t, OrderFilledEvent)]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, sell_order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount), sell_order_completed_event.base_asset_amount)
        self.assertEqual("ETH", sell_order_completed_event.base_asset)
        self.assertEqual("USDT", sell_order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded, float(sell_order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded, float(sell_order_completed_event.quote_asset_amount))
        self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0))
        self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id
                             for event in self.market_logger.event_log]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel(self):
        trading_pair = "ETH-USDT"

        current_price: float = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal(0.01)

        price: Decimal = Decimal(current_price) * Decimal(1.1)
        quantized_price: Decimal = self.market.quantize_order_price(trading_pair, price)
        quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount)

        order_id, exch_order_id = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
                                                   quantized_price, 10001,
                                                   FixtureKucoin.ORDER_PLACE_2, FixtureKucoin.OPEN_SELL_LIMIT_ORDER)
        [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent))
        if API_MOCK_ENABLED:
            resp = FixtureKucoin.CANCEL_ORDER.copy()
            resp["data"]["cancelledOrderIds"] = [exch_order_id]
            self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp)
        self.market.cancel(trading_pair, order_id)
        if API_MOCK_ENABLED:
            resp = FixtureKucoin.GET_CANCELLED_ORDER.copy()
            resp["data"]["id"] = exch_order_id
            resp["data"]["clientOid"] = order_id
            self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp)
        [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, order_id)
        self.market_logger.clear()

    def test_cancel_all(self):
        trading_pair = "ETH-USDT"

        bid_price: Decimal = Decimal(self.market_2.get_price(trading_pair, True))
        ask_price: Decimal = Decimal(self.market_2.get_price(trading_pair, False))
        amount: Decimal = Decimal(0.01)
        quantized_amount: Decimal = self.market_2.quantize_order_amount(trading_pair, amount)

        # Intentionally setting high price to prevent getting filled
        quantize_bid_price: Decimal = self.market_2.quantize_order_price(trading_pair, bid_price * Decimal(0.8))
        quantize_ask_price: Decimal = self.market_2.quantize_order_price(trading_pair, ask_price * Decimal(1.2))

        _, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price,
                                            10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_BUY_LIMIT_ORDER)

        _, exch_order_id2 = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price,
                                             10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_SELL_LIMIT_ORDER)

        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            resp = FixtureKucoin.ORDERS_BATCH_CANCELLED.copy()
            resp["data"]["cancelledOrderIds"] = [exch_order_id, exch_order_id2]
            self.web_app.update_response("delete", API_BASE_URL, "/api/v1/orders", resp)
        [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)
        self.market_2_logger.clear()

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ETH-USDT"
        sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name)
        recorder.start()

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

            # Try to put limit buy order for 0.04 ETH, and watch for order creation event.
            current_bid_price: float = self.market.get_price(trading_pair, True)
            bid_price: Decimal = Decimal(current_bid_price * Decimal(0.8))
            quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price)

            amount: Decimal = Decimal(0.04)
            quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount)

            order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
                                                       quantize_bid_price,
                                                       10001, FixtureKucoin.ORDER_PLACE,
                                                       FixtureKucoin.OPEN_BUY_LIMIT_ORDER)
            [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event
            self.assertEqual(order_id, order_created_event.order_id)

            # Verify tracking states
            self.assertEqual(1, len(self.market.tracking_states))
            self.assertEqual(order_id, list(self.market.tracking_states.keys())[0])

            # Verify orders from recorder
            recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market)
            self.assertEqual(1, len(recorded_orders))
            self.assertEqual(order_id, recorded_orders[0].id)

            # Verify saved market states
            saved_market_states: MarketState = recorder.get_market_states(config_path, self.market)
            self.assertIsNotNone(saved_market_states)
            self.assertIsInstance(saved_market_states.saved_state, dict)
            self.assertGreater(len(saved_market_states.saved_state), 0)

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: KucoinExchange = KucoinExchange(
                kucoin_api_key=API_KEY,
                kucoin_passphrase=API_PASSPHRASE,
                kucoin_secret_key=API_SECRET,
                trading_pairs=["ETH-USDT"]
            )
            for event_tag in self.events:
                self.market.add_listener(event_tag, self.market_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(config_path, self.market)
            self.clock.add_iterator(self.market)
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            self.market.restore_tracking_states(saved_market_states.saved_state)
            self.assertEqual(1, len(self.market.limit_orders))
            self.assertEqual(1, len(self.market.tracking_states))

            if API_MOCK_ENABLED:
                resp = FixtureKucoin.CANCEL_ORDER.copy()
                resp["data"]["cancelledOrderIds"] = exch_order_id
                self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp)
            # Cancel the order and verify that the change is saved.
            self.market.cancel(trading_pair, order_id)
            if API_MOCK_ENABLED:
                resp = FixtureKucoin.GET_CANCELLED_ORDER.copy()
                resp["data"]["id"] = exch_order_id
                resp["data"]["clientOid"] = order_id
                self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp)
            self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
            order_id = None
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            saved_market_states = recorder.get_market_states(config_path, self.market)
            self.assertEqual(0, len(saved_market_states.saved_state))
        finally:
            if order_id is not None:
                self.market.cancel(trading_pair, order_id)
                self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
            self.market_logger.clear()

    def test_order_fill_record(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:
            # Try to buy 0.01 ETH from the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal(0.01)
            order_id, _ = self.place_order(True, trading_pair, amount, OrderType.LIMIT, price, 10001,
                                           FixtureKucoin.ORDER_PLACE, FixtureKucoin.BUY_MARKET_ORDER)
            [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of ETH to the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(trading_pair, False)
            amount: Decimal = Decimal(buy_order_completed_event.base_asset_amount)
            order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10002,
                                           FixtureKucoin.ORDER_PLACE, FixtureKucoin.SELL_MARKET_ORDER)
            [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"]
            sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        finally:
            if order_id is not None:
                self.market.cancel(trading_pair, order_id)
                self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
            self.market_logger.clear()

    def test_update_last_prices(self):
        # This is basic test to see if order_book last_trade_price is initiated and updated.
        for order_book in self.market.order_books.values():
            for _ in range(5):
                self.ev_loop.run_until_complete(asyncio.sleep(1))
                self.assertFalse(math.isnan(order_book.last_trade_price))
Esempio n. 8
0
class OkexExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled,
        MarketEvent.OrderCancelled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]

    market: OkexExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def strip_host_from_okex_url(cls, url):
        HOST = "https://www.okex.com"
        return url.split(HOST)[-1]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        if MOCK_API_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
            # mock_account_id = FixtureOKEx.GET_ACCOUNTS["data"][0]["id"]

            # warning: second parameter starts with /
            cls.web_app.update_response(
                "get", API_BASE_URL,
                cls.strip_host_from_okex_url(OKEX_INSTRUMENTS_URL),
                FixtureOKEx.OKEX_INSTRUMENTS_URL)
            cls.web_app.update_response(
                "get", API_BASE_URL,
                cls.strip_host_from_okex_url(OKEX_PRICE_URL).format(
                    trading_pair='ETH-USDT'), FixtureOKEx.INSTRUMENT_TICKER)
            cls.web_app.update_response(
                "get", API_BASE_URL,
                cls.strip_host_from_okex_url(OKEX_DEPTH_URL).format(
                    trading_pair='ETH-USDT'), FixtureOKEx.OKEX_ORDER_BOOK)
            cls.web_app.update_response(
                "get", API_BASE_URL,
                cls.strip_host_from_okex_url(OKEX_TICKERS_URL),
                FixtureOKEx.OKEX_TICKERS)
            cls.web_app.update_response("get", API_BASE_URL,
                                        '/' + OKEX_BALANCE_URL,
                                        FixtureOKEx.OKEX_BALANCE_URL)
            cls.web_app.update_response("get", API_BASE_URL,
                                        '/' + OKEX_SERVER_TIME,
                                        FixtureOKEx.TIMESTAMP)

            # cls.web_app.update_response("POST", API_BASE_URL, '/' + OKEX_PLACE_ORDER, FixtureOKEx.ORDER_PLACE)
            # cls.web_app.update_response("get", OKEX_BASE_URL, f"/v1/account/accounts/{mock_account_id}/balance",
            #                             FixtureOKEx.GET_BALANCES)

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.okex.okex_exchange.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: OkexExchange = OkexExchange(API_KEY,
                                                API_SECRET,
                                                API_PASSPHRASE,
                                                trading_pairs=["ETH-USDT"])
        # Need 2nd instance of market to prevent events mixing up across tests
        cls.market_2: OkexExchange = OkexExchange(API_KEY,
                                                  API_SECRET,
                                                  API_PASSPHRASE,
                                                  trading_pairs=["ETH-USDT"])
        cls.clock.add_iterator(cls.market)
        cls.clock.add_iterator(cls.market_2)
        cls.stack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()
        if MOCK_API_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._t_nonce_patcher.stop()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready and cls.market_2.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../okex_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        self.market_2_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)
            self.market_2.add_listener(event_tag, self.market_2_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
            self.market_2.remove_listener(event_tag, self.market_2_logger)
        self.market_logger = None
        self.market_2_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(0.5)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)
        sell_trade_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10)
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

    def test_fee_overrides_config(self):
        fee_overrides_config_map["okex_taker_fee"].value = None
        taker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.0015"), taker_fee.percent)
        fee_overrides_config_map["okex_taker_fee"].value = Decimal('0.1')
        taker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent)
        fee_overrides_config_map["okex_maker_fee"].value = None
        maker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent)
        fee_overrides_config_map["okex_maker_fee"].value = Decimal('0.5')
        maker_fee: AddedToCostTradeFee = self.market.get_fee(
            "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent)

    def place_order(self,
                    is_buy,
                    trading_pair,
                    amount,
                    order_type,
                    price,
                    nonce,
                    get_resp,
                    market_connector=None):
        global EXCHANGE_ORDER_ID
        order_id, exch_order_id = None, None
        if MOCK_API_ENABLED:
            exch_order_id = f"OKEX_{EXCHANGE_ORDER_ID}"
            EXCHANGE_ORDER_ID += 1
            self._t_nonce_mock.return_value = nonce
            resp = FixtureOKEx.ORDER_PLACE.copy()
            resp["data"][0]["ordId"] = exch_order_id
            # resp = exch_order_id
            side = 'buy' if is_buy else 'sell'
            order_id = f"{side}-{trading_pair}-{nonce}"
            self.web_app.update_response("post", API_BASE_URL,
                                         "/" + OKEX_PLACE_ORDER, resp)
        market = self.market if market_connector is None else market_connector
        if is_buy:
            order_id = market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = market.sell(trading_pair, amount, order_type, price)
        if MOCK_API_ENABLED:
            resp = get_resp.copy()
            # resp is the response passed by parameter
            resp["data"][0]["ordId"] = exch_order_id
            resp["data"][0]["clOrdId"] = order_id
            self.web_app.update_response(
                "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format(
                    ordId=exch_order_id, trading_pair="ETH-USDT"), resp)
        return order_id, exch_order_id

    def cancel_order(self, trading_pair, order_id, exchange_order_id,
                     get_resp):
        if MOCK_API_ENABLED:
            resp = FixtureOKEx.ORDER_CANCEL.copy()
            resp["data"][0]["ordId"] = exchange_order_id
            resp["data"][0]["clOrdId"] = order_id
            self.web_app.update_response("post",
                                         API_BASE_URL,
                                         '/' + OKEX_ORDER_CANCEL,
                                         resp,
                                         params={"ordId": exchange_order_id})
        self.market.cancel(trading_pair, order_id)
        if MOCK_API_ENABLED:
            resp = get_resp.copy()
            resp["data"][0]["ordId"] = exchange_order_id
            resp["data"][0]["clOrdId"] = order_id
            self.web_app.update_response(
                "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format(
                    ordId=exchange_order_id, trading_pair="ETH-USDT"), resp)

    def test_limit_maker_rejections(self):
        if MOCK_API_ENABLED:
            return
        trading_pair = "ETH-USDT"

        # Try to put a buy limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('1.02')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair,
                                                   Decimal("0.06"))

        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER,
                                   price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

        self.market_logger.clear()

        # Try to put a sell limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('0.98')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair,
                                                   Decimal("0.06"))

        order_id = self.market.sell(trading_pair, amount,
                                    OrderType.LIMIT_MAKER, price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

    def test_limit_makers_unfilled(self):
        if MOCK_API_ENABLED:
            return

        trading_pair = "ETH-USDT"

        bid_price: Decimal = self.market.get_price(trading_pair,
                                                   True) * Decimal("0.5")
        ask_price: Decimal = self.market.get_price(trading_pair, False) * 2
        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price * Decimal("0.9"))
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price * Decimal("1.1"))

        order_id1, exch_order_id1 = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001,
            FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id1, order_created_event.order_id)

        order_id2, exch_order_id2 = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002,
            FixtureOKEx.ORDER_GET_LIMIT_SELL_UNFILLED)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id2, order_created_event.order_id)

        self.run_parallel(asyncio.sleep(1))
        if MOCK_API_ENABLED:
            resp = FixtureOKEx.ORDERS_BATCH_CANCELED.copy()
            resp["data"]["success"] = [exch_order_id1, exch_order_id2]
            self.web_app.update_response("post", API_BASE_URL,
                                         "/v1/order/orders/batchcancel", resp)
        [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

        # Reset the logs
        self.market_logger.clear()

    def test_limit_taker_buy(self):
        trading_pair = "ETH-USDT"
        price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(True, trading_pair, quantized_amount,
                                       OrderType.LIMIT, price, 10001,
                                       FixtureOKEx.ORDER_GET_MARKET_BUY)
        [buy_order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, buy_order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               buy_order_completed_event.base_asset_amount,
                               places=4)
        self.assertEqual("ETH", buy_order_completed_event.base_asset)
        self.assertEqual("USDT", buy_order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               buy_order_completed_event.base_asset_amount,
                               places=4)
        self.assertAlmostEqual(quote_amount_traded,
                               buy_order_completed_event.quote_asset_amount,
                               places=4)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_taker_sell(self):
        trading_pair = "ETH-USDT"
        price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(False, trading_pair, amount,
                                       OrderType.LIMIT, price, 10001,
                                       FixtureOKEx.ORDER_GET_MARKET_SELL)
        [sell_order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, sell_order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               sell_order_completed_event.base_asset_amount)
        self.assertEqual("ETH", sell_order_completed_event.base_asset)
        self.assertEqual("USDT", sell_order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               sell_order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               sell_order_completed_event.quote_asset_amount)
        self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0))
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):
        trading_pair = "ETH-USDT"

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("0.05")

        bid_price: Decimal = current_bid_price - Decimal(
            "0.1") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, exch_order_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001,
            FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.cancel_order(trading_pair, order_id, exch_order_id,
                          FixtureOKEx.ORDER_GET_CANCELED)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, order_id)

    def test_cancel_all(self):
        trading_pair = "ETH-USDT"

        bid_price: Decimal = self.market_2.get_price(trading_pair,
                                                     True) * Decimal("0.5")
        ask_price: Decimal = self.market_2.get_price(trading_pair, False) * 2
        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market_2.quantize_order_amount(
            trading_pair, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market_2.quantize_order_price(
            trading_pair, bid_price * Decimal("0.9"))
        quantize_ask_price: Decimal = self.market_2.quantize_order_price(
            trading_pair, ask_price * Decimal("1.1"))

        _, exch_order_id1 = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 1001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED,
            self.market_2)
        _, exch_order_id2 = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 1002, FixtureOKEx.ORDER_GET_LIMIT_BUY_FILLED,
            self.market_2)
        self.run_parallel(asyncio.sleep(1))
        if MOCK_API_ENABLED:
            resp = FixtureOKEx.ORDERS_BATCH_CANCELED.copy()
            resp["data"][0]["ordId"] = exch_order_id1
            self.web_app.update_response("post", API_BASE_URL,
                                         '/' + OKEX_BATCH_ORDER_CANCEL, resp)

        [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, '0')

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ETH-USDT"
        sql: SQLConnectionManager = SQLConnectionManager(
            SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market],
                                                    config_path, strategy_name)
        recorder.start()

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

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

            amount: Decimal = Decimal("0.06")
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, amount)

            order_id, exch_order_id = self.place_order(
                True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
                quantize_bid_price, 10001,
                FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED)
            [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: OkexExchange = OkexExchange(
                API_KEY,
                API_SECRET,
                API_PASSPHRASE,
                trading_pairs=["ETH-USDT", "BTC-USDT"])
            for event_tag in self.events:
                self.market.add_listener(event_tag, self.market_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [self.market], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.clock.add_iterator(self.market)
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            self.market.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(self.market.limit_orders))
            self.assertEqual(1, len(self.market.tracking_states))

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

    def test_order_fill_record(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:
            # Try to buy 0.04 ETH from the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal("0.06")
            order_id, _ = self.place_order(True, trading_pair, amount,
                                           OrderType.LIMIT, price, 10001,
                                           FixtureOKEx.ORDER_GET_MARKET_BUY)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of ETH to the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(trading_pair, False)
            amount = buy_order_completed_event.base_asset_amount
            order_id, _ = self.place_order(False, trading_pair, amount,
                                           OrderType.LIMIT, price, 10002,
                                           FixtureOKEx.ORDER_GET_MARKET_SELL)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        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)

    def test_update_last_prices(self):
        # This is basic test to see if order_book last_trade_price is initiated and updated.
        for order_book in self.market.order_books.values():
            for _ in range(5):
                self.ev_loop.run_until_complete(asyncio.sleep(1))
                self.assertFalse(math.isnan(order_book.last_trade_price))
 def initialize_event_loggers(self):
     self.event_logger = EventLogger()
     # TODO subscribe to other events?
     for event in self.exchange.MARKET_EVENTS:
         self.exchange.add_listener(event, self.event_logger)
         print("subscribing event_logger to ", event)
Esempio n. 10
0
class BinanceMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled,
        MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure
    ]

    market: BinanceMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack
    base_api_url = "api.binance.com"

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.ev_loop = asyncio.get_event_loop()

        if API_MOCK_ENABLED:
            cls.web_app = HummingWebApp.get_instance()
            cls.web_app.add_host_to_mock(
                cls.base_api_url,
                ["/api/v1/ping", "/api/v1/time", "/api/v1/ticker/24hr"])
            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._req_patcher = unittest.mock.patch.object(requests.Session,
                                                          "request",
                                                          autospec=True)
            cls._req_url_mock = cls._req_patcher.start()
            cls._req_url_mock.side_effect = HummingWebApp.reroute_request
            cls.web_app.update_response("get", cls.base_api_url,
                                        "/api/v3/account",
                                        FixtureBinance.BALANCES)
            cls.web_app.update_response("get", cls.base_api_url,
                                        "/api/v1/exchangeInfo",
                                        FixtureBinance.MARKETS)
            cls.web_app.update_response("get", cls.base_api_url,
                                        "/wapi/v3/tradeFee.html",
                                        FixtureBinance.TRADE_FEES)
            cls.web_app.update_response("post", cls.base_api_url,
                                        "/api/v1/userDataStream",
                                        FixtureBinance.LISTEN_KEY)
            cls.web_app.update_response("put", cls.base_api_url,
                                        "/api/v1/userDataStream",
                                        FixtureBinance.LISTEN_KEY)
            cls.web_app.update_response("get",
                                        cls.base_api_url,
                                        "/api/v1/depth",
                                        FixtureBinance.LINKETH_SNAP,
                                        params={'symbol': 'LINKETH'})
            cls.web_app.update_response("get",
                                        cls.base_api_url,
                                        "/api/v1/depth",
                                        FixtureBinance.ZRXETH_SNAP,
                                        params={'symbol': 'ZRXETH'})
            ws_base_url = "wss://stream.binance.com:9443/ws"
            cls._ws_user_url = f"{ws_base_url}/{FixtureBinance.LISTEN_KEY['listenKey']}"
            HummingWsServerFactory.start_new_server(cls._ws_user_url)
            HummingWsServerFactory.start_new_server(
                f"{ws_base_url}/linketh@depth/zrxeth@depth")
            cls._ws_patcher = unittest.mock.patch("websockets.connect",
                                                  autospec=True)
            cls._ws_mock = cls._ws_patcher.start()
            cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.binance.binance_market.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BinanceMarket = BinanceMarket(API_KEY, API_SECRET,
                                                  ["LINK-ETH", "ZRX-ETH"],
                                                  True)
        print("Initializing Binance market... this will take about a minute.")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()
        if API_MOCK_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._req_patcher.stop()
            cls._ws_patcher.stop()
            cls._t_nonce_patcher.stop()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../binance_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        maker_buy_trade_fee: TradeFee = self.market.get_fee(
            "BTC", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1),
            Decimal(4000))
        self.assertGreater(maker_buy_trade_fee.percent, 0)
        self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0)
        taker_buy_trade_fee: TradeFee = self.market.get_fee(
            "BTC", "USDT", OrderType.LIMIT, TradeType.BUY, Decimal(1))
        self.assertGreater(taker_buy_trade_fee.percent, 0)
        self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0)
        sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT",
                                                       OrderType.LIMIT,
                                                       TradeType.SELL,
                                                       Decimal(1),
                                                       Decimal(4000))
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)
        sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT",
                                                       OrderType.LIMIT_MAKER,
                                                       TradeType.SELL,
                                                       Decimal(1),
                                                       Decimal(4000))
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

    def test_fee_overrides_config(self):
        fee_overrides_config_map["binance_taker_fee"].value = None
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent)
        fee_overrides_config_map["binance_taker_fee"].value = Decimal('0.2')
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["binance_maker_fee"].value = None
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT_MAKER,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent)
        fee_overrides_config_map["binance_maker_fee"].value = Decimal('0.5')
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT_MAKER,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent)

    def test_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05"))
        bid_price: Decimal = self.market.get_price("LINK-ETH", True)
        amount: Decimal = 1
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "LINK-ETH", amount)

        order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT,
                                    bid_price, 10001,
                                    FixtureBinance.BUY_MARKET_ORDER,
                                    FixtureBinance.WS_AFTER_BUY_1,
                                    FixtureBinance.WS_AFTER_BUY_2)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("LINK", order_completed_event.base_asset)
        self.assertEqual("ETH", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertGreater(order_completed_event.fee_amount, Decimal(0))
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))

        # Reset the logs
        self.market_logger.clear()

        # Try to sell back the same amount of ZRX to the exchange, and watch for completion event.
        ask_price: Decimal = self.market.get_price("LINK-ETH", False)
        amount = order_completed_event.base_asset_amount
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT,
                                    ask_price, 10002,
                                    FixtureBinance.SELL_MARKET_ORDER,
                                    FixtureBinance.WS_AFTER_SELL_1,
                                    FixtureBinance.WS_AFTER_SELL_2)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("LINK", order_completed_event.base_asset)
        self.assertEqual("ETH", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertGreater(order_completed_event.fee_amount, Decimal(0))
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))

    def test_limit_maker_rejections(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05"))

        # Try to put a buy limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price("LINK-ETH",
                                               True) * Decimal('1.02')
        price: Decimal = self.market.quantize_order_price("LINK-ETH", price)
        amount = self.market.quantize_order_amount("LINK-ETH", 1)

        order_id = self.place_order(True, "LINK-ETH", amount,
                                    OrderType.LIMIT_MAKER, price, 10001,
                                    FixtureBinance.LIMIT_MAKER_ERROR)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

        self.market_logger.clear()

        # Try to put a sell limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price("LINK-ETH",
                                               True) * Decimal('0.98')
        price: Decimal = self.market.quantize_order_price("LINK-ETH", price)
        amount = self.market.quantize_order_amount("LINK-ETH", 1)

        order_id = self.place_order(False, "LINK-ETH", amount,
                                    OrderType.LIMIT_MAKER, price, 10002,
                                    FixtureBinance.LIMIT_MAKER_ERROR)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

    def test_limit_makers_unfilled(self):
        price = self.market.get_price("LINK-ETH", True) * Decimal("0.8")
        price = self.market.quantize_order_price("LINK-ETH", price)
        amount = self.market.quantize_order_amount("LINK-ETH", 1)

        order_id = self.place_order(True, "LINK-ETH", amount,
                                    OrderType.LIMIT_MAKER, price, 10001,
                                    FixtureBinance.OPEN_BUY_ORDER)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id, order_created_event.order_id)

        price = self.market.get_price("LINK-ETH", True) * Decimal("1.2")
        price = self.market.quantize_order_price("LINK-ETH", price)
        amount = self.market.quantize_order_amount("LINK-ETH", 1)

        order_id = self.place_order(False, "LINK-ETH", amount,
                                    OrderType.LIMIT_MAKER, price, 10002,
                                    FixtureBinance.OPEN_SELL_ORDER)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id, order_created_event.order_id)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def fixture(self, fixture_data, **overwrites):
        data = fixture_data.copy()
        for key, value in overwrites.items():
            if key not in data:
                raise Exception(f"{key} not found in fixture_data")
            data[key] = value
        return data

    def order_response(self, fixture_data, nonce, side, trading_pair):
        self._t_nonce_mock.return_value = nonce
        order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}"
        order_resp = fixture_data.copy()
        order_resp["clientOrderId"] = order_id
        return order_resp

    def place_order(self,
                    is_buy,
                    trading_pair,
                    amount,
                    order_type,
                    price,
                    nonce,
                    fixture_resp,
                    fixture_ws_1=None,
                    fixture_ws_2=None):
        order_id = None
        if API_MOCK_ENABLED:
            resp = self.order_response(fixture_resp, nonce,
                                       'buy' if is_buy else 'sell',
                                       trading_pair)
            self.web_app.update_response("post", self.base_api_url,
                                         "/api/v3/order", resp)
        if is_buy:
            order_id = self.market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = self.market.sell(trading_pair, amount, order_type,
                                        price)
        if API_MOCK_ENABLED and fixture_ws_1 is not None and fixture_ws_2 is not None:
            data = self.fixture(fixture_ws_1, c=order_id)
            HummingWsServerFactory.send_json_threadsafe(self._ws_user_url,
                                                        data,
                                                        delay=0.1)
            data = self.fixture(fixture_ws_2, c=order_id)
            HummingWsServerFactory.send_json_threadsafe(self._ws_user_url,
                                                        data,
                                                        delay=0.11)
        return order_id

    def test_cancel_all(self):
        trading_pair = "LINK-ETH"
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        ask_price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = 1
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price * Decimal("0.7"))
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price * Decimal("1.5"))

        buy_id = self.place_order(True, "LINK-ETH", quantized_amount,
                                  OrderType.LIMIT, quantize_bid_price, 10001,
                                  FixtureBinance.OPEN_BUY_ORDER,
                                  FixtureBinance.WS_AFTER_BUY_1,
                                  FixtureBinance.WS_AFTER_BUY_2)

        sell_id = self.place_order(False, "LINK-ETH", quantized_amount,
                                   OrderType.LIMIT, quantize_ask_price, 10002,
                                   FixtureBinance.OPEN_SELL_ORDER,
                                   FixtureBinance.WS_AFTER_SELL_1,
                                   FixtureBinance.WS_AFTER_SELL_2)

        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            resp = self.fixture(FixtureBinance.CANCEL_ORDER,
                                origClientOrderId=buy_id,
                                side="BUY")
            self.web_app.update_response("delete",
                                         self.base_api_url,
                                         "/api/v3/order",
                                         resp,
                                         params={'origClientOrderId': buy_id})
            resp = self.fixture(FixtureBinance.CANCEL_ORDER,
                                origClientOrderId=sell_id,
                                side="SELL")
            self.web_app.update_response("delete",
                                         self.base_api_url,
                                         "/api/v3/order",
                                         resp,
                                         params={'origClientOrderId': sell_id})
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_order_price_precision(self):
        # As of the day this test was written, the min order size (base) is 1 LINK, the min order size (quote) is
        # 0.01 ETH, and order step size is 1 LINK.
        trading_pair = "LINK-ETH"
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        ask_price: Decimal = self.market.get_price(trading_pair, False)
        mid_price: Decimal = (bid_price + ask_price) / 2
        amount: Decimal = Decimal("1.23123216")
        binance_client = self.market.binance_client

        # Make sure there's enough balance to make the limit orders.
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05"))
        self.assertGreater(self.market.get_balance("LINK"), amount * 2)

        # Intentionally set some prices with too many decimal places s.t. they
        # need to be quantized. Also, place them far away from the mid-price s.t. they won't
        # get filled during the test.
        bid_price: Decimal = mid_price * Decimal("0.9333192292111341")
        ask_price: Decimal = mid_price * Decimal("1.0492431474884933")

        # This is needed to get around the min quote amount limit.
        bid_amount: Decimal = Decimal("1.23123216")

        if API_MOCK_ENABLED:
            resp = self.order_response(FixtureBinance.ORDER_BUY_PRECISION,
                                       1000001, "buy", "LINK-ETH")
            self.web_app.update_response("post", self.base_api_url,
                                         "/api/v3/order", resp)
        # Test bid order
        bid_order_id: str = self.market.buy(trading_pair, Decimal(bid_amount),
                                            OrderType.LIMIT,
                                            Decimal(bid_price))
        if API_MOCK_ENABLED:
            resp = FixtureBinance.ORDER_BUY_PRECISION_GET
            resp["clientOrderId"] = bid_order_id
            self.web_app.update_response("get", self.base_api_url,
                                         "/api/v3/order", resp)

        # Wait for the order created event and examine the order made
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent,
                                        timeout_seconds=10))
        order_data: Dict[str, any] = binance_client.get_order(
            symbol=trading_pair, origClientOrderId=bid_order_id)
        quantized_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, Decimal(bid_price))
        bid_size_quantum: Decimal = self.market.get_order_size_quantum(
            trading_pair, Decimal(bid_amount))
        self.assertEqual(quantized_bid_price, Decimal(order_data["price"]))
        self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0)

        # Test ask order
        if API_MOCK_ENABLED:
            resp = self.order_response(FixtureBinance.ORDER_SELL_PRECISION,
                                       1000002, "sell", "LINK-ETH")
            self.web_app.update_response("post", self.base_api_url,
                                         "/api/v3/order", resp)
        ask_order_id: str = self.market.sell(trading_pair,
                                             Decimal(amount), OrderType.LIMIT,
                                             Decimal(ask_price))
        if API_MOCK_ENABLED:
            resp = FixtureBinance.ORDER_SELL_PRECISION_GET
            resp["clientOrderId"] = ask_order_id
            self.web_app.update_response("get", self.base_api_url,
                                         "/api/v3/order", resp)

        # Wait for the order created event and examine and order made
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent,
                                        timeout_seconds=10))
        order_data = binance_client.get_order(symbol=trading_pair,
                                              origClientOrderId=ask_order_id)
        quantized_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, Decimal(ask_price))
        quantized_ask_size: Decimal = self.market.quantize_order_amount(
            trading_pair, Decimal(amount))
        self.assertEqual(quantized_ask_price, Decimal(order_data["price"]))
        self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"]))

        # Cancel all the orders
        if API_MOCK_ENABLED:
            resp = self.fixture(FixtureBinance.CANCEL_ORDER,
                                origClientOrderId=bid_order_id,
                                side="BUY")
            self.web_app.update_response(
                "delete",
                self.base_api_url,
                "/api/v3/order",
                resp,
                params={'origClientOrderId': bid_order_id})
            resp = self.fixture(FixtureBinance.CANCEL_ORDER,
                                origClientOrderId=ask_order_id,
                                side="SELL")
            self.web_app.update_response(
                "delete",
                self.base_api_url,
                "/api/v3/order",
                resp,
                params={'origClientOrderId': ask_order_id})
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_server_time_offset(self):
        time_obj: BinanceTime = binance_client_module.time
        old_check_interval: float = time_obj._server_time_offset_check_interval
        time_obj._server_time_offset_check_interval = 1.0
        time_obj.stop()
        time_obj.start()

        try:
            local_time_offset = (time.time() - time.perf_counter()) * 1e3
            with patch(
                    "hummingbot.connector.exchange.binance.binance_time.time"
            ) as market_time:

                def delayed_time():
                    return time.perf_counter() - 30.0

                market_time.perf_counter = delayed_time
                self.run_parallel(asyncio.sleep(3.0))
                raw_time_offset = BinanceTime.get_instance().time_offset_ms
                time_offset_diff = raw_time_offset - local_time_offset
                # check if it is less than 5% off
                self.assertTrue(time_offset_diff > 10000)
                self.assertTrue(abs(time_offset_diff - 30.0 * 1e3) < 1.5 * 1e3)
        finally:
            time_obj._server_time_offset_check_interval = old_check_interval
            time_obj.stop()
            time_obj.start()

    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.02 ETH worth of ZRX, and watch for order creation event.
            current_bid_price: Decimal = self.market.get_price(
                "LINK-ETH", True)
            bid_price: Decimal = current_bid_price * Decimal("0.8")
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                "LINK-ETH", bid_price)

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

            if API_MOCK_ENABLED:
                resp = self.order_response(FixtureBinance.OPEN_BUY_ORDER,
                                           1000001, "buy", "LINK-ETH")
                self.web_app.update_response("post", self.base_api_url,
                                             "/api/v3/order", resp)
            order_id = self.market.buy("LINK-ETH", quantized_amount,
                                       OrderType.LIMIT, quantize_bid_price)
            [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: BinanceMarket = BinanceMarket(API_KEY, API_SECRET,
                                                       ["LINK-ETH", "ZRX-ETH"],
                                                       True)
            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:
                resp = self.fixture(FixtureBinance.CANCEL_ORDER,
                                    origClientOrderId=order_id,
                                    side="BUY")
                self.web_app.update_response(
                    "delete",
                    self.base_api_url,
                    "/api/v3/order",
                    resp,
                    params={'origClientOrderId': order_id})
            self.market.cancel("LINK-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("LINK-ETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_update_last_prices(self):
        # This is basic test to see if order_book last_trade_price is initiated and updated.
        for order_book in self.market.order_books.values():
            for _ in range(5):
                self.ev_loop.run_until_complete(asyncio.sleep(1))
                print(order_book.last_trade_price)
                self.assertFalse(math.isnan(order_book.last_trade_price))

    def test_order_fill_record(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:
            # Try to buy 1 LINK from the exchange, and watch for completion event.
            bid_price: Decimal = self.market.get_price("LINK-ETH", True)
            amount: Decimal = 1
            order_id = self.place_order(True, "LINK-ETH", amount,
                                        OrderType.LIMIT, bid_price, 10001,
                                        FixtureBinance.BUY_LIMIT_ORDER,
                                        FixtureBinance.WS_AFTER_BUY_1,
                                        FixtureBinance.WS_AFTER_BUY_2)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of LINK to the exchange, and watch for completion event.
            ask_price: Decimal = self.market.get_price("LINK-ETH", False)
            amount = buy_order_completed_event.base_asset_amount
            order_id = self.place_order(False, "LINK-ETH", amount,
                                        OrderType.LIMIT, ask_price, 10002,
                                        FixtureBinance.SELL_LIMIT_ORDER,
                                        FixtureBinance.WS_AFTER_SELL_1,
                                        FixtureBinance.WS_AFTER_SELL_2)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertGreaterEqual(len(trade_fills), 2)
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertGreaterEqual(len(buy_fills), 1)
            self.assertGreaterEqual(len(sell_fills), 1)

            order_id = None

        finally:
            if order_id is not None:
                self.market.cancel("LINK-ETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_pair_convesion(self):
        if API_MOCK_ENABLED:
            return
        for pair in self.market.trading_rules:
            exchange_pair = convert_to_exchange_trading_pair(pair)
            self.assertTrue(exchange_pair in self.market.order_books)
    def setUp(self):
        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size,
                                  self.start_timestamp, self.end_timestamp)
        self.mid_price = 100
        self.time_delay = 15
        self.cancel_order_wait_time = 45

        self.market: MockPaperExchange = MockPaperExchange()
        self.market.set_balanced_order_book(trading_pair=self.trading_pair,
                                            mid_price=self.mid_price,
                                            min_price=1,
                                            max_price=200,
                                            price_step_size=1,
                                            volume_step_size=10)

        self.market.set_balance("COINALPHA", 500)
        self.market.set_balance("WETH", 5000)
        self.market.set_quantization_param(
            QuantizationParams(self.trading_pair, 6, 6, 6, 6))

        self.market_info: MarketTradingPairTuple = MarketTradingPairTuple(
            self.market, self.trading_pair, self.base_asset, self.quote_asset)

        # Define strategies to test
        self.buy_mid_price_strategy: PerformTradeStrategy = PerformTradeStrategy(
            exchange=self.market,
            trading_pair=self.trading_pair,
            is_buy=True,
            spread=self.spread,
            order_amount=Decimal("1.0"),
            price_type=PriceType.MidPrice)

        self.sell_mid_price_strategy: PerformTradeStrategy = PerformTradeStrategy(
            exchange=self.market,
            trading_pair=self.trading_pair,
            is_buy=False,
            spread=self.spread,
            order_amount=Decimal("1.0"),
            price_type=PriceType.MidPrice)

        self.buy_last_price_strategy: PerformTradeStrategy = PerformTradeStrategy(
            exchange=self.market,
            trading_pair=self.trading_pair,
            is_buy=True,
            spread=self.spread,
            order_amount=Decimal("1.0"),
            price_type=PriceType.LastTrade)

        self.sell_last_price_strategy: PerformTradeStrategy = PerformTradeStrategy(
            exchange=self.market,
            trading_pair=self.trading_pair,
            is_buy=False,
            spread=self.spread,
            order_amount=Decimal("1.0"),
            price_type=PriceType.LastTrade)

        self.buy_last_own_trade_price_strategy: PerformTradeStrategy = PerformTradeStrategy(
            exchange=self.market,
            trading_pair=self.trading_pair,
            is_buy=True,
            spread=self.spread,
            order_amount=Decimal("1.0"),
            price_type=PriceType.LastOwnTrade)

        self.sell_last_own_trade_price_strategy: PerformTradeStrategy = PerformTradeStrategy(
            exchange=self.market,
            trading_pair=self.trading_pair,
            is_buy=False,
            spread=self.spread,
            order_amount=Decimal("1.0"),
            price_type=PriceType.LastOwnTrade)

        self.clock.add_iterator(self.market)
        self.maker_order_fill_logger: EventLogger = EventLogger()
        self.cancel_order_logger: EventLogger = EventLogger()
        self.buy_order_completed_logger: EventLogger = EventLogger()
        self.sell_order_completed_logger: EventLogger = EventLogger()

        self.market.add_listener(MarketEvent.BuyOrderCompleted,
                                 self.buy_order_completed_logger)
        self.market.add_listener(MarketEvent.SellOrderCompleted,
                                 self.sell_order_completed_logger)
        self.market.add_listener(MarketEvent.OrderFilled,
                                 self.maker_order_fill_logger)
        self.market.add_listener(MarketEvent.OrderCancelled,
                                 self.cancel_order_logger)
 def setUp(self):
     self.logger_a = EventLogger()
     self.logger_b = EventLogger()
     for event_tag in self.events:
         self.wallet_a.add_listener(event_tag, self.logger_a)
         self.wallet_b.add_listener(event_tag, self.logger_b)
class Web3WalletUnitTest(unittest.TestCase):
    wallet_a: Optional[Web3Wallet] = None
    wallet_b: Optional[Web3Wallet] = None
    erc20_token: Optional[ERC20Token] = None
    events: List[WalletEvent] = [
        WalletEvent.ReceivedAsset,
        WalletEvent.GasUsed,
        WalletEvent.TokenApproved,
        WalletEvent.TransactionFailure
    ]

    logger_a: EventLogger
    logger_b: EventLogger

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.erc20_token_address = conf.test_erc20_token_address
        cls.w3 = Web3(Web3.HTTPProvider(conf.test_web3_provider_list[0]))

        cls.wallet_a = Web3Wallet(
            conf.web3_test_private_key_a, conf.test_web3_provider_list, [cls.erc20_token_address])
        cls.wallet_b = Web3Wallet(
            conf.web3_test_private_key_b, conf.test_web3_provider_list, [cls.erc20_token_address])

        cls.erc20_token: ERC20Token = list(cls.wallet_a.current_backend.erc20_tokens.values())[0]

        cls.clock.add_iterator(cls.wallet_a)
        cls.clock.add_iterator(cls.wallet_b)
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()

        next_iteration = (time.time() // 5.0 + 1) * 5
        cls.ev_loop.run_until_complete(cls.clock.run_til(next_iteration))

    def setUp(self):
        self.logger_a = EventLogger()
        self.logger_b = EventLogger()
        for event_tag in self.events:
            self.wallet_a.add_listener(event_tag, self.logger_a)
            self.wallet_b.add_listener(event_tag, self.logger_b)

    def tearDown(self):
        for event_tag in self.events:
            self.wallet_a.remove_listener(event_tag, self.logger_a)
            self.wallet_b.remove_listener(event_tag, self.logger_b)
        self.logger_a = None
        self.logger_b = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_send_balances(self):
        # Check the initial conditions. There should be a certain number of initial tokens before the test can be
        # carried out.
        self.assertGreater(self.wallet_a.get_balance("ETH"), 1.0)
        self.assertGreater(self.wallet_b.get_balance("ETH"), 1.0)
        self.assertGreater(self.wallet_a.get_balance("BNB"), 0.1)
        self.assertGreater(self.wallet_b.get_balance("BNB"), 0.1)

        # Send some Ether between wallets.
        eth_tx_hash: str = self.wallet_a.send(self.wallet_b.address, "ETH", 0.1)
        bnb_tx_hash: str = self.wallet_b.send(self.wallet_a.address, "BNB", 0.01)
        bnb_asset_received, eth_asset_received, eth_gas_used, bnb_gas_used = self.run_parallel(
            self.logger_a.wait_for(WalletReceivedAssetEvent),
            self.logger_b.wait_for(WalletReceivedAssetEvent),
            self.logger_a.wait_for(EthereumGasUsedEvent),
            self.logger_b.wait_for(EthereumGasUsedEvent)
        )
        eth_asset_received: WalletReceivedAssetEvent = eth_asset_received
        eth_gas_used: EthereumGasUsedEvent = eth_gas_used
        self.assertEqual(eth_tx_hash, eth_asset_received.tx_hash)
        self.assertEqual(self.wallet_a.address, eth_asset_received.from_address)
        self.assertEqual(self.wallet_b.address, eth_asset_received.to_address)
        self.assertEqual("ETH", eth_asset_received.asset_name)
        self.assertEqual(0.1, eth_asset_received.amount_received)
        self.assertEqual(int(1e17), eth_asset_received.raw_amount_received)
        self.assertEqual(eth_tx_hash, eth_gas_used.tx_hash)
        self.assertEqual(21000, eth_gas_used.gas_used)

        bnb_asset_received: WalletReceivedAssetEvent = bnb_asset_received
        bnb_gas_used: EthereumGasUsedEvent = bnb_gas_used
        self.assertEqual(bnb_tx_hash, bnb_asset_received.tx_hash)
        self.assertEqual(self.wallet_b.address, bnb_asset_received.from_address)
        self.assertEqual(self.wallet_a.address, bnb_asset_received.to_address)
        self.assertEqual("BNB", bnb_asset_received.asset_name)
        self.assertEqual(0.01, bnb_asset_received.amount_received)
        self.assertEqual(int(1e16), bnb_asset_received.raw_amount_received)
        self.assertEqual(bnb_tx_hash, bnb_gas_used.tx_hash)
        self.assertTrue(bnb_gas_used.gas_used > 21000)

        # Send out the reverse transactions.
        self.wallet_b.send(self.wallet_a.address, "ETH", 0.1)
        self.wallet_a.send(self.wallet_b.address, "BNB", 0.01)

    def test_transaction_failure(self):
        # Produce a transfer failure, by not transferring more than the account has.
        erc20_token_contract: Contract = self.erc20_token.contract
        failure_hash: str = self.wallet_a.execute_transaction(
            erc20_token_contract.functions.transfer(self.wallet_b.address, int(1e30)), gas=500000
        )
        failure_tx, gas_used_event = self.run_parallel(
            self.logger_a.wait_for(str),
            self.logger_a.wait_for(EthereumGasUsedEvent)
        )
        failure_tx: str = failure_tx
        gas_used_event: EthereumGasUsedEvent = gas_used_event
        self.assertEqual(failure_hash, failure_tx)
        self.assertGreater(gas_used_event.gas_used, 21000)

    def test_token_approval(self):
        approval_hash: str = self.wallet_a.approve_token_transfer(self.erc20_token.symbol, self.wallet_b.address, 1.0)
        approval_event, gas_used_event = self.run_parallel(
            self.logger_a.wait_for(TokenApprovedEvent),
            self.logger_a.wait_for(EthereumGasUsedEvent)
        )
        approval_event: TokenApprovedEvent = approval_event
        gas_used_event: EthereumGasUsedEvent = gas_used_event
        self.assertEqual(approval_hash, approval_event.tx_hash)
        self.assertEqual(approval_hash, gas_used_event.tx_hash)
        self.assertEqual(self.wallet_a.address, approval_event.owner_address)
        self.assertEqual(self.wallet_b.address, approval_event.spender_address)
        self.assertEqual(self.erc20_token.symbol, approval_event.asset_name)
        self.assertEqual(1.0, approval_event.amount)
        self.assertEqual(int(1e18), approval_event.raw_amount)

        self.wallet_a.approve_token_transfer(self.erc20_token.symbol, self.wallet_b.address, 0.0)
        self.run_parallel(
            self.logger_a.wait_for(TokenApprovedEvent),
            self.logger_a.wait_for(EthereumGasUsedEvent)
        )
class CoinbaseProMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled,
        MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled
    ]

    market: CoinbaseProMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: CoinbaseProMarket = CoinbaseProMarket(
            conf.coinbase_pro_api_key,
            conf.coinbase_pro_secret_key,
            conf.coinbase_pro_passphrase,
            symbols=["ETH-USDC", "ETH-USD"]
        )
        print("Initializing Coinbase Pro market... this will take about a minute.")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../coinbase_pro_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_limit_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        symbol = "ETH-USDC"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

        current_bid_price: float = self.market.get_price(symbol, True)
        bid_price: float = current_bid_price + 0.05 * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price)

        order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log
                                                if isinstance(t, OrderFilledEvent)]
        base_amount_traded: float = sum(t.amount for t in trade_events)
        quote_amount_traded: float = sum(t.amount * t.price for t in trade_events)

        self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount))
        self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id
                             for event in self.market_logger.event_log]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_sell(self):
        symbol = "ETH-USDC"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)
        current_ask_price: float = self.market.get_price(symbol, False)
        ask_price: float = current_ask_price - 0.05 * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(symbol, ask_price)

        order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount))
        self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id
                             for event in self.market_logger.event_log]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_market_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        symbol = "ETH-USD"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

        order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log
                                                if isinstance(t, OrderFilledEvent)]
        base_amount_traded: float = sum(t.amount for t in trade_events)
        quote_amount_traded: float = sum(t.amount * t.price for t in trade_events)

        self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USD", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount))
        self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id
                             for event in self.market_logger.event_log]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_market_sell(self):
        symbol = "ETH-USD"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

        order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USD", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount))
        self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id
                             for event in self.market_logger.event_log]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):
        self.assertGreater(self.market.get_balance("ETH"), 10)
        symbol = "ETH-USDC"

        current_bid_price: float = self.market.get_price(symbol, True)
        amount: float = 10 / current_bid_price

        bid_price: float = current_bid_price - 0.1 * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price)
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

        client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price)
        self.market.cancel(symbol, client_order_id)
        [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, client_order_id)

    def test_cancel_all(self):
        symbol = "ETH-USDC"
        bid_price: float = self.market.get_price(symbol, True) * 0.5
        ask_price: float = self.market.get_price(symbol, False) * 2
        amount: float = 10 / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price * 0.7)
        quantize_ask_price: Decimal = self.market.quantize_order_price(symbol, ask_price * 1.5)

        self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price)
        self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price)
        self.run_parallel(asyncio.sleep(1))
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.")
    def test_list_orders(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        symbol = "ETH-USDC"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

        current_bid_price: float = self.market.get_price(symbol, True)
        bid_price: float = current_bid_price + 0.05 * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price)

        self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price)
        self.run_parallel(asyncio.sleep(1))
        [order_details] = self.run_parallel(self.market.list_orders())
        self.assertGreaterEqual(len(order_details), 1)

        self.market_logger.clear()

    def test_deposit_info(self):
        [deposit_info] = self.run_parallel(
            self.market.get_deposit_info("ETH")
        )
        deposit_info: DepositInfo = deposit_info
        self.assertIsInstance(deposit_info, DepositInfo)
        self.assertGreater(len(deposit_info.address), 0)

    @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.")
    def test_withdraw(self):
        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("ZRX"), 1)

        # Withdraw ZRX from Coinbase Pro to test wallet.
        self.market.withdraw(self.wallet.address, "ZRX", 1)
        [withdraw_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketWithdrawAssetEvent)
        )
        withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event
        self.assertEqual(self.wallet.address, withdraw_asset_event.to_address)
        self.assertEqual("ZRX", withdraw_asset_event.asset_name)
        self.assertEqual(1, withdraw_asset_event.amount)
        self.assertEqual(withdraw_asset_event.fee_amount, 0)

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "ETH-USDC"
        sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name)
        recorder.start()

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

            # Try to put limit buy order for 0.04 ETH, and watch for order creation event.
            current_bid_price: float = self.market.get_price(symbol, True)
            bid_price: float = current_bid_price * 0.8
            quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price)

            amount: float = 0.04
            quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

            order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price)
            [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: CoinbaseProMarket = CoinbaseProMarket(
                coinbase_pro_api_key=conf.coinbase_pro_api_key,
                coinbase_pro_secret_key=conf.coinbase_pro_secret_key,
                coinbase_pro_passphrase=conf.coinbase_pro_passphrase,
                symbols=["ETH-USDC", "ETH-USD"]
            )
            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.market.cancel(symbol, 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(symbol, order_id)
                self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_order_fill_record(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "ETH-USDC"
        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:
            # Try to buy 0.04 ETH from the exchange, and watch for completion event.
            current_price: float = self.market.get_price(symbol, True)
            amount: float = 0.04
            order_id = self.market.buy(symbol, amount)
            [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of ETH to the exchange, and watch for completion event.
            amount = float(buy_order_completed_event.base_asset_amount)
            order_id = self.market.sell(symbol, amount)
            [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"]
            sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        finally:
            if order_id is not None:
                self.market.cancel(symbol, order_id)
                self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
Esempio n. 15
0
class BambooRelayOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[BambooRelayOrderBookTracker] = None
    events: List[OrderBookEvent] = [
        OrderBookEvent.TradeEvent
    ]
    trading_pairs: List[str] = [
        "WETH-DAI",
        "ZRX-WETH",
        "WETH-USDC",
        "DAI-USDC"
    ]
    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: BambooRelayOrderBookTracker = BambooRelayOrderBookTracker(
            trading_pairs=cls.trading_pairs
        )
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        await cls.order_book_tracker._order_books_initialized.wait()

    async def run_parallel_async(self, *tasks, timeout=None):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        timer = 0
        while not future.done():
            if timeout and timer > timeout:
                raise Exception("Time out running parallel async task in tests.")
            timer += 1
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def setUp(self):
        self.event_logger = EventLogger()
        for event_tag in self.events:
            for trading_pair, order_book in self.order_book_tracker.order_books.items():
                order_book.add_listener(event_tag, self.event_logger)

    @unittest.skipUnless(any("test_order_book_trade_event_emission" in arg for arg in sys.argv),
                         "test_order_book_trade_event_emission test requires waiting or manual trade.")
    def test_order_book_trade_event_emission(self):
        """
        Test if order book tracker is able to retrieve order book trade message from exchange and
        emit order book trade events after correctly parsing the trade messages
        """
        self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent))
        for ob_trade_event in self.event_logger.event_log:
            self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent)
            self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs)
            self.assertTrue(type(ob_trade_event.timestamp) in [float, int])
            self.assertTrue(type(ob_trade_event.amount) == float)
            self.assertTrue(type(ob_trade_event.price) == float)
            self.assertTrue(type(ob_trade_event.type) == TradeType)
            self.assertTrue(math.ceil(math.log10(ob_trade_event.timestamp)) == 10)
            self.assertTrue(ob_trade_event.amount > 0)
            self.assertTrue(ob_trade_event.price > 0)

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        weth_dai_book: OrderBook = order_books["WETH-DAI"]
        zrx_weth_book: OrderBook = order_books["ZRX-WETH"]
        # print(weth_dai_book.snapshot)
        # print(zrx_weth_book.snapshot)
        self.assertGreaterEqual(weth_dai_book.get_price_for_volume(True, 10).result_price,
                                weth_dai_book.get_price(True))
        self.assertLessEqual(weth_dai_book.get_price_for_volume(False, 10).result_price,
                             weth_dai_book.get_price(False))
        self.assertGreaterEqual(zrx_weth_book.get_price_for_volume(True, 10).result_price,
                                zrx_weth_book.get_price(True))
        self.assertLessEqual(zrx_weth_book.get_price_for_volume(False, 10).result_price,
                             zrx_weth_book.get_price(False))
Esempio n. 16
0
class BeaxyExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure,
    ]
    market: BeaxyExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    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(PRIVET_API_BASE_URL, [])
            cls.web_app.add_host_to_mock(PUBLIC_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", PUBLIC_API_BASE_URL,
                                        "/api/v2/symbols",
                                        FixtureBeaxy.SYMBOLS)
            cls.web_app.update_response("get", PUBLIC_API_BASE_URL,
                                        "/api/v2/symbols/DASHBTC/book",
                                        FixtureBeaxy.TRADE_BOOK)
            cls.web_app.update_response("get", PUBLIC_API_BASE_URL,
                                        "/api/v2/symbols/DASHBTC/rate",
                                        FixtureBeaxy.EXCHANGE_RATE)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/health", FixtureBeaxy.HEALTH)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/wallets",
                                        FixtureBeaxy.BALANCES)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/tradingsettings",
                                        FixtureBeaxy.TRADE_SETTINGS)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/orders/open",
                                        FixtureBeaxy.ORDERS_OPEN_EMPTY)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/orders/closed",
                                        FixtureBeaxy.ORDERS_CLOSED_EMPTY)

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.beaxy.beaxy_exchange.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()

            MockWebSocketServerFactory.url_host_only = True
            MockWebSocketServerFactory.start_new_server(
                BeaxyConstants.TradingApi.WS_BASE_URL)
            MockWebSocketServerFactory.start_new_server(
                BeaxyConstants.PublicApi.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

            cls._auth_confirm_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._BeaxyAuth__login_confirm"
            )
            cls._auth_confirm_mock = cls._auth_confirm_patcher.start()
            cls._auth_session_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._BeaxyAuth__get_session_data"
            )
            cls._auth_session_mock = cls._auth_session_patcher.start()
            cls._auth_session_mock.return_value = {
                "sign_key": 123,
                "session_id": '123'
            }
            cls._auth_headers_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth.get_token"
            )
            cls._auth_headers_mock = cls._auth_headers_patcher.start()
            cls._auth_headers_mock.return_value = '123'
            cls._auth_poll_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._auth_token_polling_loop"
            )
            cls._auth_poll_mock = cls._auth_poll_patcher.start()

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BeaxyExchange = BeaxyExchange(API_KEY,
                                                  API_SECRET,
                                                  trading_pairs=["DASH-BTC"])

        if API_MOCK_ENABLED:

            async def mock_status_polling_task():
                pass

            # disable status polling as it will make orders update inconsistent from mock view
            cls.market._status_polling_task = asyncio.ensure_future(
                mock_status_polling_task())
            cls.ev_loop.run_until_complete(cls.market._update_balances())

        print("Initializing Beaxy 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.")

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../beaxy_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def test_balances(self):
        balances = self.market.get_all_balances()
        self.assertGreater(len(balances), 0)

    def test_get_fee(self):
        limit_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_fee_overrides_config(self):
        fee_overrides_config_map["beaxy_taker_fee"].value = None
        taker_fee: AddedToCostTradeFee = self.market.get_fee(
            "BTC", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.0025"), taker_fee.percent)
        fee_overrides_config_map["beaxy_taker_fee"].value = Decimal('0.2')
        taker_fee: AddedToCostTradeFee = self.market.get_fee(
            "BTC", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["beaxy_maker_fee"].value = None
        maker_fee: AddedToCostTradeFee = self.market.get_fee(
            "BTC", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent)
        fee_overrides_config_map["beaxy_maker_fee"].value = Decimal('0.75')
        maker_fee: AddedToCostTradeFee = self.market.get_fee(
            "BTC", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
            Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent)

    def place_order(self,
                    is_buy,
                    trading_pair,
                    amount,
                    order_type,
                    price,
                    ws_resps=[]):
        global EXCHANGE_ORDER_ID
        order_id, exch_order_id = None, None

        if is_buy:
            order_id = self.market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = self.market.sell(trading_pair, amount, order_type,
                                        price)
        if API_MOCK_ENABLED:
            for delay, ws_resp in ws_resps:
                MockWebSocketServerFactory.send_str_threadsafe(
                    BeaxyConstants.TradingApi.WS_BASE_URL,
                    ws_resp,
                    delay=delay)
        return order_id, exch_order_id

    def cancel_order(self, trading_pair, order_id, exch_order_id):
        self.market.cancel(trading_pair, order_id)

    def test_limit_buy(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_LIMIT_BUY_ORDER)

        amount: Decimal = Decimal("0.01")

        self.assertGreater(self.market.get_balance("BTC"), 0.00005)
        trading_pair = "DASH-BTC"

        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal(1.1)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT, price,
            [(3, FixtureBeaxy.TEST_LIMIT_BUY_WS_ORDER_CREATED),
             (5, FixtureBeaxy.TEST_LIMIT_BUY_WS_ORDER_COMPLETED)])
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("DASH", order_completed_event.base_asset)
        self.assertEqual("BTC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_sell(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_LIMIT_SELL_ORDER)

        trading_pair = "DASH-BTC"
        self.assertGreater(self.market.get_balance("DASH"), 0.01)

        price: Decimal = self.market.get_price(trading_pair,
                                               False) * Decimal(0.9)
        amount: Decimal = Decimal("0.01")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT, price,
            [(3, FixtureBeaxy.TEST_LIMIT_SELL_WS_ORDER_CREATED),
             (5, FixtureBeaxy.TEST_LIMIT_SELL_WS_ORDER_COMPLETED)])
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("DASH", order_completed_event.base_asset)
        self.assertEqual("BTC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_maker_rejections(self):
        if API_MOCK_ENABLED:
            return
        trading_pair = "DASH-BTC"

        # Try to put a buy limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('1.02')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair, 1)

        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER,
                                   price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

        self.market_logger.clear()

        # Try to put a sell limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('0.98')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair, 1)

        order_id = self.market.sell(trading_pair, amount,
                                    OrderType.LIMIT_MAKER, price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

    def test_limit_makers_unfilled(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_UNFILLED_ORDER1)

        self.assertGreater(self.market.get_balance("BTC"), 0.00005)
        trading_pair = "DASH-BTC"

        current_bid_price: Decimal = self.market.get_price(
            trading_pair, True) * Decimal('0.8')
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_bid_price)
        bid_amount: Decimal = Decimal('0.01')
        quantized_bid_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, bid_amount)

        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_ask_price)
        ask_amount: Decimal = Decimal('0.01')
        quantized_ask_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, ask_amount)

        order_id1, exch_order_id_1 = self.place_order(
            True, trading_pair, quantized_bid_amount, OrderType.LIMIT,
            quantize_bid_price,
            [(3, FixtureBeaxy.TEST_UNFILLED_ORDER1_WS_ORDER_CREATED)])
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id1, order_created_event.order_id)

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_UNFILLED_ORDER2)

        order_id2, exch_order_id_2 = self.place_order(
            False, trading_pair, quantized_ask_amount, OrderType.LIMIT,
            quantize_ask_price,
            [(3, FixtureBeaxy.TEST_UNFILLED_ORDER2_WS_ORDER_CREATED)])
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id2, order_created_event.order_id)

        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_str_threadsafe(
                BeaxyConstants.TradingApi.WS_BASE_URL,
                FixtureBeaxy.TEST_UNFILLED_ORDER1_WS_ORDER_CANCELED,
                delay=3)
            MockWebSocketServerFactory.send_str_threadsafe(
                BeaxyConstants.TradingApi.WS_BASE_URL,
                FixtureBeaxy.TEST_UNFILLED_ORDER2_WS_ORDER_CANCELED,
                delay=3)

            self.web_app.update_response("delete", PRIVET_API_BASE_URL,
                                         "/api/v1/orders", "")

        self.run_parallel(asyncio.sleep(1))
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_market_buy(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_MARKET_BUY_ORDER)

        amount: Decimal = Decimal("0.01")

        self.assertGreater(self.market.get_balance("BTC"), 0.00005)
        trading_pair = "DASH-BTC"

        price: Decimal = self.market.get_price(trading_pair, True)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(
            True, trading_pair, quantized_amount, OrderType.MARKET, price,
            [(3, FixtureBeaxy.TEST_MARKET_BUY_WS_ORDER_CREATED),
             (5, FixtureBeaxy.TEST_MARKET_BUY_WS_ORDER_COMPLETED)])
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("DASH", order_completed_event.base_asset)
        self.assertEqual("BTC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_market_sell(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_MARKET_SELL_ORDER)

        trading_pair = "DASH-BTC"
        self.assertGreater(self.market.get_balance("DASH"), 0.01)

        price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal("0.01")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(
            False, trading_pair, quantized_amount, OrderType.MARKET, price,
            [(3, FixtureBeaxy.TEST_MARKET_SELL_WS_ORDER_CREATED),
             (5, FixtureBeaxy.TEST_MARKET_SELL_WS_ORDER_COMPLETED)])
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("DASH", order_completed_event.base_asset)
        self.assertEqual("BTC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_CANCEL_BUY_ORDER)

            self.web_app.update_response(
                "delete", PRIVET_API_BASE_URL,
                "/api/v2/orders/open/435118B0-A7F7-40D2-A409-820E8FC342A2", '')

        amount: Decimal = Decimal("0.01")

        self.assertGreater(self.market.get_balance("BTC"), 0.00005)
        trading_pair = "DASH-BTC"

        # make worst price so order wont be executed
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('0.5')
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, exch_order_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT, price,
            [(3, FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER_COMPLETED)])
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))

        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_str_threadsafe(
                BeaxyConstants.TradingApi.WS_BASE_URL,
                FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER_CANCELED,
                delay=3)

        self.cancel_order(trading_pair, order_id, exch_order_id)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, order_id)

    def test_cancel_all(self):

        if API_MOCK_ENABLED:
            self.web_app.update_response(
                "delete", PRIVET_API_BASE_URL,
                "/api/v2/orders/open/435118B0-A7F7-40D2-A409-820E8FC342A2", '')

        self.assertGreater(self.market.get_balance("BTC"), 0.00005)
        self.assertGreater(self.market.get_balance("DASH"), 0.01)
        trading_pair = "DASH-BTC"

        # make worst price so order wont be executed
        current_bid_price: Decimal = self.market.get_price(
            trading_pair, True) * Decimal('0.5')
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_bid_price)
        bid_amount: Decimal = Decimal('0.01')
        quantized_bid_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, bid_amount)

        # make worst price so order wont be executed
        current_ask_price: Decimal = self.market.get_price(
            trading_pair, False) * Decimal('2')
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_ask_price)
        ask_amount: Decimal = Decimal('0.01')
        quantized_ask_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, ask_amount)

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_CANCEL_ALL_ORDER1)

        _, exch_order_id_1 = self.place_order(True, trading_pair,
                                              quantized_bid_amount,
                                              OrderType.LIMIT_MAKER,
                                              quantize_bid_price)

        if API_MOCK_ENABLED:
            self.web_app.update_response("post", PRIVET_API_BASE_URL,
                                         "/api/v2/orders",
                                         FixtureBeaxy.TEST_CANCEL_ALL_ORDER2)

        _, exch_order_id_2 = self.place_order(False, trading_pair,
                                              quantized_ask_amount,
                                              OrderType.LIMIT_MAKER,
                                              quantize_ask_price)
        self.run_parallel(asyncio.sleep(1))

        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))

        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_str_threadsafe(
                BeaxyConstants.TradingApi.WS_BASE_URL,
                FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER1_CANCELED,
                delay=3)
            MockWebSocketServerFactory.send_str_threadsafe(
                BeaxyConstants.TradingApi.WS_BASE_URL,
                FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER2_CANCELED,
                delay=3)

        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_cancel_empty(self):
        trading_pair = "DASH-BTC"
        self.cancel_order(trading_pair, '123', '123')
Esempio n. 17
0
class RadarRelayMarketUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderExpired,
        MarketEvent.OrderFilled,
        MarketEvent.WithdrawAsset,
    ]

    wallet_events: List[WalletEvent] = [
        WalletEvent.WrappedEth, WalletEvent.UnwrappedEth
    ]

    wallet: Web3Wallet
    market: RadarRelayMarket
    market_logger: EventLogger
    wallet_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.wallet = Web3Wallet(private_key=conf.web3_private_key_radar,
                                backend_urls=conf.test_web3_provider_list,
                                erc20_token_addresses=[
                                    conf.mn_zerox_token_address,
                                    conf.mn_weth_token_address
                                ],
                                chain=EthereumChain.MAIN_NET)
        cls.market: RadarRelayMarket = RadarRelayMarket(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            symbols=["ZRX-WETH"])
        print("Initializing Radar Relay market... ")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.wallet)
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(
            join(__file__, "../radar_relay_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        self.wallet_logger = EventLogger()
        for event_tag in self.market_events:
            self.market.add_listener(event_tag, self.market_logger)
        for event_tag in self.wallet_events:
            self.wallet.add_listener(event_tag, self.wallet_logger)

    def tearDown(self):
        for event_tag in self.market_events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None
        for event_tag in self.wallet_events:
            self.wallet.remove_listener(event_tag, self.wallet_logger)
        self.wallet_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        maker_buy_trade_fee: TradeFee = self.market.get_fee(
            "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, Decimal(20),
            Decimal(0.01))
        self.assertEqual(maker_buy_trade_fee.percent, 0)
        self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0)
        taker_buy_trade_fee: TradeFee = self.market.get_fee(
            "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, Decimal(20))
        self.assertEqual(taker_buy_trade_fee.percent, 0)
        self.assertEqual(len(taker_buy_trade_fee.flat_fees), 1)
        self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH")

    def test_get_wallet_balances(self):
        balances = self.market.get_all_balances()
        self.assertGreaterEqual((balances["ETH"]), 0)
        self.assertGreaterEqual((balances["WETH"]), 0)

    def test_single_limit_order_cancel(self):
        symbol: str = "ZRX-WETH"
        current_price: float = self.market.get_price(symbol, True)
        amount: Decimal = Decimal(10)
        expires = int(time.time() + 60 * 5)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        buy_order_id = self.market.buy(symbol=symbol,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=Decimal(current_price -
                                                     0.2 * current_price),
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        self.assertEqual(quantized_amount,
                         Decimal(buy_order_opened_event.amount))

        self.run_parallel(self.market.cancel_order(buy_order_id))
        [buy_order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_cancelled_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_limit_buy_and_sell_and_cancel_all(self):
        symbol: str = "ZRX-WETH"
        current_price: float = self.market.get_price(symbol, True)
        amount: Decimal = Decimal(10)
        expires = int(time.time() + 60 * 5)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        buy_order_id = self.market.buy(symbol=symbol,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=Decimal(current_price -
                                                     0.2 * current_price),
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(buy_order_id, buy_order_opened_event.order_id)
        self.assertEqual(quantized_amount,
                         Decimal(buy_order_opened_event.amount))
        self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)

        # Reset the logs
        self.market_logger.clear()

        sell_order_id = self.market.sell(symbol=symbol,
                                         amount=amount,
                                         order_type=OrderType.LIMIT,
                                         price=Decimal(current_price +
                                                       0.2 * current_price),
                                         expiration_ts=expires)
        [sell_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        self.assertEqual(sell_order_id, sell_order_opened_event.order_id)
        self.assertEqual(quantized_amount,
                         Decimal(sell_order_opened_event.amount))
        self.assertEqual("ZRX-WETH", sell_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type)

        [cancellation_results
         ] = self.run_parallel(self.market.cancel_all(60 * 5))
        self.assertEqual(cancellation_results[0],
                         CancellationResult(buy_order_id, True))
        self.assertEqual(cancellation_results[1],
                         CancellationResult(sell_order_id, True))
        # Reset the logs
        self.market_logger.clear()

    def test_order_expire(self):
        symbol: str = "ZRX-WETH"
        current_price: float = self.market.get_price(symbol, True)
        amount: Decimal = Decimal(10)
        expires = int(time.time() + 60 * 2)  # expires in 2 min
        self.market.buy(symbol=symbol,
                        amount=amount,
                        order_type=OrderType.LIMIT,
                        price=Decimal(current_price - 0.2 * current_price),
                        expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))

        self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        [buy_order_expired_event] = self.run_parallel(
            self.market_logger.wait_for(OrderExpiredEvent, 60 * 3))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_expired_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_market_buy(self):
        amount: Decimal = Decimal(5)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRX-WETH", amount)
        order_id = self.market.buy("ZRX-WETH", amount, OrderType.MARKET)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual("ZRX", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_market_sell(self):
        amount: Decimal = Decimal(5)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRX-WETH", amount)
        order_id = self.market.sell("ZRX-WETH", amount, OrderType.MARKET)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual("ZRX", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_wrap_eth(self):
        amount_to_wrap = 0.01
        tx_hash = self.wallet.wrap_eth(amount_to_wrap)
        [tx_completed_event] = self.run_parallel(
            self.wallet_logger.wait_for(WalletWrappedEthEvent))
        tx_completed_event: WalletWrappedEthEvent = tx_completed_event

        self.assertEqual(tx_hash, tx_completed_event.tx_hash)
        self.assertEqual(amount_to_wrap, tx_completed_event.amount)
        self.assertEqual(self.wallet.address, tx_completed_event.address)

    def test_unwrap_eth(self):
        amount_to_unwrap = 0.01
        tx_hash = self.wallet.unwrap_eth(amount_to_unwrap)
        [tx_completed_event] = self.run_parallel(
            self.wallet_logger.wait_for(WalletUnwrappedEthEvent))
        tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event

        self.assertEqual(tx_hash, tx_completed_event.tx_hash)
        self.assertEqual(amount_to_unwrap, tx_completed_event.amount)
        self.assertEqual(self.wallet.address, tx_completed_event.address)

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "ZRX-WETH"
        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["limit_orders"]))

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

            amount: float = 0.05 / bid_price
            quantized_amount: Decimal = self.market.quantize_order_amount(
                symbol, Decimal(amount))

            expires = int(time.time() + 60 * 5)
            order_id = self.market.buy(symbol,
                                       quantized_amount,
                                       OrderType.LIMIT,
                                       quantize_bid_price,
                                       expiration_ts=expires)
            [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["limit_orders"]))
            self.assertEqual(
                order_id,
                list(self.market.tracking_states["limit_orders"].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.assertIsInstance(
                saved_market_states.saved_state["limit_orders"], dict)
            self.assertGreater(
                len(saved_market_states.saved_state["limit_orders"]), 0)

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.market_events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: RadarRelayMarket = RadarRelayMarket(
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_web3_provider_list[0],
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                symbols=["ZRX-WETH"])
            for event_tag in self.market_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["limit_orders"]))
            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["limit_orders"]))

            # Cancel the order and verify that the change is saved.
            self.market.cancel(symbol, order_id)
            self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
            order_id = None
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(1,
                             len(self.market.tracking_states["limit_orders"]))
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.assertEqual(
                1, len(saved_market_states.saved_state["limit_orders"]))
        finally:
            if order_id is not None:
                self.market.cancel(symbol, order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_order_fill_record(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "ZRX-WETH"
        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:
            # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price(symbol, True)
            amount: Decimal = Decimal("0.05" / current_price)
            order_id = self.market.buy(symbol, amount)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            # Try to sell back the same amount of ZRX to the exchange, and watch for completion event.
            amount = Decimal(buy_order_completed_event.base_asset_amount)
            order_id = self.market.sell(symbol, amount)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        finally:
            if order_id is not None:
                self.market.cancel(symbol, order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
Esempio n. 18
0
class DolomiteMarketUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
    ]

    wallet_events: List[WalletEvent] = [
        WalletEvent.WrappedEth, WalletEvent.UnwrappedEth
    ]

    wallet: Web3Wallet
    market: DolomiteMarket
    market_logger: EventLogger
    wallet_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.wallet = Web3Wallet(
            private_key=conf.dolomite_test_web3_private_key,
            backend_urls=conf.test_web3_provider_list,
            erc20_token_addresses=[conf.dolomite_test_web3_address],
            chain=EthereumChain.MAIN_NET,
        )
        cls.market: DolomiteMarket = DolomiteMarket(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            isTestNet=True,
            symbols=["WETH-DAI"],
        )
        print("Initializing Dolomite market... ")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.wallet)
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../dolomite_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        self.wallet_logger = EventLogger()
        for event_tag in self.market_events:
            self.market.add_listener(event_tag, self.market_logger)
        for event_tag in self.wallet_events:
            self.wallet.add_listener(event_tag, self.wallet_logger)

    def tearDown(self):
        for event_tag in self.market_events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None
        for event_tag in self.wallet_events:
            self.wallet.remove_listener(event_tag, self.wallet_logger)
        self.wallet_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    # ====================================================

    def test_get_fee(self):
        limit_trade_fee: TradeFee = self.market.get_fee(
            "WETH", "DAI", OrderType.LIMIT, TradeType.BUY, 10000, 1)
        self.assertLess(limit_trade_fee.percent, 0.01)
        self.assertEqual(len(limit_trade_fee.flat_fees), 0)
        market_trade_fee: TradeFee = self.market.get_fee(
            "WETH", "DAI", OrderType.MARKET, TradeType.BUY, 0.1)
        self.assertGreater(market_trade_fee.percent, 0)
        self.assertEqual(len(market_trade_fee.flat_fees), 1)
        self.assertEqual(market_trade_fee.flat_fees[0][0], "DAI")

    def test_get_wallet_balances(self):
        balances = self.market.get_all_balances()
        self.assertGreaterEqual((balances["WETH"]), 0)
        self.assertGreaterEqual((balances["DAI"]), 0)

    def test_get_available_balances(self):
        balance = self.market.get_available_balance("WETH")
        self.assertGreaterEqual(balance, 0)

    def test_limit_orders(self):
        orders = self.market.limit_orders
        self.assertGreaterEqual(len(orders), 0)

    def test_cancel_order(self):
        symbol = "WETH-DAI"
        bid_price: float = self.market.get_price(symbol, True)
        amount = 0.5

        # Intentionally setting invalid price to prevent getting filled
        client_order_id = self.market.buy(symbol, amount, OrderType.LIMIT,
                                          bid_price * 0.7)
        self.run_parallel(asyncio.sleep(1.0))
        self.market.cancel(symbol, client_order_id)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event

        self.run_parallel(asyncio.sleep(6.0))
        self.assertEqual(0, len(self.market.limit_orders))
        self.assertEqual(client_order_id, order_cancelled_event.order_id)

    def test_place_limit_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("WETH"), 0.4)
        self.assertGreater(self.market.get_balance("DAI"), 60)

        # Try to buy 0.2 WETH from the exchange, and watch for creation event.
        symbol = "WETH-DAI"
        bid_price: float = self.market.get_price(symbol, True)
        amount: float = 0.4
        buy_order_id: str = self.market.buy(symbol, amount, OrderType.LIMIT,
                                            bid_price * 0.7)
        [buy_order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(buy_order_id, buy_order_created_event.order_id)
        self.market.cancel(symbol, buy_order_id)
        [_] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))

        # Try to sell 0.2 WETH to the exchange, and watch for creation event.
        ask_price: float = self.market.get_price(symbol, False)
        sell_order_id: str = self.market.sell(symbol, amount, OrderType.LIMIT,
                                              ask_price * 1.5)
        [sell_order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        self.assertEqual(sell_order_id, sell_order_created_event.order_id)
        self.market.cancel(symbol, sell_order_id)
        [_] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))

    @unittest.skipUnless(
        any("test_place_market_buy_and_sell" in arg for arg in sys.argv),
        "test_place_market_buy_and_sell test requires manual action.",
    )
    def test_place_market_buy_and_sell(self):
        # Cannot trade between yourself on Dolomite. Testing this is... hard.
        # These orders use the same code as limit orders except for fee calculation
        # and setting a field in the http request to "MARKET" instead of "LIMIT".
        # Fee calculations for market orders is tested above
        pass
Esempio n. 19
0
    def setUp(self):

        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size,
                                  self.start_timestamp, self.end_timestamp)
        self.market: BacktestMarket = BacktestMarket()
        self.maker_data: MockOrderBookLoader = MockOrderBookLoader(
            *self.maker_symbols)
        self.mid_price = 100
        self.time_delay = 15
        self.cancel_order_wait_time = 45
        self.maker_data.set_balanced_order_book(mid_price=self.mid_price,
                                                min_price=1,
                                                max_price=200,
                                                price_step_size=1,
                                                volume_step_size=10)
        self.market.add_data(self.maker_data)
        self.market.set_balance("COINALPHA", 500)
        self.market.set_balance("WETH", 500000000000)
        self.market.set_balance("QETH", 500)
        self.market.set_quantization_param(
            QuantizationParams(self.maker_symbols[0], 6, 6, 6, 6))

        self.market_info: MarketTradingPairTuple = MarketTradingPairTuple(
            *([self.market] + self.maker_symbols))

        logging_options: int = (
            Dev5TwapTradeStrategy.OPTION_LOG_ALL &
            (~Dev5TwapTradeStrategy.OPTION_LOG_NULL_ORDER_SIZE))

        # Define strategies to test
        self.limit_buy_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy(
            [self.market_info],
            order_type="limit",
            order_price=Decimal("99"),
            cancel_order_wait_time=self.cancel_order_wait_time,
            is_buy=True,
            time_delay=self.time_delay,
            is_vwap=True,
            percent_slippage=50.0,
            order_percent_of_volume=0.5,
            order_amount=Decimal("100.0"),
            logging_options=logging_options)
        self.limit_sell_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy(
            [self.market_info],
            order_type="limit",
            order_price=Decimal("101"),
            cancel_order_wait_time=self.cancel_order_wait_time,
            is_buy=False,
            time_delay=self.time_delay,
            is_vwap=True,
            percent_slippage=50.0,
            order_percent_of_volume=0.5,
            order_amount=Decimal("100.0"),
            logging_options=logging_options)
        self.market_buy_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy(
            [self.market_info],
            order_type="market",
            order_price=None,
            cancel_order_wait_time=self.cancel_order_wait_time,
            is_buy=True,
            time_delay=self.time_delay,
            is_vwap=True,
            percent_slippage=50.0,
            order_percent_of_volume=0.5,
            order_amount=Decimal("100.0"),
            logging_options=logging_options)
        self.market_sell_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy(
            [self.market_info],
            order_type="market",
            order_price=None,
            cancel_order_wait_time=self.cancel_order_wait_time,
            is_buy=False,
            time_delay=self.time_delay,
            is_vwap=True,
            percent_slippage=50.0,
            order_percent_of_volume=0.5,
            order_amount=Decimal("100.0"),
            logging_options=logging_options)
        self.logging_options = logging_options
        self.clock.add_iterator(self.market)
        self.maker_order_fill_logger: EventLogger = EventLogger()
        self.cancel_order_logger: EventLogger = EventLogger()
        self.buy_order_completed_logger: EventLogger = EventLogger()
        self.sell_order_completed_logger: EventLogger = EventLogger()

        self.market.add_listener(MarketEvent.BuyOrderCompleted,
                                 self.buy_order_completed_logger)
        self.market.add_listener(MarketEvent.SellOrderCompleted,
                                 self.sell_order_completed_logger)
        self.market.add_listener(MarketEvent.OrderFilled,
                                 self.maker_order_fill_logger)
        self.market.add_listener(MarketEvent.OrderCancelled,
                                 self.cancel_order_logger)
class CryptoComOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[CryptoComOrderBookTracker] = None
    events: List[OrderBookEvent] = [
        OrderBookEvent.TradeEvent
    ]
    trading_pairs: List[str] = [
        "BTC-USDT",
        "ETH-USDT",
    ]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: CryptoComOrderBookTracker = CryptoComOrderBookTracker(cls.trading_pairs)
        cls.order_book_tracker.start()
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        while True:
            if len(cls.order_book_tracker.order_books) > 0:
                print("Initialized real-time order books.")
                return
            await asyncio.sleep(1)

    async def run_parallel_async(self, *tasks, timeout=None):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        timer = 0
        while not future.done():
            if timeout and timer > timeout:
                raise Exception("Timeout running parallel async tasks in tests")
            timer += 1
            now = time.time()
            _next_iteration = now // 1.0 + 1  # noqa: F841
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def setUp(self):
        self.event_logger = EventLogger()
        for event_tag in self.events:
            for trading_pair, order_book in self.order_book_tracker.order_books.items():
                order_book.add_listener(event_tag, self.event_logger)

    def test_order_book_trade_event_emission(self):
        """
        Tests if the order book tracker is able to retrieve order book trade message from exchange and emit order book
        trade events after correctly parsing the trade messages
        """
        self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent))
        for ob_trade_event in self.event_logger.event_log:
            self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent)
            self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs)
            self.assertTrue(type(ob_trade_event.timestamp) in [float, int])
            self.assertTrue(type(ob_trade_event.amount) == float)
            self.assertTrue(type(ob_trade_event.price) == float)
            self.assertTrue(type(ob_trade_event.type) == TradeType)
            # datetime is in seconds
            self.assertTrue(math.ceil(math.log10(ob_trade_event.timestamp)) == 10)
            self.assertTrue(ob_trade_event.amount > 0)
            self.assertTrue(ob_trade_event.price > 0)

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(10.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        eth_usdt: OrderBook = order_books["ETH-USDT"]
        self.assertIsNot(eth_usdt.last_diff_uid, 0)
        self.assertGreaterEqual(eth_usdt.get_price_for_volume(True, 10).result_price,
                                eth_usdt.get_price(True))
        self.assertLessEqual(eth_usdt.get_price_for_volume(False, 10).result_price,
                             eth_usdt.get_price(False))

    def test_api_get_last_traded_prices(self):
        prices = self.ev_loop.run_until_complete(
            CryptoComAPIOrderBookDataSource.get_last_traded_prices(["BTC-USDT", "LTC-BTC"]))
        for key, value in prices.items():
            print(f"{key} last_trade_price: {value}")
        self.assertGreater(prices["BTC-USDT"], 1000)
        self.assertLess(prices["LTC-BTC"], 1)
Esempio n. 21
0
class BittrexMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled,
        MarketEvent.OrderCancelled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]

    market: BittrexMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        if API_MOCK_ENABLED:
            cls.web_app = HummingWebApp.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.market.bittrex.bittrex_market.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()

            cls._us_patcher = unittest.mock.patch(
                "hummingbot.market.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.market.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

            HummingWsServerFactory.url_host_only = True
            ws_server = HummingWsServerFactory.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 = HummingWsServerFactory.reroute_ws_connect
            ws_server.add_stock_response(
                "queryExchangeState",
                FixtureBittrex.WS_ORDER_BOOK_SNAPSHOT.copy())

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BittrexMarket = BittrexMarket(
            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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()
        if API_MOCK_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._t_nonce_patcher.stop()
            cls._ob_patcher.stop()
            cls._us_patcher.stop()
            cls._ws_patcher.stop()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../bittrex_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: TradeFee = self.market.get_fee("ETH", "USDT",
                                                  OrderType.LIMIT_MAKER,
                                                  TradeType.BUY, 1, 1)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("ETH", "USDT",
                                                   OrderType.LIMIT,
                                                   TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_fee_overrides_config(self):
        fee_overrides_config_map["bittrex_taker_fee"].value = None
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.0025"), taker_fee.percent)
        fee_overrides_config_map["bittrex_taker_fee"].value = Decimal('0.2')
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["bittrex_maker_fee"].value = None
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT_MAKER,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.0025"), maker_fee.percent)
        fee_overrides_config_map["bittrex_maker_fee"].value = Decimal('0.5')
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT_MAKER,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent)

    def place_order(self, is_buy, trading_pair, amount, order_type, price,
                    nonce, post_resp, ws_resp):
        global EXCHANGE_ORDER_ID
        order_id, exch_order_id = None, None
        if API_MOCK_ENABLED:
            exch_order_id = f"BITTREX_{EXCHANGE_ORDER_ID}"
            EXCHANGE_ORDER_ID += 1
            self._t_nonce_mock.return_value = nonce
            resp = post_resp.copy()
            resp["id"] = exch_order_id
            side = 'buy' if is_buy else 'sell'
            resp["direction"] = side.upper()
            resp["type"] = order_type.name.upper()
            if order_type == OrderType.LIMIT:
                del resp["limit"]
            self.web_app.update_response("post", API_BASE_URL, "/v3/orders",
                                         resp)
        if is_buy:
            order_id = self.market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = self.market.sell(trading_pair, amount, order_type,
                                        price)
        if API_MOCK_ENABLED:
            resp = ws_resp.copy()
            resp["content"]["o"]["OU"] = exch_order_id
            HummingWsServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                        resp,
                                                        delay=1.0)
        return order_id, exch_order_id

    def cancel_order(self, trading_pair, order_id, exch_order_id):
        if API_MOCK_ENABLED:
            resp = FixtureBittrex.ORDER_CANCEL.copy()
            resp["id"] = exch_order_id
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id}", resp)
        self.market.cancel(trading_pair, order_id)

    def test_limit_maker_rejections(self):
        if API_MOCK_ENABLED:
            return
        trading_pair = "ETH-USDT"

        # Try to put a buy limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('1.02')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair, 1)

        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER,
                                   price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

        self.market_logger.clear()

        # Try to put a sell limit maker order that is going to match, this should triggers order failure event.
        price: Decimal = self.market.get_price(trading_pair,
                                               True) * Decimal('0.98')
        price: Decimal = self.market.quantize_order_price(trading_pair, price)
        amount = self.market.quantize_order_amount(trading_pair, 1)

        order_id = self.market.sell(trading_pair, amount,
                                    OrderType.LIMIT_MAKER, price)
        [order_failure_event] = self.run_parallel(
            self.market_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, order_failure_event.order_id)

    def test_limit_makers_unfilled(self):
        self.assertGreater(self.market.get_balance("USDT"), 20)
        trading_pair = "ETH-USDT"

        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)

        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_ask_price)
        ask_amount: Decimal = Decimal('0.06')
        quantized_ask_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, ask_amount)

        order_id, exch_order_id_1 = self.place_order(
            True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN,
            FixtureBittrex.WS_ORDER_OPEN)
        [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)

        order_id2, exch_order_id_2 = self.place_order(
            False, trading_pair, quantized_ask_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002, FixtureBittrex.ORDER_PLACE_OPEN,
            FixtureBittrex.WS_ORDER_OPEN)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id2, order_created_event.order_id)

        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            resp = FixtureBittrex.ORDER_CANCEL.copy()
            resp["id"] = exch_order_id_1
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id_1}", resp)
            resp = FixtureBittrex.ORDER_CANCEL.copy()
            resp["id"] = exch_order_id_2
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id_2}", resp)
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_limit_taker_buy(self):
        self.assertGreater(self.market.get_balance("USDT"), 20)
        trading_pair = "ETH-USDT"

        price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(True, trading_pair, quantized_amount,
                                       OrderType.LIMIT, price, 10001,
                                       FixtureBittrex.ORDER_PLACE_FILLED,
                                       FixtureBittrex.WS_ORDER_FILLED)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDT", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_taker_sell(self):
        trading_pair = "ETH-USDT"
        self.assertGreater(self.market.get_balance("ETH"), 0.06)

        price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, _ = self.place_order(False, trading_pair, quantized_amount,
                                       OrderType.LIMIT, price, 10001,
                                       FixtureBittrex.ORDER_PLACE_FILLED,
                                       FixtureBittrex.WS_ORDER_FILLED)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDT", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):
        trading_pair = "ETH-USDT"

        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)

        amount: Decimal = Decimal("0.06")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id, exch_order_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN,
            FixtureBittrex.WS_ORDER_OPEN)
        self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.cancel_order(trading_pair, order_id, exch_order_id)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, order_id)

    def test_cancel_all(self):
        self.assertGreater(self.market.get_balance("USDT"), 20)
        trading_pair = "ETH-USDT"

        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)

        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_ask_price)
        ask_amount: Decimal = Decimal('0.06')
        quantized_ask_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, ask_amount)

        _, exch_order_id_1 = self.place_order(True, trading_pair,
                                              quantized_bid_amount,
                                              OrderType.LIMIT_MAKER,
                                              quantize_bid_price, 10001,
                                              FixtureBittrex.ORDER_PLACE_OPEN,
                                              FixtureBittrex.WS_ORDER_OPEN)
        _, exch_order_id_2 = self.place_order(False, trading_pair,
                                              quantized_ask_amount,
                                              OrderType.LIMIT_MAKER,
                                              quantize_ask_price, 10002,
                                              FixtureBittrex.ORDER_PLACE_OPEN,
                                              FixtureBittrex.WS_ORDER_OPEN)
        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            resp = FixtureBittrex.ORDER_CANCEL.copy()
            resp["id"] = exch_order_id_1
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id_1}", resp)
            resp = FixtureBittrex.ORDER_CANCEL.copy()
            resp["id"] = exch_order_id_2
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id_2}", resp)
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    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.ORDER_PLACE_OPEN,
                FixtureBittrex.WS_ORDER_OPEN)
            [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: BittrexMarket = BittrexMarket(
                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)

    def test_order_fill_record(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:

            price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal("0.06")
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, amount)
            order_id, _ = self.place_order(True, trading_pair,
                                           quantized_amount, OrderType.LIMIT,
                                           price, 10001,
                                           FixtureBittrex.ORDER_PLACE_FILLED,
                                           FixtureBittrex.WS_ORDER_FILLED)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

            amount = Decimal(buy_order_completed_event.base_asset_amount)
            order_id, _ = self.place_order(False, trading_pair, amount,
                                           OrderType.LIMIT, 0, 10001,
                                           FixtureBittrex.ORDER_PLACE_FILLED,
                                           FixtureBittrex.WS_ORDER_FILLED)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        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)
class CoinbaseProMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated
    ]

    market: CoinbaseProMarket
    market_logger: EventLogger

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: CoinbaseProMarket = CoinbaseProMarket(
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            coinbase_pro_api_key=conf.coinbase_pro_api_key,
            coinbase_pro_secret_key=conf.coinbase_pro_secret_key,
            coinbase_pro_passphrase=conf.coinbase_pro_passphrase,
            symbols=["ETH-USDC", "ETH-USD"])
        cls.wallet: Web3Wallet = Web3Wallet(
            private_key=conf.web3_private_key_coinbase_pro,
            backend_urls=conf.test_web3_provider_list,
            erc20_token_addresses=[
                conf.mn_weth_token_address, conf.mn_zerox_token_address
            ],
            chain=EthereumChain.MAIN_NET)
        print(
            "Initializing Coinbase Pro market... this will take about a minute."
        )
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.market)
        cls.clock.add_iterator(cls.wallet)
        stack = contextlib.ExitStack()
        cls._clock = stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: TradeFee = self.market.get_fee("ETH", "USDC",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, 1, 1)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("ETH", "USDC",
                                                   OrderType.MARKET,
                                                   TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_limit_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        symbol = "ETH-USDC"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        current_bid_price: float = self.market.get_price(symbol, True)
        bid_price: float = current_bid_price + 0.05 * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            symbol, bid_price)

        order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT,
                                   quantize_bid_price)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: float = sum(t.amount for t in trade_events)
        quote_amount_traded: float = sum(t.amount * t.price
                                         for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount),
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded,
                               float(order_completed_event.quote_asset_amount))
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_sell(self):
        symbol = "ETH-USDC"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        current_ask_price: float = self.market.get_price(symbol, False)
        ask_price: float = current_ask_price - 0.05 * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            symbol, ask_price)

        order_id = self.market.sell(symbol, amount, OrderType.LIMIT,
                                    quantize_ask_price)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount),
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDC", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded,
                               float(order_completed_event.quote_asset_amount))
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_market_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        symbol = "ETH-USD"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET,
                                   0)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: float = sum(t.amount for t in trade_events)
        quote_amount_traded: float = sum(t.amount * t.price
                                         for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount),
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USD", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded,
                               float(order_completed_event.quote_asset_amount))
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_market_sell(self):
        symbol = "ETH-USD"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(float(quantized_amount),
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USD", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               float(order_completed_event.base_asset_amount))
        self.assertAlmostEqual(quote_amount_traded,
                               float(order_completed_event.quote_asset_amount))
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):
        self.assertGreater(self.market.get_balance("ETH"), 10)
        symbol = "ETH-USDC"

        current_bid_price: float = self.market.get_price(symbol, True)
        amount: float = 10 / current_bid_price

        bid_price: float = current_bid_price - 0.1 * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            symbol, bid_price)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        client_order_id = self.market.buy(symbol, quantized_amount,
                                          OrderType.LIMIT, quantize_bid_price)
        self.market.cancel(symbol, client_order_id)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, client_order_id)

    def test_cancel_all(self):
        symbol = "ETH-USDC"
        bid_price: float = self.market.get_price(symbol, True) * 0.5
        ask_price: float = self.market.get_price(symbol, False) * 2
        amount: float = 10 / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            symbol, bid_price * 0.7)
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            symbol, ask_price * 1.5)

        self.market.buy(symbol, quantized_amount, OrderType.LIMIT,
                        quantize_bid_price)
        self.market.sell(symbol, quantized_amount, OrderType.LIMIT,
                         quantize_ask_price)
        self.run_parallel(asyncio.sleep(1))
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv),
                         "List order test requires manual action.")
    def test_list_orders(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        symbol = "ETH-USDC"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        current_bid_price: float = self.market.get_price(symbol, True)
        bid_price: float = current_bid_price + 0.05 * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            symbol, bid_price)

        self.market.buy(symbol, quantized_amount, OrderType.LIMIT,
                        quantize_bid_price)
        self.run_parallel(asyncio.sleep(1))
        [order_details] = self.run_parallel(self.market.list_orders())
        self.assertGreaterEqual(len(order_details), 1)

        self.market_logger.clear()

    @unittest.skipUnless(any("test_deposit_eth" in arg for arg in sys.argv),
                         "Deposit test requires manual action.")
    def test_deposit_eth(self):
        # Ensure the local wallet has enough balance for deposit testing.
        self.assertGreaterEqual(self.wallet.get_balance("ETH"), 0.02)

        # Deposit ETH to Binance, and wait.
        tracking_id: str = self.market.deposit(self.wallet, "ETH", 0.01)
        [received_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketReceivedAssetEvent,
                                        timeout_seconds=1800))
        received_asset_event: MarketReceivedAssetEvent = received_asset_event
        self.assertEqual("ETH", received_asset_event.asset_name)
        self.assertEqual(tracking_id, received_asset_event.tx_hash)
        self.assertEqual(self.wallet.address,
                         received_asset_event.from_address)
        self.assertAlmostEqual(0.01, received_asset_event.amount_received)

    @unittest.skipUnless(any("test_deposit_zrx" in arg for arg in sys.argv),
                         "Deposit test requires manual action.")
    def test_deposit_zrx(self):
        # Ensure the local wallet has enough balance for deposit testing.
        self.assertGreaterEqual(self.wallet.get_balance("ZRX"), 1)

        # Deposit ZRX to Coinbase Pro, and wait.
        tracking_id: str = self.market.deposit(self.wallet, "ZRX", 1)
        [received_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketReceivedAssetEvent,
                                        timeout_seconds=1800))
        received_asset_event: MarketReceivedAssetEvent = received_asset_event
        self.assertEqual("ZRX", received_asset_event.asset_name)
        self.assertEqual(tracking_id, received_asset_event.tx_hash)
        self.assertEqual(self.wallet.address,
                         received_asset_event.from_address)
        self.assertEqual(1, received_asset_event.amount_received)

    @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv),
                         "Withdraw test requires manual action.")
    def test_withdraw(self):
        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("ZRX"), 1)

        # Withdraw ZRX from Coinbase Pro to test wallet.
        self.market.withdraw(self.wallet.address, "ZRX", 1)
        [withdraw_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketWithdrawAssetEvent))
        withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event
        self.assertEqual(self.wallet.address, withdraw_asset_event.to_address)
        self.assertEqual("ZRX", withdraw_asset_event.asset_name)
        self.assertEqual(1, withdraw_asset_event.amount)
        self.assertEqual(withdraw_asset_event.fee_amount, 0)
Esempio n. 23
0
    def setUp(self):
        self.clock_tick_size = 1
        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size,
                                  self.start_timestamp, self.end_timestamp)
        self.market: MockPaperExchange = MockPaperExchange()
        self.mid_price = 100
        self.bid_spread = 0.01
        self.ask_spread = 0.01
        self.order_refresh_time = 30
        self.market.set_balanced_order_book(trading_pair=self.trading_pair,
                                            mid_price=self.mid_price,
                                            min_price=1,
                                            max_price=200,
                                            price_step_size=1,
                                            volume_step_size=10)
        self.market.set_balance("HBOT", 500)
        self.market.set_balance("ETH", 5000)
        self.market.set_quantization_param(
            QuantizationParams(self.trading_pair, 6, 6, 6, 6))
        self.market_info = MarketTradingPairTuple(self.market,
                                                  self.trading_pair,
                                                  self.base_asset,
                                                  self.quote_asset)
        self.clock.add_iterator(self.market)
        self.maker_order_fill_logger: EventLogger = EventLogger()
        self.cancel_order_logger: EventLogger = EventLogger()
        self.market.add_listener(MarketEvent.OrderFilled,
                                 self.maker_order_fill_logger)
        self.market.add_listener(MarketEvent.OrderCancelled,
                                 self.cancel_order_logger)

        self.one_level_strategy: PureMarketMakingStrategy = PureMarketMakingStrategy(
        )
        self.one_level_strategy.init_params(self.market_info,
                                            bid_spread=Decimal("0.01"),
                                            ask_spread=Decimal("0.01"),
                                            order_amount=Decimal("1"),
                                            order_refresh_time=4,
                                            filled_order_delay=8,
                                            hanging_orders_enabled=True,
                                            hanging_orders_cancel_pct=0.05,
                                            order_refresh_tolerance_pct=0)
        self.multi_levels_strategy: PureMarketMakingStrategy = PureMarketMakingStrategy(
        )
        self.multi_levels_strategy.init_params(
            self.market_info,
            bid_spread=Decimal("0.01"),
            ask_spread=Decimal("0.01"),
            order_amount=Decimal("1"),
            order_levels=5,
            order_level_spread=Decimal("0.01"),
            order_refresh_time=4,
            filled_order_delay=8,
            order_refresh_tolerance_pct=0)
        self.hanging_order_multiple_strategy = PureMarketMakingStrategy()
        self.hanging_order_multiple_strategy.init_params(
            self.market_info,
            bid_spread=Decimal("0.01"),
            ask_spread=Decimal("0.01"),
            order_amount=Decimal("1"),
            order_levels=5,
            order_level_spread=Decimal("0.01"),
            order_refresh_time=4,
            filled_order_delay=8,
            order_refresh_tolerance_pct=0,
            hanging_orders_enabled=True)
 def setUp(self):
     self.market_logger = EventLogger()
     for event_tag in self.events:
         self.market.add_listener(event_tag, self.market_logger)
class WazirxExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]
    connector: WazirxExchange
    event_logger: EventLogger
    trading_pair = "BTC-INR"
    base_token, quote_token = trading_pair.split("-")
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.ev_loop = asyncio.get_event_loop()

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: WazirxExchange = WazirxExchange(
            wazirx_api_key=API_KEY,
            wazirx_secret_key=API_SECRET,
            trading_pairs=[cls.trading_pair],
            trading_required=True)
        print("Initializing Waxirx market... this will take about a minute.")
        cls.clock.add_iterator(cls.connector)
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls, connector=None):
        if connector is None:
            connector = cls.connector
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if connector.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__,
                                          "../connector_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.event_logger = EventLogger()
        for event_tag in self.events:
            self.connector.add_listener(event_tag, self.event_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.connector.remove_listener(event_tag, self.event_logger)
        self.event_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_estimate_fee(self):
        maker_fee = self.connector.estimate_fee_pct(True)
        self.assertAlmostEqual(maker_fee, Decimal("0.001"))
        taker_fee = self.connector.estimate_fee_pct(False)
        self.assertAlmostEqual(taker_fee, Decimal("0.001"))

    def _place_order(self,
                     is_buy,
                     amount,
                     order_type,
                     price,
                     ex_order_id,
                     get_order_fixture=None,
                     ws_trade_fixture=None,
                     ws_order_fixture=None) -> str:
        if is_buy:
            cl_order_id = self.connector.buy(self.trading_pair, amount,
                                             order_type, price)
        else:
            cl_order_id = self.connector.sell(self.trading_pair, amount,
                                              order_type, price)
        return cl_order_id

    def _cancel_order(self, cl_order_id):
        self.connector.cancel(self.trading_pair, cl_order_id)

    def test_buy_and_sell(self):
        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("1.05")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))
        quote_bal = self.connector.get_available_balance(self.quote_token)
        base_bal = self.connector.get_available_balance(self.base_token)

        order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1,
                                     None, fixture.WS_TRADE)
        order_completed_event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(BuyOrderCompletedEvent))
        self.ev_loop.run_until_complete(asyncio.sleep(2))
        trade_events = [
            t for t in self.event_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(amount, order_completed_event.base_asset_amount)
        self.assertEqual("BTC", order_completed_event.base_asset)
        self.assertEqual("INR", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.event_logger.event_log
            ]))

        # check available quote balance gets updated, we need to wait a bit for the balance message to arrive
        expected_quote_bal = quote_bal - quote_amount_traded
        self._mock_ws_bal_update(self.quote_token, expected_quote_bal)
        self.ev_loop.run_until_complete(asyncio.sleep(1))
        self.assertAlmostEqual(
            expected_quote_bal,
            self.connector.get_available_balance(self.quote_token))

        # Reset the logs
        self.event_logger.clear()

        # Try to sell back the same amount to the exchange, and watch for completion event.
        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("0.95")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))
        order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2,
                                     None, fixture.WS_TRADE)
        order_completed_event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(SellOrderCompletedEvent))
        trade_events = [
            t for t in self.event_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(amount, order_completed_event.base_asset_amount)
        self.assertEqual("BTC", order_completed_event.base_asset)
        self.assertEqual("INR", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertGreater(order_completed_event.fee_amount, Decimal(0))
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.event_logger.event_log
            ]))

        # check available base balance gets updated, we need to wait a bit for the balance message to arrive
        expected_base_bal = base_bal
        self._mock_ws_bal_update(self.base_token, expected_base_bal)
        self.ev_loop.run_until_complete(asyncio.sleep(1))
        self.assertAlmostEqual(
            expected_base_bal,
            self.connector.get_available_balance(self.base_token), 5)

    def test_limit_makers_unfilled(self):
        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("0.8")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))
        quote_bal = self.connector.get_available_balance(self.quote_token)

        # order_id = self.connector.buy(self.trading_pair, amount, OrderType.LIMIT_MAKER, price)
        cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER,
                                        price, 1, fixture.UNFILLED_ORDER)
        order_created_event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(cl_order_id, order_created_event.order_id)
        # check available quote balance gets updated, we need to wait a bit for the balance message to arrive
        expected_quote_bal = quote_bal - (price * amount)
        self._mock_ws_bal_update(self.quote_token, expected_quote_bal)
        self.ev_loop.run_until_complete(asyncio.sleep(2))
        self.assertAlmostEqual(
            expected_quote_bal,
            self.connector.get_available_balance(self.quote_token))
        self._cancel_order(cl_order_id)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(cl_order_id, event.order_id)

        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("1.2")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))

        cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER,
                                        price, 2, fixture.UNFILLED_ORDER)
        order_created_event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(SellOrderCreatedEvent))
        self.assertEqual(cl_order_id, order_created_event.order_id)
        self._cancel_order(cl_order_id)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(cl_order_id, event.order_id)

    def _mock_ws_bal_update(self, token, available):
        # TODO: Determine best way to test balance via ws
        pass

    def test_limit_maker_rejections(self):
        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("1.2")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))
        cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER,
                                        price, 1, None, None,
                                        fixture.WS_ORDER_CANCELED)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(cl_order_id, event.order_id)

        price = self.connector.get_price(self.trading_pair,
                                         False) * Decimal("0.8")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))
        cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER,
                                        price, 2, None, None,
                                        fixture.WS_ORDER_CANCELED)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(cl_order_id, event.order_id)

    def test_cancel_all(self):
        bid_price = self.connector.get_price(self.trading_pair, True)
        ask_price = self.connector.get_price(self.trading_pair, False)
        bid_price = self.connector.quantize_order_price(
            self.trading_pair, bid_price * Decimal("0.7"))
        ask_price = self.connector.quantize_order_price(
            self.trading_pair, ask_price * Decimal("1.5"))
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("15"))

        buy_id = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1)
        sell_id = self._place_order(False, amount, OrderType.LIMIT, ask_price,
                                    2)

        self.ev_loop.run_until_complete(asyncio.sleep(1))
        asyncio.ensure_future(self.connector.cancel_all(3))
        self.ev_loop.run_until_complete(asyncio.sleep(3))
        cancel_events = [
            t for t in self.event_logger.event_log
            if isinstance(t, OrderCancelledEvent)
        ]
        self.assertEqual({buy_id, sell_id},
                         {o.order_id
                          for o in cancel_events})

    def test_order_price_precision(self):
        bid_price: Decimal = self.connector.get_price(self.trading_pair, True)
        ask_price: Decimal = self.connector.get_price(self.trading_pair, False)
        mid_price: Decimal = (bid_price + ask_price) / 2
        amount: Decimal = Decimal("0.000123456")

        # Make sure there's enough balance to make the limit orders.
        self.assertGreater(self.connector.get_balance("BTC"), Decimal("0.001"))
        self.assertGreater(self.connector.get_balance("INR"), Decimal("50"))

        # Intentionally set some prices with too many decimal places s.t. they
        # need to be quantized. Also, place them far away from the mid-price s.t. they won't
        # get filled during the test.
        bid_price = mid_price * Decimal("0.9333192292111341")
        ask_price = mid_price * Decimal("1.0492431474884933")

        cl_order_id_1 = self._place_order(True, amount, OrderType.LIMIT,
                                          bid_price, 1, fixture.UNFILLED_ORDER)

        # Wait for the order created event and examine the order made
        self.ev_loop.run_until_complete(
            self.event_logger.wait_for(BuyOrderCreatedEvent))
        order = self.connector.in_flight_orders[cl_order_id_1]
        quantized_bid_price = self.connector.quantize_order_price(
            self.trading_pair, bid_price)
        quantized_bid_size = self.connector.quantize_order_amount(
            self.trading_pair, amount)
        self.assertEqual(quantized_bid_price, order.price)
        self.assertEqual(quantized_bid_size, order.amount)

        # Test ask order
        cl_order_id_2 = self._place_order(False, amount, OrderType.LIMIT,
                                          ask_price, 1, fixture.UNFILLED_ORDER)

        # Wait for the order created event and examine and order made
        self.ev_loop.run_until_complete(
            self.event_logger.wait_for(SellOrderCreatedEvent))
        order = self.connector.in_flight_orders[cl_order_id_2]
        quantized_ask_price = self.connector.quantize_order_price(
            self.trading_pair, Decimal(ask_price))
        quantized_ask_size = self.connector.quantize_order_amount(
            self.trading_pair, Decimal(amount))
        self.assertEqual(quantized_ask_price, order.price)
        self.assertEqual(quantized_ask_size, order.amount)

        self._cancel_order(cl_order_id_1)
        self._cancel_order(cl_order_id_2)

    def test_orders_saving_and_restoration(self):
        config_path = "test_config"
        strategy_name = "test_strategy"
        sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS,
                                   db_path=self.db_path)
        order_id = None
        recorder = MarketsRecorder(sql, [self.connector], config_path,
                                   strategy_name)
        recorder.start()

        try:
            self.connector._in_flight_orders.clear()
            self.assertEqual(0, len(self.connector.tracking_states))

            # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event.
            current_bid_price: Decimal = self.connector.get_price(
                self.trading_pair, True)
            price: Decimal = current_bid_price * Decimal("0.8")
            price = self.connector.quantize_order_price(
                self.trading_pair, price)

            amount: Decimal = Decimal("0.0001")
            amount = self.connector.quantize_order_amount(
                self.trading_pair, amount)

            cl_order_id = self._place_order(True, amount,
                                            OrderType.LIMIT_MAKER, price, 1,
                                            fixture.UNFILLED_ORDER)
            order_created_event = self.ev_loop.run_until_complete(
                self.event_logger.wait_for(BuyOrderCreatedEvent))
            self.assertEqual(cl_order_id, order_created_event.order_id)

            # Verify tracking states
            self.assertEqual(1, len(self.connector.tracking_states))
            self.assertEqual(cl_order_id,
                             list(self.connector.tracking_states.keys())[0])

            # Verify orders from recorder
            recorded_orders: List[
                Order] = recorder.get_orders_for_config_and_market(
                    config_path, self.connector)
            self.assertEqual(1, len(recorded_orders))
            self.assertEqual(cl_order_id, recorded_orders[0].id)

            # Verify saved market states
            saved_market_states: MarketState = recorder.get_market_states(
                config_path, self.connector)
            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.connector.stop(self._clock)
            self.ev_loop.run_until_complete(asyncio.sleep(5))
            self.clock.remove_iterator(self.connector)
            for event_tag in self.events:
                self.connector.remove_listener(event_tag, self.event_logger)
            new_connector = WazirxExchange(API_KEY, API_SECRET,
                                           [self.trading_pair], True)
            for event_tag in self.events:
                new_connector.add_listener(event_tag, self.event_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [new_connector], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, new_connector)
            self.clock.add_iterator(new_connector)
            self.assertEqual(0, len(new_connector.limit_orders))
            self.assertEqual(0, len(new_connector.tracking_states))
            new_connector.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(new_connector.limit_orders))
            self.assertEqual(1, len(new_connector.tracking_states))

            # Cancel the order and verify that the change is saved.
            self._cancel_order(cl_order_id)
            self.ev_loop.run_until_complete(
                self.event_logger.wait_for(OrderCancelledEvent))
            order_id = None
            self.assertEqual(0, len(new_connector.limit_orders))
            self.assertEqual(0, len(new_connector.tracking_states))
            saved_market_states = recorder.get_market_states(
                config_path, new_connector)
            self.assertEqual(0, len(saved_market_states.saved_state))
        finally:
            if order_id is not None:
                self.connector.cancel(self.trading_pair, cl_order_id)
                self.run_parallel(
                    self.event_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)

    def test_update_last_prices(self):
        # This is basic test to see if order_book last_trade_price is initiated and updated.
        for order_book in self.connector.order_books.values():
            for _ in range(5):
                self.ev_loop.run_until_complete(asyncio.sleep(1))
                print(order_book.last_trade_price)
                self.assertFalse(math.isnan(order_book.last_trade_price))

    def test_filled_orders_recorded(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS,
                                   db_path=self.db_path)
        order_id = None
        recorder = MarketsRecorder(sql, [self.connector], config_path,
                                   strategy_name)
        recorder.start()

        try:
            # Try to buy some token from the exchange, and watch for completion event.
            price = self.connector.get_price(self.trading_pair,
                                             True) * Decimal("1.05")
            price = self.connector.quantize_order_price(
                self.trading_pair, price)
            amount = self.connector.quantize_order_amount(
                self.trading_pair, Decimal("15"))

            order_id = self._place_order(True, amount, OrderType.LIMIT, price,
                                         1, None, fixture.WS_TRADE)
            self.ev_loop.run_until_complete(
                self.event_logger.wait_for(BuyOrderCompletedEvent))
            self.ev_loop.run_until_complete(asyncio.sleep(1))

            # Reset the logs
            self.event_logger.clear()

            # Try to sell back the same amount to the exchange, and watch for completion event.
            price = self.connector.get_price(self.trading_pair,
                                             True) * Decimal("0.95")
            price = self.connector.quantize_order_price(
                self.trading_pair, price)
            amount = self.connector.quantize_order_amount(
                self.trading_pair, Decimal("15"))
            order_id = self._place_order(False, amount, OrderType.LIMIT, price,
                                         2, None, fixture.WS_TRADE)
            self.ev_loop.run_until_complete(
                self.event_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            self.assertGreaterEqual(len(trade_fills), 2)
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertGreaterEqual(len(buy_fills), 1)
            self.assertGreaterEqual(len(sell_fills), 1)

            order_id = None

        finally:
            if order_id is not None:
                self.connector.cancel(self.trading_pair, order_id)
                self.run_parallel(
                    self.event_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
class PerpetualFinanceDerivativeUnitTest(unittest.TestCase):
    event_logger: EventLogger
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure,
        MarketEvent.FundingPaymentCompleted
    ]
    connector: PerpetualFinanceDerivative
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.ev_loop = asyncio.get_event_loop()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: PerpetualFinanceDerivative = PerpetualFinanceDerivative(
            [trading_pair], "PRIVATE_KEY_HERE", "")
        print(
            "Initializing PerpetualFinanceDerivative market... this will take about a minute."
        )
        cls.connector.set_leverage(trading_pair, leverage)
        cls.connector.set_position_mode(PositionMode.ONEWAY)
        cls.clock.add_iterator(cls.connector)
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.connector.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__,
                                          "../connector_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass
        self.event_logger = EventLogger()
        for event_tag in self.events:
            self.connector.add_listener(event_tag, self.event_logger)

    def test_update_balances(self):
        all_bals = self.connector.get_all_balances()
        for token, bal in all_bals.items():
            print(f"{token}: {bal}")
        self.assertIn(quote, all_bals)
        self.assertTrue(all_bals["XDAI"] > 0)

    def test_allowances(self):
        asyncio.get_event_loop().run_until_complete(self._test_allowances())

    async def _test_allowances(self):
        perfi = self.connector
        allowances = await perfi.get_allowances()
        print(allowances)

    def test_approve(self):
        asyncio.get_event_loop().run_until_complete(self._test_approve())

    async def _test_approve(self):
        perfi = self.connector
        ret_val = await perfi.approve_perpetual_finance_spender()
        print(ret_val)

    def test_get_quote_price(self):
        asyncio.get_event_loop().run_until_complete(
            self._test_get_quote_price())

    async def _test_get_quote_price(self):
        perfi = self.connector
        buy_price = await perfi.get_quote_price(trading_pair, True,
                                                Decimal("1"))
        self.assertTrue(buy_price > 0)
        print(f"buy_price: {buy_price}")
        sell_price = await perfi.get_quote_price(trading_pair, False,
                                                 Decimal("1"))
        self.assertTrue(sell_price > 0)
        print(f"sell_price: {sell_price}")
        self.assertTrue(buy_price != sell_price)

    def test_open_and_close_long_position(self):
        perfi = self.connector
        amount = Decimal("0.1")
        price = Decimal("10")
        order_id = perfi.buy(trading_pair,
                             amount,
                             OrderType.LIMIT,
                             price,
                             position_action=PositionAction.OPEN)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(BuyOrderCompletedEvent))
        self.assertTrue(event.order_id is not None)
        self.assertEqual(order_id, event.order_id)
        self.assertEqual(event.base_asset_amount, amount)
        print(event.order_id)

        order_id = perfi.sell(trading_pair,
                              amount,
                              OrderType.LIMIT,
                              price,
                              position_action=PositionAction.CLOSE)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(SellOrderCompletedEvent))
        self.assertTrue(event.order_id is not None)
        self.assertEqual(order_id, event.order_id)
        self.assertEqual(event.base_asset_amount, amount)
        print(event.order_id)

    def test_open_position_failure(self):
        perfi = self.connector
        # Since we don't have 1000000 xUSDC, this should trigger order failure
        amount = Decimal("1000000")
        price = Decimal("10")
        order_id = perfi.sell(trading_pair,
                              amount,
                              OrderType.LIMIT,
                              price,
                              position_action=PositionAction.OPEN)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, event.order_id)

    def test_filled_orders_recorded(self):
        config_path = "test_config"
        strategy_name = "test_strategy"
        sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS,
                                   db_path=self.db_path)
        recorder = MarketsRecorder(sql, [self.connector], config_path,
                                   strategy_name)
        recorder.start()
        try:
            self.connector._in_flight_orders.clear()
            self.assertEqual(0, len(self.connector.tracking_states))

            price: Decimal = Decimal("10")  # quote_price * Decimal("0.8")
            price = self.connector.quantize_order_price(trading_pair, price)

            amount: Decimal = Decimal("0.1")
            amount = self.connector.quantize_order_amount(trading_pair, amount)

            sell_order_id = self.connector.sell(
                trading_pair,
                amount,
                OrderType.LIMIT,
                price,
                position_action=PositionAction.OPEN)
            self.ev_loop.run_until_complete(
                self.event_logger.wait_for(SellOrderCompletedEvent))
            self.ev_loop.run_until_complete(asyncio.sleep(1))

            price: Decimal = Decimal("10")  # quote_price * Decimal("0.8")
            price = self.connector.quantize_order_price(trading_pair, price)

            buy_order_id = self.connector.buy(
                trading_pair,
                amount,
                OrderType.LIMIT,
                price,
                position_action=PositionAction.CLOSE)
            self.ev_loop.run_until_complete(
                self.event_logger.wait_for(BuyOrderCompletedEvent))
            self.ev_loop.run_until_complete(asyncio.sleep(1))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)
            # self.assertGreaterEqual(len(trade_fills), 2)
            fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertGreaterEqual(len(fills), 1)
            self.assertEqual(amount, Decimal(str(fills[0].amount)))
            # self.assertEqual(price, Decimal(str(fills[0].price)))
            self.assertEqual(base, fills[0].base_asset)
            self.assertEqual(quote, fills[0].quote_asset)
            self.assertEqual(sell_order_id, fills[0].order_id)
            self.assertEqual(trading_pair, fills[0].symbol)
            fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            self.assertGreaterEqual(len(fills), 1)
            self.assertEqual(amount, Decimal(str(fills[0].amount)))
            # self.assertEqual(price, Decimal(str(fills[0].price)))
            self.assertEqual(base, fills[0].base_asset)
            self.assertEqual(quote, fills[0].quote_asset)
            self.assertEqual(buy_order_id, fills[0].order_id)
            self.assertEqual(trading_pair, fills[0].symbol)

        finally:
            recorder.stop()
            os.unlink(self.db_path)
class RadarRelayMarketUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled,
        MarketEvent.OrderExpired, MarketEvent.OrderFilled,
        MarketEvent.WithdrawAsset
    ]

    wallet_events: List[WalletEvent] = [
        WalletEvent.WrappedEth, WalletEvent.UnwrappedEth
    ]

    wallet: Web3Wallet
    market: RadarRelayMarket
    market_logger: EventLogger
    wallet_logger: EventLogger

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.wallet = Web3Wallet(private_key=conf.web3_private_key_radar,
                                backend_urls=conf.test_web3_provider_list,
                                erc20_token_addresses=[
                                    conf.mn_zerox_token_address,
                                    conf.mn_weth_token_address
                                ],
                                chain=EthereumChain.MAIN_NET)
        cls.market: RadarRelayMarket = RadarRelayMarket(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            symbols=["ZRX-WETH"])
        print("Initializing Radar Relay market... ")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.wallet)
        cls.clock.add_iterator(cls.market)
        stack = contextlib.ExitStack()
        cls._clock = stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.market_logger = EventLogger()
        self.wallet_logger = EventLogger()
        for event_tag in self.market_events:
            self.market.add_listener(event_tag, self.market_logger)
        for event_tag in self.wallet_events:
            self.wallet.add_listener(event_tag, self.wallet_logger)

    def tearDown(self):
        for event_tag in self.market_events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None
        for event_tag in self.wallet_events:
            self.wallet.remove_listener(event_tag, self.wallet_logger)
        self.wallet_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        maker_buy_trade_fee: TradeFee = self.market.get_fee(
            "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, 20, 0.01)
        self.assertEqual(maker_buy_trade_fee.percent, 0)
        self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0)
        taker_buy_trade_fee: TradeFee = self.market.get_fee(
            "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, 20)
        self.assertEqual(taker_buy_trade_fee.percent, 0)
        self.assertEqual(len(taker_buy_trade_fee.flat_fees), 1)
        self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH")

    def test_get_wallet_balances(self):
        balances = self.market.get_all_balances()
        self.assertGreaterEqual((balances["ETH"]), 0)
        self.assertGreaterEqual((balances["WETH"]), 0)

    def test_single_limit_order_cancel(self):
        symbol: str = "ZRX-WETH"
        current_price: float = self.market.get_price(symbol, True)
        amount: float = 10
        expires = int(time.time() + 60 * 5)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        buy_order_id = self.market.buy(symbol=symbol,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       0.2 * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        self.assertEqual(quantized_amount,
                         Decimal(buy_order_opened_event.amount))

        self.run_parallel(self.market.cancel_order(buy_order_id))
        [buy_order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_cancelled_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_limit_buy_and_sell_and_cancel_all(self):
        symbol: str = "ZRX-WETH"
        current_price: float = self.market.get_price(symbol, True)
        amount: float = 10
        expires = int(time.time() + 60 * 5)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        buy_order_id = self.market.buy(symbol=symbol,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       0.2 * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(buy_order_id, buy_order_opened_event.order_id)
        self.assertEqual(quantized_amount,
                         Decimal(buy_order_opened_event.amount))
        self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)

        # Reset the logs
        self.market_logger.clear()

        sell_order_id = self.market.sell(symbol=symbol,
                                         amount=amount,
                                         order_type=OrderType.LIMIT,
                                         price=current_price +
                                         0.2 * current_price,
                                         expiration_ts=expires)
        [sell_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        self.assertEqual(sell_order_id, sell_order_opened_event.order_id)
        self.assertEqual(quantized_amount,
                         Decimal(sell_order_opened_event.amount))
        self.assertEqual("ZRX-WETH", sell_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type)

        [cancellation_results
         ] = self.run_parallel(self.market.cancel_all(60 * 5))
        self.assertEqual(cancellation_results[0],
                         CancellationResult(buy_order_id, True))
        self.assertEqual(cancellation_results[1],
                         CancellationResult(sell_order_id, True))
        # Reset the logs
        self.market_logger.clear()

    def test_order_expire(self):
        symbol: str = "ZRX-WETH"
        current_price: float = self.market.get_price(symbol, True)
        amount: float = 10
        expires = int(time.time() + 60 * 3)  # expires in 3 min
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        buy_order_id = self.market.buy(symbol=symbol,
                                       amount=amount,
                                       order_type=OrderType.LIMIT,
                                       price=current_price -
                                       0.2 * current_price,
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))

        self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        [buy_order_expired_event] = self.run_parallel(
            self.market_logger.wait_for(OrderExpiredEvent, 60 * 3))
        self.assertEqual(buy_order_opened_event.order_id,
                         buy_order_expired_event.order_id)

        # Reset the logs
        self.market_logger.clear()

    def test_market_buy(self):
        amount: float = 5
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRX-WETH", amount)
        order_id = self.market.buy("ZRX-WETH", amount, OrderType.MARKET)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual("ZRX", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_market_sell(self):
        amount: float = 5
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRX-WETH", amount)
        order_id = self.market.sell("ZRX-WETH", amount, OrderType.MARKET)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        order_filled_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]

        self.assertTrue([
            evt.order_type == OrderType.MARKET for evt in order_filled_events
        ])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         float(order_completed_event.base_asset_amount))
        self.assertEqual("ZRX", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_wrap_eth(self):
        amount_to_wrap = 0.01
        tx_hash = self.wallet.wrap_eth(amount_to_wrap)
        [tx_completed_event] = self.run_parallel(
            self.wallet_logger.wait_for(WalletWrappedEthEvent))
        tx_completed_event: WalletWrappedEthEvent = tx_completed_event

        self.assertEqual(tx_hash, tx_completed_event.tx_hash)
        self.assertEqual(amount_to_wrap, tx_completed_event.amount)
        self.assertEqual(self.wallet.address, tx_completed_event.address)

    def test_unwrap_eth(self):
        amount_to_unwrap = 0.01
        tx_hash = self.wallet.unwrap_eth(amount_to_unwrap)
        [tx_completed_event] = self.run_parallel(
            self.wallet_logger.wait_for(WalletUnwrappedEthEvent))
        tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event

        self.assertEqual(tx_hash, tx_completed_event.tx_hash)
        self.assertEqual(amount_to_unwrap, tx_completed_event.amount)
        self.assertEqual(self.wallet.address, tx_completed_event.address)
class HuobiOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[HuobiOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["btcusdt", "xrpusdt"]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: HuobiOrderBookTracker = HuobiOrderBookTracker(
            data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API,
            trading_pairs=cls.trading_pairs)
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(
            cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        while True:
            if len(cls.order_book_tracker.order_books) > 0:
                print("Initialized real-time order books.")
                return
            await asyncio.sleep(1)

    async def run_parallel_async(self, *tasks, timeout=None):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        timer = 0
        while not future.done():
            if timeout and timer > timeout:
                raise Exception(
                    "Time out running parallel async task in tests.")
            timer += 1
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def setUp(self):
        self.event_logger = EventLogger()
        for event_tag in self.events:
            for trading_pair, order_book in self.order_book_tracker.order_books.items(
            ):
                order_book.add_listener(event_tag, self.event_logger)

    def test_order_book_trade_event_emission(self):
        """
        Test if order book tracker is able to retrieve order book trade message from exchange and
        emit order book trade events after correctly parsing the trade messages
        """
        self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent))
        for ob_trade_event in self.event_logger.event_log:
            self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent)
            self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs)
            self.assertTrue(type(ob_trade_event.timestamp) in [float, int])
            self.assertTrue(type(ob_trade_event.amount) == float)
            self.assertTrue(type(ob_trade_event.price) == float)
            self.assertTrue(type(ob_trade_event.type) == TradeType)
            self.assertTrue(
                math.ceil(math.log10(ob_trade_event.timestamp)) == 10)
            self.assertTrue(ob_trade_event.amount > 0)
            self.assertTrue(ob_trade_event.price > 0)

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        btcusdt_book: OrderBook = order_books["btcusdt"]
        xrpusdt_book: OrderBook = order_books["xrpusdt"]
        # print(btcusdt_book.snapshot)
        # print(xrpusdt_book.snapshot)
        self.assertGreaterEqual(
            btcusdt_book.get_price_for_volume(True, 10).result_price,
            btcusdt_book.get_price(True))
        self.assertLessEqual(
            btcusdt_book.get_price_for_volume(False, 10).result_price,
            btcusdt_book.get_price(False))
        self.assertGreaterEqual(
            xrpusdt_book.get_price_for_volume(True, 10000).result_price,
            xrpusdt_book.get_price(True))
        self.assertLessEqual(
            xrpusdt_book.get_price_for_volume(False, 10000).result_price,
            xrpusdt_book.get_price(False))
Esempio n. 29
0
class EterbaseMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled
    ]

    market: EterbaseMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: EterbaseMarket = EterbaseMarket(conf.eterbase_api_key,
                                                    conf.eterbase_secret_key,
                                                    conf.eterbase_account,
                                                    trading_pairs=["ETHEUR"])
        print("Initializing Eterbase market... this will take about a minute.")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        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.")

    @classmethod
    def tearDownClass(cls) -> None:
        cls.stack.close()

    @classmethod
    async def wait_til_ready(cls):
        while True:
            now = time.time()
            next_iteration = now // 1.0 + 1
            if cls.market.ready:
                break
            else:
                await cls._clock.run_til(next_iteration)
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../eterbase_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

        self.market_logger = EventLogger()
        for event_tag in self.events:
            self.market.add_listener(event_tag, self.market_logger)

    def tearDown(self):
        for event_tag in self.events:
            self.market.remove_listener(event_tag, self.market_logger)
        self.market_logger = None

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            now = time.time()
            next_iteration = now // 1.0 + 1
            await self.clock.run_til(next_iteration)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def test_get_fee(self):
        limit_fee: TradeFee = self.market.get_fee("ETH", "EUR",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, 1, 1)
        self.assertGreaterEqual(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("ETH", "EUR",
                                                   OrderType.MARKET,
                                                   TradeType.BUY, 1)
        self.assertGreaterEqual(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_limit_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))
        trading_pair = "ETHEUR"
        amount: Decimal = Decimal("0.02")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        bid_price: Decimal = current_bid_price + Decimal(
            "0.05") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)

        order_id = self.market.buy(trading_pair, quantized_amount,
                                   OrderType.LIMIT, quantize_bid_price)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("EUR", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_limit_sell(self):
        trading_pair = "ETHEUR"
        amount: Decimal = Decimal("0.01")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        ask_price: Decimal = current_ask_price - Decimal(
            "0.05") * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price)

        order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT,
                                    quantize_ask_price)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.amount * t.price for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("EUR", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount, 1)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_market_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.2"))
        trading_pair = "ETHEUR"
        amount: Decimal = Decimal("0.02")
        quantize_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        ask_price: Decimal = current_ask_price - Decimal(
            "0.05") * current_ask_price
        quantize_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price)

        order_id = self.market.buy(trading_pair, quantize_amount,
                                   OrderType.MARKET, quantize_price)

        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event

        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded: Decimal = sum(t.amount for t in trade_events)
        quote_amount_traded: Decimal = sum(t.amount * t.price
                                           for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.MARKET for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        # not possible to exactly defined, as reuest is in COSTS (amount*price)
        self.assertAlmostEqual(quantize_amount * quantize_price,
                               order_completed_event.quote_asset_amount, 1)

        sig_dig = abs(
            (quantize_amount * quantize_price).as_tuple().exponent) - 1

        str_quant_cost = ("{0:." + str(sig_dig) + "g}").format(
            quantize_amount * quantize_price)
        str_order_cost = ("{0:." + str(sig_dig) + "g}").format(
            order_completed_event.quote_asset_amount)

        quant_cost = None
        order_cost = None

        if re.search(r'e+', str(quant_cost)):
            quant_cost = Decimal("{:.0f}".format(Decimal(str_quant_cost)))
            order_cost = Decimal("{:.0f}".format(Decimal(str_order_cost)))
        else:
            quant_cost = Decimal(str_quant_cost)
            order_cost = Decimal(str_order_cost)

        self.assertAlmostEqual(quant_cost, order_cost)

        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("EUR", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only
    def test_market_sell(self):
        trading_pair = "ETHEUR"
        amount: Decimal = Decimal("0.02")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id = self.market.sell(trading_pair, amount, OrderType.MARKET)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent)
        ]
        base_amount_traded = sum(t.amount for t in trade_events)
        quote_amount_traded = sum(t.price * t.amount for t in trade_events)

        self.assertTrue(
            [evt.order_type == OrderType.MARKET for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("EUR", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount, 1)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_cancel_order(self):
        trading_pair = "ETHEUR"

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = 10 / current_bid_price
        self.assertGreater(self.market.get_balance("ETH"), amount)

        bid_price: Decimal = current_bid_price - Decimal(
            "0.1") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        client_order_id = self.market.buy(trading_pair, quantized_amount,
                                          OrderType.LIMIT, quantize_bid_price)
        self.market.cancel(trading_pair, client_order_id)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        order_cancelled_event: OrderCancelledEvent = order_cancelled_event
        self.assertEqual(order_cancelled_event.order_id, client_order_id)

    def test_cancel_all(self):
        trading_pair = "ETHEUR"
        bid_price: Decimal = self.market.get_price(trading_pair,
                                                   True) * Decimal("0.5")
        ask_price: Decimal = self.market.get_price(trading_pair, False) * 2
        amount: Decimal = 10 / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        # Intentionally setting invalid price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price * Decimal("0.7"))
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price * Decimal("1.5"))

        self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT,
                        quantize_bid_price)
        self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT,
                         quantize_ask_price)
        self.run_parallel(asyncio.sleep(1))
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv),
                         "List order test requires manual action.")
    def test_list_orders(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))
        trading_pair = "ETHEUR"
        amount: Decimal = Decimal("0.02")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        bid_price: Decimal = current_bid_price + Decimal(
            "0.05") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)

        self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT,
                        quantize_bid_price)
        self.run_parallel(asyncio.sleep(1))
        [order_details] = self.run_parallel(self.market.list_orders())
        self.assertGreaterEqual(len(order_details), 1)

        self.market_logger.clear()

    @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv),
                         "Withdraw test requires manual action.")
    def test_withdraw(self):
        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("XBASE"), Decimal('1'))

        # Withdraw XBASE from Eterbase to test wallet.
        self.market.withdraw(self.wallet.address, "XBASE", Decimal('1'))
        [withdraw_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketWithdrawAssetEvent))
        withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event
        self.assertEqual(self.wallet.address, withdraw_asset_event.to_address)
        self.assertEqual("XBASE", withdraw_asset_event.asset_name)
        self.assertEqual(Decimal('1'), withdraw_asset_event.amount)
        self.assertEqual(withdraw_asset_event.fee_amount, Decimal(0))

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

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

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

            amount: Decimal = Decimal("0.04")
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, amount)

            order_id = self.market.buy(trading_pair, quantized_amount,
                                       OrderType.LIMIT, quantize_bid_price)
            [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)
            print("close out the current market and start another market")
            # 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: EterbaseMarket = EterbaseMarket(
                eterbase_api_key=conf.eterbase_api_key,
                eterbase_secret_key=conf.eterbase_secret_key,
                eterbase_account=conf.eterbase_account,
                trading_pairs=["ETHUSDT", "ETHEUR"])
            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.market.cancel(trading_pair, 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)

    def test_order_fill_record(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ETHEUR"

        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:
            # Try to buy 0.04 ETH from the exchange, and watch for completion event.
            amount: Decimal = Decimal("0.01")

            current_ask_price: Decimal = self.market.get_price(
                trading_pair, False)
            ask_price: Decimal = current_ask_price - Decimal(
                "0.05") * current_ask_price
            quantize_price: Decimal = self.market.quantize_order_price(
                trading_pair, ask_price)

            order_id = self.market.buy(trading_pair, amount, OrderType.MARKET,
                                       quantize_price)

            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logger
            self.market_logger.clear()

            # Try to sell back the same amount of ETH to the exchange, and watch for completion event.
            amount = buy_order_completed_event.base_asset_amount
            order_id = self.market.sell(trading_pair, amount)
            [sell_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(SellOrderCompletedEvent))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(
                config_path)

            self.assertEqual(2, len(trade_fills))
            buy_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "BUY"
            ]
            sell_fills: List[TradeFill] = [
                t for t in trade_fills if t.trade_type == "SELL"
            ]
            self.assertEqual(1, len(buy_fills))
            self.assertEqual(1, len(sell_fills))

            order_id = None

        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)
class BinanceOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[BinanceOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["BTCUSDT", "XRPUSDT"]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: BinanceOrderBookTracker = BinanceOrderBookTracker(
            trading_pairs=cls.trading_pairs)
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(
            cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        while True:
            if len(cls.order_book_tracker.order_books) > 0:
                print("Initialized real-time order books.")
                return
            await asyncio.sleep(1)

    async def run_parallel_async(self, *tasks):
        future: asyncio.Future = safe_ensure_future(safe_gather(*tasks))
        while not future.done():
            await asyncio.sleep(1.0)
        return future.result()

    def run_parallel(self, *tasks):
        return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks))

    def setUp(self):
        self.event_logger = EventLogger()
        for event_tag in self.events:
            for trading_pair, order_book in self.order_book_tracker.order_books.items(
            ):
                order_book.add_listener(event_tag, self.event_logger)

    def test_order_book_trade_event_emission(self):
        """
        Test if order book tracker is able to retrieve order book trade message from exchange and
        emit order book trade events after correctly parsing the trade messages
        """
        self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent))
        for ob_trade_event in self.event_logger.event_log:
            print(f"ob_trade_event: {ob_trade_event}")
            self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent)
            self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs)
            self.assertTrue(type(ob_trade_event.timestamp) == float)
            self.assertTrue(type(ob_trade_event.amount) == float)
            self.assertTrue(type(ob_trade_event.price) == float)
            self.assertTrue(type(ob_trade_event.type) == TradeType)
            self.assertTrue(
                math.ceil(math.log10(ob_trade_event.timestamp)) == 10)
            self.assertTrue(ob_trade_event.amount > 0)
            self.assertTrue(ob_trade_event.price > 0)

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(10.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        btcusdt_book: OrderBook = order_books["BTCUSDT"]
        xrpusdt_book: OrderBook = order_books["XRPUSDT"]
        # print(btcusdt_book.snapshot)
        # print("xrpusdt")
        # print(xrpusdt_book.snapshot)
        self.assertGreaterEqual(
            btcusdt_book.get_price_for_volume(True, 10).result_price,
            btcusdt_book.get_price(True))
        self.assertLessEqual(
            btcusdt_book.get_price_for_volume(False, 10).result_price,
            btcusdt_book.get_price(False))
        self.assertGreaterEqual(
            xrpusdt_book.get_price_for_volume(True, 10000).result_price,
            xrpusdt_book.get_price(True))
        self.assertLessEqual(
            xrpusdt_book.get_price_for_volume(False, 10000).result_price,
            xrpusdt_book.get_price(False))
        for order_book in self.order_book_tracker.order_books.values():
            print(order_book.last_trade_price)
            self.assertFalse(math.isnan(order_book.last_trade_price))

    def test_api_get_last_traded_prices(self):
        prices = self.ev_loop.run_until_complete(
            BinanceAPIOrderBookDataSource.get_last_traded_prices(
                ["BTCUSDT", "LTCBTC"]))
        for key, value in prices.items():
            print(f"{key} last_trade_price: {value}")
        self.assertGreater(prices["BTCUSDT"], 1000)
        self.assertLess(prices["LTCBTC"], 1)