示例#1
0
class DydxExchangeUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
    ]

    market: DydxExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack
    base_api_url = "api.dydx.exchange"

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        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(cls.base_api_url, [])

            cls.web_app.start()

            cls.ev_loop.run_until_complete(cls.web_app.wait_til_started())

            cls._req_patcher = mock.patch.object(requests.Session,
                                                 "request",
                                                 autospec=True)
            cls._req_url_mock = cls._req_patcher.start()
            cls._req_url_mock.side_effect = MockWebServer.reroute_request

            cls.web_app.update_response("get",
                                        cls.base_api_url,
                                        f"/v1/accounts/{WALLET_ADDRESS}",
                                        FixtureDydx.BALANCES,
                                        params={'number': f'{ACCOUNT_NUMBER}'})

            cls.web_app.update_response("get", cls.base_api_url, "/v2/markets",
                                        FixtureDydx.MARKETS)
            cls.web_app.update_response("get", cls.base_api_url,
                                        "/v1/orderbook/WETH-USDC",
                                        FixtureDydx.WETHUSDC_SNAP)
            cls._buy_order_exchange_id = "0xb0751a113c759779ff5fd6a53b37b26211a9\
              f8845d443323b9f877f32d9aafd9"

            cls._sell_order_exchange_id = "0x03dfd18edc2f26fc9298edcd28ca6cad4971\
              bd1f44d40253d5154b0d1f217680"

            cls.web_app.update_response(
                "delete", cls.base_api_url,
                f"/v2/orders/{cls._buy_order_exchange_id}",
                FixtureDydx.CANCEL_ORDER_BUY)
            cls.web_app.update_response(
                "delete", cls.base_api_url,
                f"/v2/orders/{cls._sell_order_exchange_id}",
                FixtureDydx.CANCEL_ORDER_SELL)
            ws_base_url = "wss://api.dydx.exchange/v1/ws"
            cls._ws_user_url = f"{ws_base_url}"
            MockWebSocketServerFactory.start_new_server(cls._ws_user_url)
            MockWebSocketServerFactory.start_new_server(f"{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.dydx.\
                dydx_exchange.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()

        cls.market: DydxExchange = DydxExchange(
            dydx_eth_private_key=PRIVATE_KEY,
            dydx_node_address=NODE_ADDRESS,
            poll_interval=10.0,
            trading_pairs=['WETH-USDC'],
            trading_required=True)

        print("Initializing Dydx market... ")
        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._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__, "../dydx_test.sqlite"))
        try:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

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

    def tearDown(self):
        for event_tag in self.market_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)
            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 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["order"]["clientId"] = 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,
                    fixture_ws_3=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,
                                         "/v2/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 and fixture_ws_1 is not None:
            self.web_app.update_response("get",
                                         self.base_api_url,
                                         "/v2/fills",
                                         FixtureDydx.FILLS,
                                         params={
                                             "orderId": order_id,
                                             "limit": 100
                                         })
            data = self.fixture(fixture_ws_1, id=order_id)
            MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url,
                                                            data,
                                                            delay=0.1)
            if fixture_ws_2 is not None:
                data = self.fixture(fixture_ws_2, id=order_id)
                MockWebSocketServerFactory.send_json_threadsafe(
                    self._ws_user_url, data, delay=0.11)
            if fixture_ws_3 is not None:
                MockWebSocketServerFactory.send_json_threadsafe(
                    self._ws_user_url, fixture_ws_3, delay=0.1)
        return order_id

    def test_get_fee(self):
        limit_trade_fee: TradeFee = self.market.get_fee(
            "WETH", "USDC", OrderType.LIMIT_MAKER, TradeType.SELL, 10000, 1)
        self.assertLess(limit_trade_fee.percent, 0.01)

    def test_limit_buy(self):
        self.assertGreater(self.market.get_balance("USDC"), 16000)
        # Try to buy 40 ETH from the exchange, and watch for creation event.
        trading_pair = "WETH-USDC"
        amount: Decimal = Decimal("40.0")
        ask_price: Decimal = self.market.get_price(trading_pair, False)
        buy_order_id: str = self.place_order(True, "WETH-USDC", amount,
                                             OrderType.LIMIT,
                                             ask_price * Decimal('1.5'), 10001,
                                             FixtureDydx.BUY_LIMIT_ORDER,
                                             FixtureDydx.WS_AFTER_BUY_1,
                                             FixtureDydx.WS_AFTER_BUY_2,
                                             FixtureDydx.WS_AFTER_BUY_3)
        [buy_order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCompletedEvent))
        self.assertEqual(buy_order_id, buy_order_completed_event.order_id)

    def test_limit_sell(self):
        self.assertGreater(self.market.get_balance("WETH"), 40)
        # Try to sell 40 ETH to the exchange, and watch for creation event.
        trading_pair = "WETH-USDC"
        amount: Decimal = Decimal("40.0")
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        sell_order_id: str = self.place_order(
            False, "WETH-USDC", amount, OrderType.LIMIT,
            bid_price * Decimal('0.5'), 10001, FixtureDydx.SELL_LIMIT_ORDER,
            FixtureDydx.WS_AFTER_SELL_1, FixtureDydx.WS_AFTER_SELL_2,
            FixtureDydx.WS_AFTER_SELL_3)
        [sell_order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        self.assertEqual(sell_order_id, sell_order_completed_event.order_id)

    def test_limit_maker_rejections(self):
        self.assertGreater(self.market.get_balance("WETH"), 40)
        trading_pair = "WETH-USDC"
        amount: Decimal = Decimal("40.0")
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        sell_order_id: str = self.place_order(
            False, "WETH-USDC", amount, OrderType.LIMIT_MAKER,
            bid_price * Decimal('0.5'), 10001,
            FixtureDydx.SELL_LIMIT_MAKER_ORDER, FixtureDydx.WS_AFTER_SELL_1,
            FixtureDydx.LIMIT_MAKER_SELL_ERROR)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(sell_order_id, order_cancelled_event.order_id)

    def test_limit_makers_unfilled(self):
        self.assertGreater(self.market.get_balance("USDC"), 16000)
        trading_pair = "WETH-USDC"
        amount: Decimal = Decimal("40.0")
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        buy_order_id: str = self.place_order(True, "WETH-USDC", amount,
                                             OrderType.LIMIT_MAKER,
                                             bid_price * Decimal('0.5'), 10001,
                                             FixtureDydx.BUY_LIMIT_MAKER_ORDER,
                                             FixtureDydx.WS_AFTER_BUY_1)
        self.run_parallel(asyncio.sleep(6.0))
        self.market.cancel(trading_pair, buy_order_id)

        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_json_threadsafe(
                self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))

    def test_market_buy(self):
        # Market orders not supported on Dydx
        pass

    def test_market_sell(self):
        # Market orders not supported on Dydx
        pass

    def test_cancel_order(self):
        self.assertGreater(self.market.get_balance("USDC"), 16000)
        trading_pair = "WETH-USDC"
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("40.0")

        # Intentionally setting price far away from best ask
        client_order_id = self.place_order(True, "WETH-USDC", amount,
                                           OrderType.LIMIT_MAKER,
                                           bid_price * Decimal('0.5'), 10001,
                                           FixtureDydx.BUY_LIMIT_ORDER,
                                           FixtureDydx.WS_AFTER_BUY_1)
        self.run_parallel(asyncio.sleep(1.0))
        self.market.cancel(trading_pair, client_order_id)
        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_json_threadsafe(
                self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1)
        [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_cancel_all(self):
        self.assertGreater(self.market.get_balance("USDC"), 16000)
        trading_pair = "WETH-USDC"
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal("40.0")

        # Intentionally setting price far away from best ask
        client_order_id = self.place_order(True, "WETH-USDC", amount,
                                           OrderType.LIMIT,
                                           bid_price * Decimal('0.5'), 10001,
                                           FixtureDydx.BUY_LIMIT_ORDER,
                                           FixtureDydx.WS_AFTER_BUY_1)
        self.run_parallel(asyncio.sleep(1.0))
        self.run_parallel(self.market.cancel_all(5.0))
        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_json_threadsafe(
                self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1)
        [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_limit_orders(self):
        self.assertGreater(self.market.get_balance("USDC"), 16000)
        trading_pair = "WETH-USDC"
        amount: Decimal = Decimal("40.0")
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        buy_order_id: str = self.place_order(True, "WETH-USDC", amount,
                                             OrderType.LIMIT,
                                             bid_price * Decimal('0.5'), 10001,
                                             FixtureDydx.BUY_LIMIT_ORDER,
                                             FixtureDydx.WS_AFTER_BUY_1)
        [buy_order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(1, len(self.market.limit_orders))
        self.assertEqual(amount, self.market.limit_orders[0].quantity)
        self.market.cancel(trading_pair, buy_order_id)
        if API_MOCK_ENABLED:
            MockWebSocketServerFactory.send_json_threadsafe(
                self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1)
        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))

    def test_order_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
        trading_pair: str = "WETH-USDC"

        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market],
                                                    config_path, strategy_name)
        recorder.start()
        try:
            self.assertEqual(0, len(self.market.tracking_states))
            self.assertGreater(self.market.get_balance("USDC"), 16000)
            amount: Decimal = Decimal("40.0")
            current_bid_price: Decimal = self.market.get_price(
                trading_pair, True)
            bid_price: Decimal = Decimal("0.5") * current_bid_price
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                trading_pair, bid_price)
            order_id = self.place_order(True, trading_pair, amount,
                                        OrderType.LIMIT, quantize_bid_price,
                                        10001, FixtureDydx.BUY_LIMIT_ORDER,
                                        FixtureDydx.WS_AFTER_BUY_1)

            [order_created_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event

            self.assertEqual(order_id, order_created_event.order_id)

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

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

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

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)

            for event_tag in self.market_events:
                self.market.remove_listener(event_tag, self.market_logger)

            self.market: DydxExchange = DydxExchange(
                dydx_eth_private_key=PRIVATE_KEY,
                dydx_node_address=NODE_ADDRESS,
                poll_interval=10.0,
                trading_pairs=[trading_pair],
                trading_required=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))
            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.run_parallel(asyncio.sleep(5.0))
            self.market.cancel(trading_pair, order_id)
            if API_MOCK_ENABLED:
                MockWebSocketServerFactory.send_json_threadsafe(
                    self._ws_user_url,
                    FixtureDydx.WS_AFTER_CANCEL_BUY,
                    delay=0.1)
            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)
                if API_MOCK_ENABLED:
                    MockWebSocketServerFactory.send_json_threadsafe(
                        self._ws_user_url,
                        FixtureDydx.WS_AFTER_CANCEL_BUY,
                        delay=0.1)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()

    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:
            ask_price: Decimal = self.market.get_price("WETH-USDC", True)
            self.assertGreater(self.market.get_balance("USDC"), 16000)
            amount: Decimal = Decimal('40')
            order_id = self.place_order(
                True, "WETH-USDC", amount, OrderType.LIMIT,
                ask_price * Decimal('1.5'), 1000100010001000,
                FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1,
                FixtureDydx.WS_AFTER_BUY_2, FixtureDydx.WS_AFTER_BUY_3)
            [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("WETH-USDC", False)
            amount = buy_order_completed_event.base_asset_amount
            order_id = self.place_order(
                False, "WETH-USDC", amount, OrderType.LIMIT, ask_price,
                1000200010001000, FixtureDydx.SELL_LIMIT_ORDER,
                FixtureDydx.WS_AFTER_SELL_1, FixtureDydx.WS_AFTER_SELL_2,
                FixtureDydx.WS_AFTER_SELL_3)
            [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("WETH-USDC", order_id)
            recorder.stop()
            os.unlink(self.db_path)
class KrakenExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled,
        MarketEvent.OrderCancelled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled
    ]

    market: KrakenExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: KrakenExchange = KrakenExchange(conf.kraken_api_key,
                                                    conf.kraken_secret_key,
                                                    trading_pairs=[PAIR])

        cls.count = 0

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

    @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)
            cls.count += 1
            await asyncio.sleep(1.0)

    def setUp(self):
        self.db_path: str = realpath(join(__file__, "../kraken_test.sqlite"))
        try:
            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.run_async(self.run_parallel_async(*tasks))

    def run_async(self, task):
        return self.ev_loop.run_until_complete(task)

    def sleep(self, t=1.0):
        self.run_parallel(asyncio.sleep(t))

    def test_get_fee(self):
        limit_fee: AddedToCostTradeFee = self.market.get_fee(
            BASE, QUOTE, 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(
            BASE, QUOTE, 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["kraken_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.0026"), taker_fee.percent)
        fee_overrides_config_map["kraken_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["kraken_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.0016"), maker_fee.percent)
        fee_overrides_config_map["kraken_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):
        order_id = 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)
        return order_id

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

    def test_limit_taker_buy(self):
        self.assertGreater(self.market.get_balance(QUOTE), 6)
        trading_pair = PAIR

        self.sleep(3)
        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)
        [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) and t.amount is not None
        ]
        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(BASE, order_completed_event.base_asset)
        self.assertEqual(QUOTE, 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):
        self.assertGreater(self.market.get_balance(BASE), 0.02)
        trading_pair = PAIR

        self.sleep(3)
        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)
        [order_completed_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        trade_events: List[OrderFilledEvent] = [
            t for t in self.market_logger.event_log
            if isinstance(t, OrderFilledEvent) and t.amount is not None
        ]
        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(BASE, order_completed_event.base_asset)
        self.assertEqual(QUOTE, 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 underpriced_limit_buy(self):
        self.assertGreater(self.market.get_balance(QUOTE), 4)
        trading_pair = PAIR

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        bid_price: Decimal = current_bid_price * Decimal('0.005')
        quantized_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 = self.place_order(True, trading_pair, quantized_amount,
                                    OrderType.LIMIT_MAKER, quantized_bid_price)

        return order_id

    def underpriced_limit_buy_multiple(self, num):
        order_ids = []
        for _ in range(num):
            order_ids.append(self.underpriced_limit_buy())
            self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
        return order_ids

    def test_cancel_order(self):
        order_id = self.underpriced_limit_buy()
        self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))

        self.cancel_order(PAIR, 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):
        order_ids = self.underpriced_limit_buy_multiple(2)

        cancelled_orders = self.run_async(self.market.cancel_all(10.))
        self.assertEqual([order.order_id for order in cancelled_orders],
                         order_ids)
        self.assertTrue([order.success for order in cancelled_orders])

    def test_order_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 at fraction of USDC market price, and watch for order creation event.
            order_id = self.underpriced_limit_buy()
            [order_created_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event
            self.assertEqual(order_id, order_created_event.order_id)

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

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

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

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: KrakenExchange = KrakenExchange(
                conf.kraken_api_key,
                conf.kraken_secret_key,
                trading_pairs=[PAIR])
            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(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(PAIR, order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            unlink(self.db_path)

    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 0.02 ETH from the exchange, and watch for completion event.
            price: Decimal = self.market.get_price(PAIR, True)
            amount: Decimal = Decimal("0.02")
            quantized_amount: Decimal = self.market.quantize_order_amount(
                PAIR, amount)
            order_id = self.place_order(True, PAIR, quantized_amount,
                                        OrderType.LIMIT, price)
            [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(PAIR, False)
            amount = buy_order_completed_event.base_asset_amount
            quantized_amount: Decimal = self.market.quantize_order_amount(
                PAIR, amount)
            order_id = self.place_order(False, PAIR, quantized_amount,
                                        OrderType.LIMIT, price)
            [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(PAIR, order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            unlink(self.db_path)

    def test_pair_convesion(self):
        for pair in self.market.trading_rules:
            exchange_pair = convert_to_exchange_trading_pair(pair)
            self.assertTrue(exchange_pair in self.market.order_books)
示例#3
0
class LiquidExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]

    market: LiquidExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.ev_loop = asyncio.get_event_loop()

        if API_MOCK_ENABLED:
            cls.web_app = MockWebServer.get_instance()
            cls.web_app.add_host_to_mock(API_HOST,
                                         ["/products", "/currencies"])
            cls.web_app.start()
            cls.ev_loop.run_until_complete(cls.web_app.wait_til_started())
            cls._patcher = mock.patch("aiohttp.client.URL")
            cls._url_mock = cls._patcher.start()
            cls._url_mock.side_effect = cls.web_app.reroute_local
            cls.web_app.update_response("get", API_HOST, "/fiat_accounts",
                                        FixtureLiquid.FIAT_ACCOUNTS)
            cls.web_app.update_response("get", API_HOST, "/crypto_accounts",
                                        FixtureLiquid.CRYPTO_ACCOUNTS)
            cls.web_app.update_response("get", API_HOST, "/orders",
                                        FixtureLiquid.ORDERS_GET)
            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.liquid.liquid_exchange.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: LiquidExchange = LiquidExchange(
            API_KEY,
            API_SECRET,
            poll_interval=5,
            trading_pairs=['CEL-ETH'],
        )
        # cls.ev_loop.run_until_complete(cls.market._update_balances())
        print("Initializing Liquid market... this will take about a minute.")
        cls.clock.add_iterator(cls.market)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

    @classmethod
    def tearDownClass(cls) -> None:
        if API_MOCK_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._t_nonce_patcher.stop()
        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__, "../liquid_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", "USD", 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", "USD", 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", "USD",
                                                       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["liquid_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["liquid_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["liquid_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["liquid_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, order_resp, get_resp):
        order_id, exchange_id = None, None
        if API_MOCK_ENABLED:
            side = 'buy' if is_buy else 'sell'
            self._t_nonce_mock.return_value = nonce
            order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}"
            resp = order_resp.copy()
            resp["client_order_id"] = order_id
            exchange_id = resp["id"]
            self.web_app.update_response("post", API_HOST, "/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["models"][-1]["client_order_id"] = order_id
            self.web_app.update_response("get", API_HOST, "/orders", resp)
        return order_id, exchange_id

    async def cancel_all_open_orders(self):
        listed_orders = await self.market.list_orders()
        live_orders = [
            o for o in listed_orders.get("models", []) if o["status"] == "live"
        ]
        for order in live_orders:
            path_url = Constants.CANCEL_ORDER_URI.format(
                exchange_order_id=str(order["id"]))
            res = await self.market._api_request("put", path_url=path_url)
            print(res)

    def test_maintain_user_balances(self):
        # self.ev_loop.run_until_complete(self.cancel_all_open_orders())
        # return

        trading_pair = "CEL-ETH"
        base = trading_pair.split("-")[0]
        quote = trading_pair.split("-")[1]
        base_bal = self.market.get_available_balance(base)
        starting_quote_bal = self.market.get_available_balance(quote)
        print(f"{base} available: {base_bal}")
        print(f"starting quote available: {starting_quote_bal}")

        bid_price = self.market.get_price(trading_pair, False)
        buy_price = bid_price * Decimal("0.9")
        buy_price = self.market.quantize_order_price(trading_pair, buy_price)
        amount = Decimal("1")
        post_data = FixtureLiquid.BUY_MARKET_ORDER.copy()
        get_data = FixtureLiquid.ORDERS_UNFILLED.copy()
        if API_MOCK_ENABLED:
            resp = FixtureLiquid.CRYPTO_ACCOUNTS.copy()
            resp[0]["reserved_balance"] = float((buy_price * amount))
            self.web_app.update_response("get", API_HOST, "/crypto_accounts",
                                         resp)
        order_id_1, exchange_id_1 = self.place_order(True, "CEL-ETH", amount,
                                                     OrderType.LIMIT,
                                                     buy_price, 10001,
                                                     post_data, get_data)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        print(f"order_created_event: {order_created_event}")
        self.assertEqual(order_id_1, order_created_event.order_id)

        # ToDo: the test from here on pass fine in real API test mode, for the API mocked we first need to fix
        # https://github.com/CoinAlpha/hummingbot/issues/2222
        if API_MOCK_ENABLED:
            return
        base_bal = self.market.get_available_balance(base)
        quote_bal = self.market.get_available_balance(quote)
        expected_quote_bal = starting_quote_bal - (buy_price * amount)
        self.assertAlmostEqual(quote_bal, expected_quote_bal, 5)
        print(f"{base} available: {base_bal}")
        print(f"{quote} available: {quote_bal}")

        self.run_parallel(asyncio.sleep(5))
        post_data = FixtureLiquid.BUY_MARKET_ORDER.copy()
        get_data = FixtureLiquid.ORDERS_UNFILLED.copy()
        get_data["models"].append(get_data["models"][0].copy())
        get_data["models"][0]["client_order_id"] = order_id_1
        get_data["models"][1]["id"] = get_data["models"][0]["id"] + 1
        if API_MOCK_ENABLED:
            resp = FixtureLiquid.CRYPTO_ACCOUNTS.copy()
            resp[0]["reserved_balance"] = float((2 * buy_price * amount))
            self.web_app.update_response("get", API_HOST, "/crypto_accounts",
                                         resp)
        order_id_2, exchange_id_2 = self.place_order(True, "CEL-ETH", amount,
                                                     OrderType.LIMIT,
                                                     buy_price, 10002,
                                                     post_data, get_data)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        print(f"order_created_event: {order_created_event}")
        self.assertEqual(order_id_2, order_created_event.order_id)

        base_bal = self.market.get_available_balance(base)
        quote_bal = self.market.get_available_balance(quote)
        expected_quote_bal = starting_quote_bal - 2 * (buy_price * amount)
        self.assertAlmostEqual(quote_bal, expected_quote_bal, 5)
        print(f"{base} available: {base_bal}")
        print(f"{quote} available: {quote_bal}")

        if API_MOCK_ENABLED:
            order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(exchange_id_2)}/cancel",
                order_cancel_resp)
            resp = FixtureLiquid.CRYPTO_ACCOUNTS.copy()
            resp[0]["reserved_balance"] = float((buy_price * amount))
            self.web_app.update_response("get", API_HOST, "/crypto_accounts",
                                         resp)
        self.market.cancel("CEL-ETH", order_id_2)
        self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
        quote_bal = self.market.get_available_balance(quote)
        expected_quote_bal = starting_quote_bal - 1 * (buy_price * amount)
        print(f"expected_quote_bal: {expected_quote_bal}")
        print(f"quote_bal: {quote_bal}")
        self.assertAlmostEqual(quote_bal, expected_quote_bal, 5)

        if API_MOCK_ENABLED:
            order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(exchange_id_1)}/cancel",
                order_cancel_resp)

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

    def test_limit_taker_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.01"))

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

        order_id, _ = self.place_order(True, "CEL-ETH", amount,
                                       OrderType.LIMIT, current_price, 10001,
                                       FixtureLiquid.BUY_MARKET_ORDER,
                                       FixtureLiquid.ORDERS_GET_AFTER_BUY)
        [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("CEL", 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 CEL to the exchange, and watch for completion event.
        current_price: Decimal = self.market.get_price("CEL-ETH", False)
        amount = order_completed_event.base_asset_amount
        quantized_amount = order_completed_event.base_asset_amount
        order_id, _ = self.place_order(
            False, "CEL-ETH", amount, OrderType.LIMIT, current_price, 10002,
            FixtureLiquid.SELL_MARKET_ORDER,
            FixtureLiquid.ORDERS_GET_AFTER_MARKET_SELL)
        [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("CEL", 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):
        if API_MOCK_ENABLED:
            return
        trading_pair = "CEL-ETH"

        # 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):
        trading_pair = "CEL-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_order_id, buy_exchange_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001,
            FixtureLiquid.BUY_LIMIT_ORDER_BEFORE_CANCEL,
            FixtureLiquid.ORDERS_GET_AFTER_BUY)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual(buy_order_id, order_created_event.order_id)

        sell_order_id, sell_exchange_id = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002,
            FixtureLiquid.SELL_LIMIT_ORDER_BEFORE_CANCEL,
            FixtureLiquid.ORDERS_GET_AFTER_MARKET_SELL)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(SellOrderCreatedEvent))
        self.assertEqual(sell_order_id, order_created_event.order_id)

        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel",
                order_cancel_resp)
            order_cancel_resp = FixtureLiquid.BUY_LIMIT_ORDER_AFTER_CANCEL
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(sell_exchange_id)}/cancel",
                order_cancel_resp)
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_cancel_all(self):
        trading_pair = "CEL-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_exchange_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001,
            FixtureLiquid.BUY_LIMIT_ORDER_BEFORE_CANCEL,
            FixtureLiquid.ORDERS_GET_AFTER_BUY)
        _, sell_exchange_id = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002,
            FixtureLiquid.SELL_LIMIT_ORDER_BEFORE_CANCEL,
            FixtureLiquid.ORDERS_GET_AFTER_MARKET_SELL)

        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel",
                order_cancel_resp)
            order_cancel_resp = FixtureLiquid.BUY_LIMIT_ORDER_AFTER_CANCEL
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(sell_exchange_id)}/cancel",
                order_cancel_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"
        sql: SQLConnectionManager = SQLConnectionManager(
            SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market],
                                                    config_path, strategy_name)
        recorder.start()

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

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

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

            order_id, buy_exchange_id = self.place_order(
                True, "CEL-ETH", quantized_amount, OrderType.LIMIT_MAKER,
                quantize_bid_price, 10001, FixtureLiquid.ORDER_SAVE_RESTORE,
                FixtureLiquid.ORDERS_GET_AFTER_BUY)
            [order_created_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event
            self.assertEqual(order_id, order_created_event.order_id)

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

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

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

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.events:
                self.market.remove_listener(event_tag, self.market_logger)

            self.market: LiquidExchange = LiquidExchange(
                API_KEY, API_SECRET, trading_pairs=['ETH-USD', 'CEL-ETH'])

            for event_tag in self.events:
                self.market.add_listener(event_tag, self.market_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [self.market], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.clock.add_iterator(self.market)
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            self.market.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(self.market.limit_orders))
            self.assertEqual(1, len(self.market.tracking_states))

            # Cancel the order and verify that the change is saved.
            if API_MOCK_ENABLED:
                order_cancel_resp = FixtureLiquid.ORDER_CANCEL_SAVE_RESTORE.copy(
                )
                self.web_app.update_response(
                    "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel",
                    order_cancel_resp)
            self.market.cancel("CEL-ETH", order_id)
            self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
            order_id = None
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.assertEqual(0, len(saved_market_states.saved_state))
        finally:
            if order_id is not None:
                self.market.cancel("CEL-ETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

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

    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 CEL from the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price("CEL-ETH", True)
            amount: Decimal = 1
            order_id, _ = self.place_order(
                True, "CEL-ETH", amount, OrderType.LIMIT, current_price, 10001,
                FixtureLiquid.FILLED_BUY_LIMIT_ORDER,
                FixtureLiquid.ORDERS_GET_AFTER_LIMIT_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 CEL to the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price("CEL-ETH", False)
            amount = buy_order_completed_event.base_asset_amount
            order_id, _ = self.place_order(
                False, "CEL-ETH", amount, OrderType.LIMIT, current_price,
                10002, FixtureLiquid.FILLED_SELL_LIMIT_ORDER,
                FixtureLiquid.ORDERS_GET_AFTER_LIMIT_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.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("CEL-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))
示例#4
0
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
        else:
            chain = EthereumChain.MAIN_NET
        cls.chain = chain
        cls.base_token_symbol = conf.test_bamboo_relay_base_token_symbol
        cls.quote_token_symbol = 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,
            symbols=[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 = 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", self.quote_token_symbol, 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", self.quote_token_symbol, 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[self.quote_token_symbol]), 0)

    def test_single_limit_order_cancel(self):
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        current_price: float = self.market.get_price(symbol, True)
        amount: float = 0.01
        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(self.base_token_symbol + "-" + self.quote_token_symbol, buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        self.assertEqual(float(quantized_amount), float(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 = self.base_token_symbol + "-" + self.quote_token_symbol
        current_price: float = self.market.get_price(symbol, True)
        amount: float = 0.01
        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(float(quantized_amount), float(buy_order_opened_event.amount))
        self.assertEqual(self.base_token_symbol + "-" + self.quote_token_symbol, buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)

        # Reset the logs
        self.market_logger.clear()

        current_price: float = self.market.get_price(symbol, False)
        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(float(quantized_amount), float(sell_order_opened_event.amount))
        self.assertEqual(self.base_token_symbol + "-" + self.quote_token_symbol, sell_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type)

        [cancellation_results, order_cancelled_event] = self.run_parallel(self.market.cancel_all(60 * 5), 
                                                                          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):
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        current_price: float = self.market.get_price(symbol, True)
        amount: float = 0.03
        expires = int(time.time() + 60) # expires in 1 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(self.base_token_symbol + "-" + self.quote_token_symbol, 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(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):
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)
        order_id = self.market.buy(self.base_token_symbol + "-" + self.quote_token_symbol, 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_symbol, order_completed_event.base_asset)
        self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_batch_market_buy(self):
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        amount: float = 0.02
        current_price: float = self.market.get_price(symbol, False)
        expires = int(time.time() + 60 * 5)
        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))

        amount: float = 0.04
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)
        order_id = self.market.buy(self.base_token_symbol + "-" + self.quote_token_symbol, amount, OrderType.MARKET)

        [order_completed_event, 
         sell_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_symbol, order_completed_event.base_asset)
        self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset)

        self.market_logger.clear()

    def test_market_sell(self):
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        amount: float = 0.01
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)
        order_id = self.market.sell(symbol, 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_symbol, order_completed_event.base_asset)
        self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset)
        self.market_logger.clear()

    def test_batch_market_sell(self):
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        amount: float = 0.02
        current_price: float = self.market.get_price(symbol, True)
        expires = int(time.time() + 60 * 5)
        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))

        amount: float = 0.05
        quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)
        order_id = self.market.sell(self.base_token_symbol + "-" + self.quote_token_symbol, amount, OrderType.MARKET)

        [order_completed_event, 
         buy_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_symbol, order_completed_event.base_asset)
        self.assertEqual(self.quote_token_symbol, 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_z_orders_saving_and_restoration(self):
        self.market.reset_state()

        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol
        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: 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.05 / bid_price
            quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount)

            expires = int(time.time() + 60 * 5)
            order_id = self.market.buy(symbol, float(quantized_amount), OrderType.LIMIT, float(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,
                symbols=[conf.test_bamboo_relay_base_token_symbol + "-" + conf.test_bamboo_relay_quote_token_symbol],
                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.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 = self.base_token_symbol + "-" + self.quote_token_symbol
        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: float = self.market.get_price(symbol, True)
            amount: float = 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 = 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)
示例#5
0
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: TradeFee = 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: 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)
        sell_trade_fee: TradeFee = 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: 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["kucoin_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["kucoin_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["kucoin_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_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))
示例#6
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_CANCELLED.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.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):
        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_CANCELLED.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))
示例#7
0
class BittrexMarketUnitTest(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: BittrexMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BittrexMarket = BittrexMarket(
            bittrex_api_key=conf.bittrex_api_key,
            bittrex_secret_key=conf.bittrex_secret_key,
            trading_pairs=["LTC-ETH", "XRP-ETH"])
        print("Initializing Bittrex 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__, "../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("LTC", "ETH",
                                                  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("LTC", "ETH",
                                                   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)
        trading_pair = "LTC-ETH"
        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.005') * 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("LTC", 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.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 = "LTC-ETH"
        amount: Decimal = Decimal('0.02')
        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.005') * 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("LTC", 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.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_market_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        trading_pair = "LTC-ETH"
        amount: Decimal = Decimal('0.02')
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id = self.market.buy(trading_pair, 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: 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("LTC", 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.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):
        trading_pair = "LTC-ETH"
        self.assertGreater(self.market.get_balance("LTC"), 0.01)
        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, 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(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("LTC", 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.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 = "XRP-ETH"

        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal('0.1') / current_bid_price

        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.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))
        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 = "XRP-ETH"
        bid_price: Decimal = self.market.get_price(trading_pair,
                                                   True) * Decimal('0.5')
        ask_price: Decimal = self.market.get_price(trading_pair,
                                                   False) * Decimal('2')
        bid_amount: Decimal = Decimal('0.01') / bid_price
        ask_amount: Decimal = Decimal(
            '3.64495247')  # Min. trade size in XRP-ETH as of 30 Sep 2019
        quantized_bid_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, bid_amount)
        quantized_ask_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, ask_amount)

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

        self.market.buy(trading_pair, quantized_bid_amount, OrderType.LIMIT,
                        quantize_bid_price)
        self.market.sell(trading_pair, quantized_ask_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)
        trading_pair = "LTC-ETH"
        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 = Decimal('0.7') * 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()

    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 Bittrex 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"
        trading_pair: str = "LTC-ETH"
        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)

            # 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=conf.bittrex_api_key,
                bittrex_secret_key=conf.bittrex_secret_key,
                trading_pairs=["LTC-ETH", "XRP-ETH"])
            for event_tag in self.events:
                self.market.add_listener(event_tag, self.market_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [self.market], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.clock.add_iterator(self.market)
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            self.market.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(self.market.limit_orders))
            self.assertEqual(1, len(self.market.tracking_states))

            # Cancel the order and verify that the change is saved.
            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 = "LTC-ETH"
        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.04')
            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 ETH to the exchange, and watch for completion event.
            amount = Decimal(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)
示例#8
0
class HuobiMarketUnitTest(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: HuobiMarket
    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, ["/v1/common/timestamp", "/v1/common/symbols",
                                                        "/market/tickers", "/market/depth"])
            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 = FixtureHuobi.GET_ACCOUNTS["data"][0]["id"]
            cls.web_app.update_response("get", API_BASE_URL, "/v1/account/accounts", FixtureHuobi.GET_ACCOUNTS)
            cls.web_app.update_response("get", API_BASE_URL, f"/v1/account/accounts/{mock_account_id}/balance",
                                        FixtureHuobi.GET_BALANCES)
            cls._t_nonce_patcher = unittest.mock.patch("hummingbot.market.huobi.huobi_market.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: HuobiMarket = HuobiMarket(
            API_KEY,
            API_SECRET,
            trading_pairs=["ethusdt"]
        )
        # Need 2nd instance of market to prevent events mixing up across tests
        cls.market_2: HuobiMarket = HuobiMarket(
            API_KEY,
            API_SECRET,
            trading_pairs=["ethusdt"]
        )
        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__, "../huobi_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: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.BUY, 1, 10)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.MARKET, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)
        sell_trade_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, 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["huobi_taker_fee"].value = None
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["huobi_taker_fee"].value = Decimal('0.1')
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent)
        fee_overrides_config_map["huobi_maker_fee"].value = None
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent)
        fee_overrides_config_map["huobi_maker_fee"].value = Decimal('0.5')
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, 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 API_MOCK_ENABLED:
            exch_order_id = f"HUOBI_{EXCHANGE_ORDER_ID}"
            EXCHANGE_ORDER_ID += 1
            self._t_nonce_mock.return_value = nonce
            resp = FixtureHuobi.ORDER_PLACE.copy()
            resp["data"] = 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, "/v1/order/orders/place", resp,
                                         params={"client-order-id": order_id})
        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 API_MOCK_ENABLED:
            resp = get_resp.copy()
            resp["data"]["id"] = exch_order_id
            resp["data"]["client-order-id"] = order_id
            self.web_app.update_response("get", API_BASE_URL, f"/v1/order/orders/{exch_order_id}", resp)
        return order_id, exch_order_id

    def cancel_order(self, trading_pair, order_id, exchange_order_id, get_resp):
        global EXCHANGE_ORDER_ID
        if API_MOCK_ENABLED:
            resp = FixtureHuobi.ORDER_PLACE.copy()
            resp["data"] = exchange_order_id
            self.web_app.update_response("post", API_BASE_URL, f"/v1/order/orders/{exchange_order_id}/submitcancel",
                                         resp)
        self.market.cancel(trading_pair, order_id)
        if API_MOCK_ENABLED:
            resp = get_resp.copy()
            resp["data"]["id"] = exchange_order_id
            resp["data"]["client-order-id"] = order_id
            self.web_app.update_response("get", API_BASE_URL, f"/v1/order/orders/{exchange_order_id}", resp)

    def test_limit_buy(self):
        trading_pair = "ethusdt"

        amount: Decimal = Decimal("0.06")
        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.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price,
                                       10001, FixtureHuobi.ORDER_GET_LIMIT_BUY_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.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()

    def test_limit_sell(self):
        trading_pair = "ethusdt"
        amount: Decimal = Decimal("0.06")
        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.place_order(False, trading_pair, amount, OrderType.LIMIT, quantize_ask_price,
                                       10001, FixtureHuobi.ORDER_GET_LIMIT_SELL_FILLED)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = 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, 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.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]))
        # Reset the logs
        self.market_logger.clear()

    def test_market_buy(self):
        trading_pair = "ethusdt"
        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.MARKET, 0, 10001,
                                       FixtureHuobi.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.MARKET 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.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_market_sell(self):
        trading_pair = "ethusdt"
        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.MARKET, 0, 10001,
                                       FixtureHuobi.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.MARKET 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 = "ethusdt"

        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,
                                                   quantize_bid_price, 10001, FixtureHuobi.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, FixtureHuobi.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 = "ethusdt"

        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, quantize_bid_price,
                                             1001, FixtureHuobi.ORDER_GET_LIMIT_BUY_UNFILLED, self.market_2)
        _, exch_order_id2 = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price,
                                             1002, FixtureHuobi.ORDER_GET_LIMIT_SELL_UNFILLED, self.market_2)
        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            resp = FixtureHuobi.ORDERS_BATCH_CANCELLED.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)

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ethusdt"
        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,
                                                       quantize_bid_price, 10001,
                                                       FixtureHuobi.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: HuobiMarket = HuobiMarket(
                huobi_api_key=API_KEY,
                huobi_secret_key=API_SECRET,
                trading_pairs=["ethusdt", "btcusdt"]
            )
            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, FixtureHuobi.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 = "ethusdt"
        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.06")
            order_id, _ = self.place_order(True, trading_pair, amount, OrderType.MARKET, 0, 10001,
                                           FixtureHuobi.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.
            amount = buy_order_completed_event.base_asset_amount
            order_id, _ = self.place_order(False, trading_pair, amount, OrderType.MARKET, 0, 10002,
                                           FixtureHuobi.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)
class EterbaseMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure
    ]

    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=["ETH-EUR"])
        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_MAKER,
                                                  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.LIMIT,
                                                   TradeType.BUY, 1)
        self.assertGreaterEqual(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_limit_maker_rejections(self):
        trading_pair = "ETH-EUR"

        # 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):
        trading_pair = "ETH-EUR"
        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 = self.market.buy(trading_pair, quantized_amount,
                                   OrderType.LIMIT_MAKER, 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)

        order_id_2 = self.market.sell(trading_pair, quantized_amount,
                                      OrderType.LIMIT_MAKER,
                                      quantize_ask_price)
        [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))
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        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.2"))
        trading_pair = "ETH-EUR"
        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.LIMIT, 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.LIMIT 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_limit_taker_sell(self):
        trading_pair = "ETH-EUR"
        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.market.sell(trading_pair, amount, OrderType.LIMIT,
                                    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.price * t.amount 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()

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

        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_MAKER,
                                          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 = "ETH-EUR"
        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_MAKER,
                        quantize_bid_price)
        self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT_MAKER,
                         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 = "ETH-EUR"
        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-EUR"
        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_MAKER,
                                       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", "ETH-EUR"])
            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 = "ETH-EUR"

        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.LIMIT,
                                       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.
            price: Decimal = self.market.get_price(trading_pair, False)
            amount = buy_order_completed_event.base_asset_amount
            order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT,
                                        price)
            [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_pair_convesion(self):
        for pair in self.market.trading_rules:
            exchange_pair = convert_to_exchange_trading_pair(pair)
            self.assertTrue(exchange_pair in self.market.order_books)
示例#10
0
class DDEXMarketUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated
    ]

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

    wallet: Web3Wallet
    market: DDEXMarket
    market_logger: EventLogger
    wallet_logger: EventLogger

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.wallet = Web3Wallet(private_key=conf.web3_test_private_key_ddex,
                                backend_urls=conf.test_ddex_web3_provider_list,
                                erc20_token_addresses=[
                                    conf.test_ddex_erc20_token_address_1,
                                    conf.test_ddex_erc20_token_address_2
                                ],
                                chain=EthereumChain.MAIN_NET)
        cls.market: DDEXMarket = DDEXMarket(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_ddex_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            symbols=["HOT-WETH"])
        print("Initializing DDEX 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):
        weth_trade_fee: TradeFee = self.market.get_fee("ZRX", "WETH",
                                                       OrderType.LIMIT,
                                                       TradeType.BUY, 10000, 1)
        self.assertGreater(weth_trade_fee.percent, 0)
        self.assertEqual(len(weth_trade_fee.flat_fees), 1)
        self.assertEqual(weth_trade_fee.flat_fees[0][0], "WETH")
        dai_trade_fee: TradeFee = self.market.get_fee("WETH", "DAI",
                                                      OrderType.MARKET,
                                                      TradeType.BUY, 10000)
        self.assertGreater(dai_trade_fee.percent, 0)
        self.assertEqual(len(dai_trade_fee.flat_fees), 1)
        self.assertEqual(dai_trade_fee.flat_fees[0][0], "DAI")

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

    def test_list_orders(self):
        [orders] = self.run_parallel(self.market.list_orders())
        self.assertGreaterEqual(len(orders), 0)

    def test_list_locked_balances(self):
        [locked_balances
         ] = self.run_parallel(self.market.list_locked_balances())
        self.assertGreaterEqual(len(locked_balances), 0)

    @unittest.skipUnless(
        any("test_bad_orders_are_not_tracked" in arg for arg in sys.argv),
        "bad_orders_are_not_tracked test requires manual action.")
    def test_bad_orders_are_not_tracked(self):
        # Should fail due to insufficient balance
        order_id = self.market.buy("WETH-DAI", 10000, OrderType.LIMIT, 1)
        self.assertEqual(self.market.in_flight_orders.get(order_id), None)

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

        # Intentionally setting invalid price to prevent getting filled
        client_order_id = self.market.buy(symbol, amount, OrderType.LIMIT,
                                          bid_price * 0.7)
        self.market.cancel(symbol, client_order_id)
        self.run_parallel(asyncio.sleep(5))
        self.assertEqual(self.market.in_flight_orders.get(client_order_id),
                         None)

    def test_place_limit_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("WETH"), 0.01)

        # Try to buy 2000 HOT from the exchange, and watch for completion event.
        symbol = "HOT-WETH"
        bid_price: float = self.market.get_price(symbol, True)
        amount: float = 2000
        buy_order_id: str = self.market.buy(symbol, amount, OrderType.LIMIT,
                                            bid_price * 0.7)
        self.run_parallel(asyncio.sleep(3))
        exchange_order_id: str = self.market.in_flight_orders.get(
            buy_order_id).exchange_order_id
        buy_order = self.run_parallel(self.market.get_order(exchange_order_id))
        self.assertEqual(buy_order[0].get('id'), exchange_order_id)
        self.market.cancel(symbol, buy_order_id)

        # Try to sell back the same amount of HOT to the exchange, and watch for completion 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)
        self.run_parallel(asyncio.sleep(3))
        exchange_order_id: str = self.market.in_flight_orders.get(
            sell_order_id).exchange_order_id
        sell_order = self.run_parallel(
            self.market.get_order(exchange_order_id))
        self.assertEqual(sell_order[0].get('id'), exchange_order_id)
        self.market.cancel(symbol, sell_order_id)

    @unittest.skipUnless(
        any("test_limit_buy_and_sell_get_matched" in arg for arg in sys.argv),
        "test_limit_buy_and_sell_get_matched test requires manual action.")
    def test_limit_buy_and_sell_get_matched(self):
        self.assertGreater(self.market.get_balance("WETH"), 0.01)

        # Try to buy 0.01 WETH worth of HOT from the exchange, and watch for completion event.
        current_price: float = self.market.get_price("HOT-WETH", True)
        amount: float = 2000
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "HOT-WETH", amount)
        order_id = self.market.buy("HOT-WETH", amount, OrderType.LIMIT,
                                   current_price)
        [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.LIMIT for evt in order_filled_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         order_completed_event.base_asset_amount)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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 HOT to the exchange, and watch for completion event.
        current_price: float = self.market.get_price("HOT-WETH", False)
        amount = float(order_completed_event.base_asset_amount)
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.market.sell("HOT-WETH", amount, OrderType.LIMIT,
                                    current_price)
        [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.LIMIT for evt in order_filled_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         order_completed_event.base_asset_amount)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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_market_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("WETH"), 0.01)

        amount: float = 2000.0  # Min order size is 1000 HOT
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "HOT-WETH", amount)

        order_id = self.market.buy("HOT-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)

        # This is because some of the tokens are deducted in the trading fees.
        self.assertTrue(
            float(quantized_amount) > order_completed_event.base_asset_amount >
            float(quantized_amount) * 0.9)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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 HOT to the exchange, and watch for completion event.
        amount = float(order_completed_event.base_asset_amount)
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.market.sell("HOT-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),
                         order_completed_event.base_asset_amount)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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
            ]))

    @unittest.skipUnless(any("test_wrap_eth" in arg for arg in sys.argv),
                         "Wrap Eth test requires manual action.")
    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)

    @unittest.skipUnless(any("test_unwrap_eth" in arg for arg in sys.argv),
                         "Unwrap Eth test requires manual action.")
    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_cancel_all_happy_case(self):
        symbol = "HOT-WETH"
        bid_price: float = self.market.get_price(symbol, True)
        ask_price: float = self.market.get_price(symbol, False)
        amount = 2000

        self.assertGreater(self.market.get_balance("WETH"), 0.02)
        self.assertGreater(self.market.get_balance("HOT"), amount)

        # Intentionally setting invalid price to prevent getting filled
        self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7)
        self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(10))
        print(cancellation_results)
        self.assertGreater(len(cancellation_results), 0)
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_cancel_all_failure_case(self):
        symbol = "HOT-WETH"
        bid_price: float = self.market.get_price(symbol, True)
        ask_price: float = self.market.get_price(symbol, False)
        # order submission should fail due to insufficient balance
        amount = 200000

        self.assertLess(self.market.get_balance("WETH"), 100)
        self.assertLess(self.market.get_balance("HOT"), amount)

        self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7)
        self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(10))
        print(cancellation_results)
        self.assertGreater(len(cancellation_results), 0)
        for cr in cancellation_results:
            self.assertEqual(cr.success, False)
示例#11
0
class BinanceMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
    ]

    market: BinanceMarket
    market_logger: EventLogger

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BinanceMarket = BinanceMarket(
            MAINNET_RPC_URL,
            conf.binance_api_key,
            conf.binance_api_secret,
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType
            .EXCHANGE_API,
            symbols=["ZRXETH", "LOOMETH", "IOSTETH"])
        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)
        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)
            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, TradeType.BUY, 1, 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.MARKET, TradeType.BUY, 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, 1, 4000)
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

    def test_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)

        # Try to buy 0.02 ETH worth of ZRX from the exchange, and watch for completion event.
        current_price: float = self.market.get_price("ZRXETH", True)
        amount: float = 0.02 / current_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRXETH", amount)
        order_id = self.market.buy("ZRXETH", amount)
        [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.MARKET 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("ZRX", order_completed_event.base_asset)
        self.assertEqual("ETH", 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.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.
        amount = float(order_completed_event.base_asset_amount)
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.market.sell("ZRXETH", amount)
        [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.MARKET 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("ZRX", order_completed_event.base_asset)
        self.assertEqual("ETH", 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.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_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), 0.1)

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

        amount: float = 0.02 / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRXETH", amount)

        order_id = self.market.buy("ZRXETH", 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.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("ZRX", order_completed_event.base_asset)
        self.assertEqual("ETH", 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.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 put limit sell order for 0.02 ETH worth of ZRX, and watch for completion event.
        current_ask_price: float = self.market.get_price("ZRXETH", False)
        ask_price: float = current_ask_price - 0.05 * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            "ZRXETH", ask_price)

        amount = float(order_completed_event.base_asset_amount)
        quantized_amount = order_completed_event.base_asset_amount

        order_id = self.market.sell("ZRXETH", 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.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("ZRX", order_completed_event.base_asset)
        self.assertEqual("ETH", 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.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
            ]))

    @unittest.skipUnless(any("test_deposit_eth" in arg for arg in sys.argv),
                         "Deposit test requires manual action.")
    def test_deposit_eth(self):
        with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd:
            zrx_abi: str = fd.read()
        local_wallet: MockWallet = MockWallet(
            conf.web3_test_private_key_a,
            MAINNET_RPC_URL,
            {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi},
            chain_id=1)

        # Ensure the local wallet has enough balance for deposit testing.
        self.assertGreaterEqual(local_wallet.get_balance("ETH"), 0.02)

        # Deposit ETH to Binance, and wait.
        tracking_id: str = self.market.deposit(local_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(local_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):
        with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd:
            zrx_abi: str = fd.read()
        local_wallet: MockWallet = MockWallet(
            conf.web3_test_private_key_a,
            MAINNET_RPC_URL,
            {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi},
            chain_id=1)

        # Ensure the local wallet has enough balance for deposit testing.
        self.assertGreaterEqual(local_wallet.get_balance("ZRX"), 1)

        # Deposit ZRX to Binance, and wait.
        tracking_id: str = self.market.deposit(local_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(local_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):
        with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd:
            zrx_abi: str = fd.read()
        local_wallet: MockWallet = MockWallet(
            conf.web3_test_private_key_a,
            MAINNET_RPC_URL,
            {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi},
            chain_id=1)

        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("ZRX"), 10)

        # Withdraw ZRX from Binance to test wallet.
        self.market.withdraw(local_wallet.address, "ZRX", 10)
        [withdraw_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketWithdrawAssetEvent))
        withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event
        print(withdraw_asset_event)
        self.assertEqual(local_wallet.address, withdraw_asset_event.to_address)
        self.assertEqual("ZRX", withdraw_asset_event.asset_name)
        self.assertEqual(10, withdraw_asset_event.amount)
        self.assertGreater(withdraw_asset_event.fee_amount, 0)

    def test_cancel_all(self):
        symbol = "LOOMETH"
        bid_price: float = self.market.get_price(symbol, True)
        ask_price: float = self.market.get_price(symbol, False)
        amount: float = 0.02 / 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)

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

        # Make sure there's enough balance to make the limit orders.
        self.assertGreater(self.market.get_balance("ETH"), 0.1)
        self.assertGreater(self.market.get_balance("IOST"), 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: float = mid_price * 0.3333192292111341
        ask_price: float = mid_price * 3.4392431474884933

        # This is needed to get around the min quote amount limit.
        bid_amount: float = 0.02 / bid_price

        # Test bid order
        bid_order_id: str = self.market.buy(symbol, bid_amount,
                                            OrderType.LIMIT, bid_price)

        # 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=symbol, origClientOrderId=bid_order_id)
        quantized_bid_price: Decimal = self.market.quantize_order_price(
            symbol, bid_price)
        bid_size_quantum: Decimal = self.market.get_order_size_quantum(
            symbol, bid_amount)
        self.assertEqual(quantized_bid_price, Decimal(order_data["price"]))
        self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0)

        # Test ask order
        ask_order_id: str = self.market.sell(symbol, amount, OrderType.LIMIT,
                                             ask_price)

        # 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=symbol,
                                              origClientOrderId=ask_order_id)
        quantized_ask_price: Decimal = self.market.quantize_order_price(
            symbol, ask_price)
        quantized_ask_size: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        self.assertEqual(quantized_ask_price, Decimal(order_data["price"]))
        self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"]))

        # Cancel all the orders
        [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):
        BinanceTime.get_instance().SERVER_TIME_OFFSET_CHECK_INTERVAL = 3.0
        self.run_parallel(asyncio.sleep(60))
        with patch("hummingbot.market.binance.binance_market.time"
                   ) as market_time:

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

            market_time.time = delayed_time
            self.run_parallel(asyncio.sleep(5.0))
            time_offset = BinanceTime.get_instance().time_offset_ms
            print("offest", time_offset)
            # check if it is less than 5% off
            self.assertTrue(time_offset > 0)
            self.assertTrue(abs(time_offset - 30.0 * 1e3) < 1.5 * 1e3)
示例#12
0
class LiquidMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled
    ]

    market: LiquidMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @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(API_HOST,
                                         ["/products", "/currencies"])
            cls.web_app.start()
            cls.ev_loop.run_until_complete(cls.web_app.wait_til_started())
            cls._patcher = mock.patch("aiohttp.client.URL")
            cls._url_mock = cls._patcher.start()
            cls._url_mock.side_effect = cls.web_app.reroute_local
            cls.web_app.update_response("get", API_HOST, "/fiat_accounts",
                                        FixtureLiquid.FIAT_ACCOUNTS)
            cls.web_app.update_response("get", API_HOST, "/crypto_accounts",
                                        FixtureLiquid.CRYPTO_ACCOUNTS)
            cls.web_app.update_response("get", API_HOST, "/orders",
                                        FixtureLiquid.ORDERS_GET)
            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.market.liquid.liquid_market.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: LiquidMarket = LiquidMarket(
            API_KEY,
            API_SECRET,
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType
            .EXCHANGE_API,
            trading_pairs=['CEL-ETH'])
        # cls.ev_loop.run_until_complete(cls.market._update_balances())
        print("Initializing Liquid market... this will take about a minute.")
        cls.clock.add_iterator(cls.market)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

    @classmethod
    def tearDownClass(cls) -> None:
        if API_MOCK_ENABLED:
            cls.web_app.stop()
            cls._patcher.stop()
            cls._t_nonce_patcher.stop()
        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__, "../liquid_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", "USD", OrderType.LIMIT, 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", "USD", OrderType.MARKET, 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", "USD",
                                                       OrderType.LIMIT,
                                                       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["liquid_taker_fee"].value = None
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.MARKET,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent)
        fee_overrides_config_map["liquid_taker_fee"].value = Decimal('0.002')
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.MARKET,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent)
        fee_overrides_config_map["liquid_maker_fee"].value = None
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, Decimal(1),
                                                  Decimal('0.1'))
        self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent)
        fee_overrides_config_map["liquid_maker_fee"].value = Decimal('0.005')
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  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, order_resp, get_resp):
        order_id, exchange_id = None, None
        if API_MOCK_ENABLED:
            side = 'buy' if is_buy else 'sell'
            self._t_nonce_mock.return_value = nonce
            order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}"
            resp = order_resp.copy()
            resp["client_order_id"] = order_id
            exchange_id = resp["id"]
            self.web_app.update_response("post", API_HOST, "/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["models"][0]["client_order_id"] = order_id
            self.web_app.update_response("get", API_HOST, "/orders", resp)
        return order_id, exchange_id

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

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

        order_id, _ = self.place_order(True, "CEL-ETH", amount,
                                       OrderType.MARKET, current_price, 10001,
                                       FixtureLiquid.ORDER_BUY,
                                       FixtureLiquid.ORDERS_GET_AFTER_BUY)
        [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)
        self.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("CEL", 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 CEL to the exchange, and watch for completion event.
        amount = order_completed_event.base_asset_amount
        quantized_amount = order_completed_event.base_asset_amount
        order_id, _ = self.place_order(False, "CEL-ETH", amount,
                                       OrderType.MARKET, current_price, 10002,
                                       FixtureLiquid.ORDER_SELL,
                                       FixtureLiquid.ORDERS_GET_AFTER_SELL)
        [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.MARKET 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("CEL", 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_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.001"))

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

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

        order_id, _ = self.place_order(
            True, "CEL-ETH", quantized_amount, OrderType.LIMIT,
            quantize_bid_price, 10001, FixtureLiquid.ORDER_BUY_LIMIT,
            FixtureLiquid.ORDERS_GET_AFTER_LIMIT_BUY)
        [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("CEL", 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 put limit sell order for 0.05 ETH worth of CEL, and watch for completion event.
        current_ask_price: Decimal = self.market.get_price("CEL-ETH", False)
        ask_price: Decimal = current_ask_price - Decimal(
            "0.05") * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            "CEL-ETH", ask_price)

        quantized_amount = order_completed_event.base_asset_amount

        order_id, _ = self.place_order(
            False, "CEL-ETH", quantized_amount, OrderType.LIMIT,
            quantize_ask_price, 10002, FixtureLiquid.ORDER_SELL_LIMIT,
            FixtureLiquid.ORDERS_GET_AFTER_SELL_LIMIT)

        [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("CEL", 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_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)
        self.assertGreater(len(deposit_info.extras), 0)
        self.assertTrue("currency_type" in deposit_info.extras.get('extras'))
        self.assertEqual("ETH",
                         deposit_info.extras.get('extras').get('currency'))

    @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv),
                         "Withdraw test requires manual action.")
    def test_withdraw(self):
        # CEL_ABI contract file can be found in
        # https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code
        with open(realpath(join(__file__, "../../../data/CELABI.json"))) as fd:
            zrx_abi: str = fd.read()

        local_wallet: MockWallet = MockWallet(
            conf.web3_test_private_key_a,
            conf.test_web3_provider_list[0],
            {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi},
            chain_id=1)

        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("CEL"), Decimal('10'))

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

    def test_cancel_all(self):
        trading_pair = "CEL-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_exchange_id = self.place_order(
            True, trading_pair, quantized_amount, OrderType.LIMIT,
            quantize_bid_price, 10001, FixtureLiquid.ORDER_BUY_CANCEL_ALL,
            FixtureLiquid.ORDERS_GET_AFTER_BUY)
        _, sell_exchange_id = self.place_order(
            False, trading_pair, quantized_amount, OrderType.LIMIT,
            quantize_ask_price, 10002, FixtureLiquid.ORDER_SELL_CANCEL_ALL,
            FixtureLiquid.ORDERS_GET_AFTER_SELL)

        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            order_cancel_resp = FixtureLiquid.ORDER_CANCEL_ALL_1
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel",
                order_cancel_resp)
            order_cancel_resp = FixtureLiquid.ORDER_CANCEL_ALL_2
            self.web_app.update_response(
                "put", API_HOST, f"/orders/{str(sell_exchange_id)}/cancel",
                order_cancel_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"
        sql: SQLConnectionManager = SQLConnectionManager(
            SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        order_id: Optional[str] = None
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market],
                                                    config_path, strategy_name)
        recorder.start()

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

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

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

            order_id, buy_exchange_id = self.place_order(
                True, "CEL-ETH", quantized_amount, OrderType.LIMIT,
                quantize_bid_price, 10001, FixtureLiquid.ORDER_SAVE_RESTORE,
                FixtureLiquid.ORDERS_GET_AFTER_BUY)
            [order_created_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event
            self.assertEqual(order_id, order_created_event.order_id)

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

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

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

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.events:
                self.market.remove_listener(event_tag, self.market_logger)

            self.market: LiquidMarket = LiquidMarket(
                API_KEY,
                API_SECRET,
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                user_stream_tracker_data_source_type=
                UserStreamTrackerDataSourceType.EXCHANGE_API,
                trading_pairs=['ETH-USD', 'CEL-ETH'])

            for event_tag in self.events:
                self.market.add_listener(event_tag, self.market_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [self.market], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.clock.add_iterator(self.market)
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            self.market.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(self.market.limit_orders))
            self.assertEqual(1, len(self.market.tracking_states))

            # Cancel the order and verify that the change is saved.
            if API_MOCK_ENABLED:
                order_cancel_resp = FixtureLiquid.ORDER_CANCEL_SAVE_RESTORE.copy(
                )
                self.web_app.update_response(
                    "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel",
                    order_cancel_resp)
            self.market.cancel("CEL-ETH", order_id)
            self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
            order_id = None
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0, len(self.market.tracking_states))
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.assertEqual(0, len(saved_market_states.saved_state))
        finally:
            if order_id is not None:
                self.market.cancel("CEL-ETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

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

    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 CEL from the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price("CEL-ETH", True)
            amount: Decimal = 1
            order_id, _ = self.place_order(
                True, "CEL-ETH", amount, OrderType.MARKET, current_price,
                10001, FixtureLiquid.ORDER_BUY_LIMIT,
                FixtureLiquid.ORDERS_GET_AFTER_LIMIT_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 CEL to the exchange, and watch for completion event.
            amount = buy_order_completed_event.base_asset_amount
            order_id, _ = self.place_order(
                False, "CEL-ETH", amount, OrderType.MARKET, current_price,
                10002, FixtureLiquid.ORDER_SELL_LIMIT,
                FixtureLiquid.ORDERS_GET_AFTER_SELL_LIMIT)
            [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("CEL-ETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
示例#13
0
class BinanceExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure
    ]

    market: BinanceExchange
    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 = MockWebServer.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 = MockWebServer.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'})
            cls.web_app.update_response("get", cls.base_api_url, "/api/v3/myTrades",
                                        {}, params={'symbol': 'ZRXETH'})
            cls.web_app.update_response("get", cls.base_api_url, "/api/v3/myTrades",
                                        {}, params={'symbol': 'LINKETH'})
            ws_base_url = "wss://stream.binance.com:9443/ws"
            cls._ws_user_url = f"{ws_base_url}/{FixtureBinance.LISTEN_KEY['listenKey']}"
            MockWebSocketServerFactory.start_new_server(cls._ws_user_url)
            MockWebSocketServerFactory.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 = MockWebSocketServerFactory.reroute_ws_connect

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.binance.binance_exchange.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.current_nonce = 1000000000000000
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BinanceExchange = BinanceExchange(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()
        self.market._current_trade_fills = set()
        self.market._exchange_order_ids = dict()
        self.ev_loop.run_until_complete(self.wait_til_ready())
        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))

    @classmethod
    def get_current_nonce(cls):
        cls.current_nonce += 1
        return cls.current_nonce

    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, self.get_current_nonce(), FixtureBinance.BUY_MARKET_ORDER,
                                    FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2)
        self.market.add_exchange_order_ids_from_market_recorder({str(FixtureBinance.BUY_MARKET_ORDER['orderId']): "buy-LINKETH-1580093594011279"})
        [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)
        self.market.add_exchange_order_ids_from_market_recorder({str(FixtureBinance.SELL_MARKET_ORDER['orderId']): "sell-LINKETH-1580194659898896"})
        [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, self.get_current_nonce(),
                                    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, self.get_current_nonce(),
                                    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)

        buy_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT_MAKER,
                                  price, self.get_current_nonce(),
                                  FixtureBinance.OPEN_BUY_ORDER)
        [buy_order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))
        buy_order_created_event: BuyOrderCreatedEvent = buy_order_created_event
        self.assertEqual(buy_id, buy_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)

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

        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 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:
            exchange_order_id = str(resp['orderId'])
            data = self.fixture(fixture_ws_1, c=order_id, i=exchange_order_id)
            MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.1)
            data = self.fixture(fixture_ws_2, c=order_id, i=exchange_order_id)
            MockWebSocketServerFactory.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, self.get_current_nonce(),
                                  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, self.get_current_nonce(),
                                   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, self.get_current_nonce(), "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, self.get_current_nonce(), "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, self.get_current_nonce(), "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.__class__.market: BinanceExchange = BinanceExchange(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.ev_loop.run_until_complete(self.wait_til_ready())

            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)
        buy_id: Optional[str] = None
        sell_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
            buy_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, self.get_current_nonce(),
                                      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
            sell_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT, ask_price, self.get_current_nonce(),
                                       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)

            buy_id = sell_id = None

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

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

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

        try:
            # Perform the same order twice which should produce the same exchange_order_id
            # 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
            buy_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, self.get_current_nonce(),
                                      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))

            self.market_logger.clear()

            # Simulate that order is still in in_flight_orders
            order_json = {"client_order_id": buy_id,
                          "exchange_order_id": str(FixtureBinance.WS_AFTER_BUY_2['t']),
                          "trading_pair": "LINK-ETH",
                          "order_type": "MARKET",
                          "trade_type": "BUY",
                          "price": bid_price,
                          "amount": amount,
                          "last_state": "NEW",
                          "executed_amount_base": "0",
                          "executed_amount_quote": "0",
                          "fee_asset": "LINK",
                          "fee_paid": "0.0"}
            self.market.restore_tracking_states({buy_id: order_json})
            self.market.in_flight_orders.get(buy_id).trade_id_set.add(str(FixtureBinance.WS_AFTER_BUY_2['t']))
            # Simulate incoming responses as if buy_id is executed again
            data = self.fixture(FixtureBinance.WS_AFTER_BUY_2, c=buy_id)
            MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.11)
            # Will wait, but no order filled event should be triggered because order is ignored
            self.run_parallel(asyncio.sleep(1))
            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path)
            buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"]
            exchange_trade_id = FixtureBinance.WS_AFTER_BUY_2['t']
            self.assertEqual(len([bf for bf in buy_fills if int(bf.exchange_trade_id) == exchange_trade_id]), 1)

            buy_id = None

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

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

    def test_history_reconciliation(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path)
        recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name)
        recorder.start()
        try:
            bid_price: Decimal = self.market.get_price("LINK-ETH", True)
            # Will temporarily change binance history request to return trades
            buy_id = "1580204166011219"
            order_id = "123456"
            self._t_nonce_mock.return_value = 1234567890123456
            binance_trades = [{
                'symbol': "LINKETH",
                'id': buy_id,
                'orderId': order_id,
                'orderListId': -1,
                'price': float(bid_price),
                'qty': 1,
                'quoteQty': float(bid_price),
                'commission': 0,
                'commissionAsset': "ETH",
                'time': 1580093596074,
                'isBuyer': True,
                'isMaker': True,
                'isBestMatch': True,
            }]
            self.market.add_exchange_order_ids_from_market_recorder({order_id: "buy-LINKETH-1580093594011279"})
            self.web_app.update_response("get", self.base_api_url, "/api/v3/myTrades",
                                         binance_trades, params={'symbol': 'LINKETH'})
            [market_order_completed] = self.run_parallel(self.market_logger.wait_for(OrderFilledEvent))

            trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path)
            buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"]
            self.assertEqual(len([bf for bf in buy_fills if bf.exchange_trade_id == buy_id]), 1)

            buy_id = None

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

            # Undo change to binance history request
            self.web_app.update_response("get", self.base_api_url, "/api/v3/myTrades",
                                         {}, params={'symbol': 'LINKETH'})

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

    def test_pair_conversion(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)
示例#14
0
class BinanceMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled
    ]

    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/exchangeInfo", "/api/v1/time"])
            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.GET_ACCOUNT)
            cls.web_app.update_response("get", cls.base_api_url,
                                        "/wapi/v3/tradeFee.html",
                                        FixtureBinance.GET_TRADE_FEES)
            cls.web_app.update_response("post", cls.base_api_url,
                                        "/api/v1/userDataStream",
                                        FixtureBinance.GET_LISTEN_KEY)
            cls.web_app.update_response("put", cls.base_api_url,
                                        "/api/v1/userDataStream",
                                        FixtureBinance.GET_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.GET_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.market.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,
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType
            .EXCHANGE_API,
            trading_pairs=["LINKETH", "ZRXETH"])
        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, 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.MARKET, 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)

    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.MARKET,
                                                  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.002')
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.MARKET,
                                                  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,
                                                  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.005')
        maker_fee: TradeFee = self.market.get_fee("LINK", "ETH",
                                                  OrderType.LIMIT,
                                                  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.1"))
        amount: Decimal = 1
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "LINKETH", amount)
        order_id = self.place_order(True, "LINKETH", amount, OrderType.MARKET,
                                    0, 10001, FixtureBinance.ORDER_BUY,
                                    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.MARKET 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.
        amount = order_completed_event.base_asset_amount
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.place_order(False, "LINKETH", amount, OrderType.MARKET,
                                    0, 10002, FixtureBinance.ORDER_SELL,
                                    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.MARKET 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_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))

        # Try to put limit buy order for 1 LINK, and watch for completion event.
        ask_price: Decimal = self.market.get_price("LINKETH",
                                                   False) * Decimal("1.01")
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            "LINKETH", ask_price)

        amount: Decimal = 1
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "LINKETH", amount)

        order_id = self.place_order(True, "LINKETH", quantized_amount,
                                    OrderType.LIMIT, quantize_bid_price, 10001,
                                    FixtureBinance.ORDER_BUY_LIMIT,
                                    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 put limit sell order for 0.02 ETH worth of ZRX, and watch for completion event.
        bid_price: Decimal = self.market.get_price("LINKETH",
                                                   True) * Decimal('0.99')
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            "LINKETH", bid_price)
        quantized_amount = order_completed_event.base_asset_amount

        order_id = self.place_order(False, "LINKETH", quantized_amount,
                                    OrderType.LIMIT, quantize_ask_price, 10002,
                                    FixtureBinance.ORDER_SELL_LIMIT,
                                    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_deposit_info(self):
        if API_MOCK_ENABLED:
            self.web_app.update_response("get", self.base_api_url,
                                         "/wapi/v3/depositAddress.html",
                                         FixtureBinance.GET_DEPOSIT_INFO)
        [deposit_info] = self.run_parallel(self.market.get_deposit_info("BNB"))
        deposit_info: DepositInfo = deposit_info
        self.assertIsInstance(deposit_info, DepositInfo)
        self.assertGreater(len(deposit_info.address), 0)
        self.assertGreater(len(deposit_info.extras), 0)
        self.assertTrue("addressTag" in deposit_info.extras)
        self.assertEqual("BNB", deposit_info.extras["asset"])

    @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv),
                         "Withdraw test requires manual action.")
    def test_withdraw(self):
        # ZRX_ABI contract file can be found in
        # https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code
        with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd:
            zrx_abi: str = fd.read()

        local_wallet: MockWallet = MockWallet(
            conf.web3_test_private_key_a,
            conf.test_web3_provider_list[0],
            {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi},
            chain_id=1)

        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("ZRX"), Decimal('10'))

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

    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, fixture_ws_2):
        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:
            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 = "LINKETH"
        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, "LINKETH", quantized_amount,
                                  OrderType.LIMIT, quantize_bid_price, 10001,
                                  FixtureBinance.ORDER_BUY_NOT_FILLED,
                                  FixtureBinance.WS_AFTER_BUY_1,
                                  FixtureBinance.WS_AFTER_BUY_2)

        sell_id = self.place_order(False, "LINKETH", quantized_amount,
                                   OrderType.LIMIT, quantize_ask_price, 10002,
                                   FixtureBinance.ORDER_SELL_NOT_FILLED,
                                   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 = "LINKETH"
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        ask_price: Decimal = self.market.get_price(trading_pair, False)
        print(f"bid_price: {bid_price} ask_price: {ask_price}")
        for ent in self.market.order_book_bid_entries("LINKETH"):
            print(f"bid: {ent.price} volume: {ent.amount}")
        for ent in self.market.order_book_ask_entries("LINKETH"):
            print(f"ask: {ent.price} volume: {ent.amount}")
        mid_price: Decimal = (bid_price + ask_price) / 2
        amount: Decimal = Decimal("0.02") / mid_price
        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.1"))
        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.3333192292111341")
        ask_price: Decimal = mid_price * Decimal("3.4392431474884933")

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

        if API_MOCK_ENABLED:
            resp = self.order_response(FixtureBinance.ORDER_BUY_PRECISION,
                                       1000001, "buy", "LINKETH")
            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", "LINKETH")
            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.market.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("LINKETH", True)
            bid_price: Decimal = current_bid_price * Decimal("0.8")
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                "LINKETH", bid_price)

            amount: Decimal = 1
            quantized_amount: Decimal = self.market.quantize_order_amount(
                "LINKETH", amount)

            if API_MOCK_ENABLED:
                resp = self.order_response(FixtureBinance.ORDER_BUY_NOT_FILLED,
                                           1000001, "buy", "LINKETH")
                self.web_app.update_response("post", self.base_api_url,
                                             "/api/v3/order", resp)
            order_id = self.market.buy("LINKETH", 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(
                binance_api_key=API_KEY,
                binance_api_secret=API_SECRET,
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                user_stream_tracker_data_source_type=
                UserStreamTrackerDataSourceType.EXCHANGE_API,
                trading_pairs=["LINKETH", "ZRXETH"])
            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("LINKETH", 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("LINKETH", 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"
        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.
            amount: Decimal = 1
            order_id = self.place_order(True, "LINKETH", amount,
                                        OrderType.MARKET, 0, 10001,
                                        FixtureBinance.ORDER_BUY_LIMIT,
                                        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.
            amount = buy_order_completed_event.base_asset_amount
            order_id = self.place_order(False, "LINKETH", amount,
                                        OrderType.MARKET, 0, 10002,
                                        FixtureBinance.ORDER_SELL_LIMIT,
                                        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("LINKETH", order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
示例#15
0
class BinanceMarketUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled
    ]

    market: BinanceMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BinanceMarket = BinanceMarket(
            conf.binance_api_key,
            conf.binance_api_secret,
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType
            .EXCHANGE_API,
            trading_pairs=["ZRXETH", "IOSTETH"])
        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()

    @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, 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.MARKET, 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)

    def test_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))

        # Try to buy 0.02 ETH worth of ZRX from the exchange, and watch for completion event.
        current_price: Decimal = self.market.get_price("ZRXETH", True)
        amount: Decimal = Decimal("0.02") / current_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRXETH", amount)
        order_id = self.market.buy("ZRXETH", amount)
        [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)
        self.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("ZRX", 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.
        amount = order_completed_event.base_asset_amount
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.market.sell("ZRXETH", amount)
        [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.MARKET 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("ZRX", 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_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1"))

        # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for completion event.
        current_bid_price: Decimal = self.market.get_price("ZRXETH", True)
        bid_price: Decimal = current_bid_price + Decimal(
            "0.05") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            "ZRXETH", bid_price)

        amount: Decimal = Decimal("0.02") / bid_price
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "ZRXETH", amount)

        order_id = self.market.buy("ZRXETH", 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.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("ZRX", 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 put limit sell order for 0.02 ETH worth of ZRX, and watch for completion event.
        current_ask_price: Decimal = self.market.get_price("ZRXETH", False)
        ask_price: Decimal = current_ask_price - Decimal(
            "0.05") * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            "ZRXETH", ask_price)

        amount = order_completed_event.base_asset_amount
        quantized_amount = order_completed_event.base_asset_amount

        order_id = self.market.sell("ZRXETH", quantized_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.assertEqual(quantized_amount,
                         order_completed_event.base_asset_amount)
        self.assertEqual("ZRX", 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_deposit_info(self):
        [deposit_info] = self.run_parallel(self.market.get_deposit_info("BNB"))
        deposit_info: DepositInfo = deposit_info
        self.assertIsInstance(deposit_info, DepositInfo)
        self.assertGreater(len(deposit_info.address), 0)
        self.assertGreater(len(deposit_info.extras), 0)
        self.assertTrue("addressTag" in deposit_info.extras)
        self.assertEqual("BNB", deposit_info.extras["asset"])

    @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv),
                         "Withdraw test requires manual action.")
    def test_withdraw(self):
        # ZRX_ABI contract file can be found in
        # https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code
        with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd:
            zrx_abi: str = fd.read()

        local_wallet: MockWallet = MockWallet(
            conf.web3_test_private_key_a,
            conf.test_web3_provider_list[0],
            {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi},
            chain_id=1)

        # Ensure the market account has enough balance for withdraw testing.
        self.assertGreaterEqual(self.market.get_balance("ZRX"), Decimal('10'))

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

    def test_cancel_all(self):
        trading_pair = "ZRXETH"
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        ask_price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal("0.02") / 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)

    def test_order_price_precision(self):
        # As of the day this test was written, the min order size (base) is 1 IOST, the min order size (quote) is
        # 0.01 ETH, and order step size is 1 IOST.
        trading_pair = "IOSTETH"
        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("0.02") / mid_price
        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.1"))
        self.assertGreater(self.market.get_balance("IOST"), 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.3333192292111341")
        ask_price: Decimal = mid_price * Decimal("3.4392431474884933")

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

        # Test bid order
        bid_order_id: str = self.market.buy(trading_pair, Decimal(bid_amount),
                                            OrderType.LIMIT,
                                            Decimal(bid_price))

        # 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
        ask_order_id: str = self.market.sell(trading_pair,
                                             Decimal(amount), OrderType.LIMIT,
                                             Decimal(ask_price))

        # 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
        [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:
            with patch("hummingbot.market.binance.binance_time.time"
                       ) as market_time:

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

                market_time.time = delayed_time
                self.run_parallel(asyncio.sleep(3.0))
                time_offset = BinanceTime.get_instance().time_offset_ms
                # check if it is less than 5% off
                self.assertTrue(time_offset > 10000)
                self.assertTrue(abs(time_offset - 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("ZRXETH", True)
            bid_price: Decimal = current_bid_price * Decimal("0.8")
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                "ZRXETH", bid_price)

            amount: Decimal = Decimal("0.02") / bid_price
            quantized_amount: Decimal = self.market.quantize_order_amount(
                "ZRXETH", amount)

            order_id = self.market.buy("ZRXETH", 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(
                binance_api_key=conf.binance_api_key,
                binance_api_secret=conf.binance_api_secret,
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                user_stream_tracker_data_source_type=
                UserStreamTrackerDataSourceType.EXCHANGE_API,
                trading_pairs=["ZRXETH", "IOSTETH"])
            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("ZRXETH", 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("ZRXETH", 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"
        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.02 ETH worth of ZRX from the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price("ZRXETH", True)
            amount: Decimal = Decimal("0.02") / current_price
            order_id = self.market.buy("ZRXETH", 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("ZRXETH", 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.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("ZRXETH", 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)
class AscendExExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]
    connector: AscendExExchange
    event_logger: EventLogger
    trading_pair = "BTC-USDT"
    base_token, quote_token = trading_pair.split("-")
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):

        cls.ev_loop = asyncio.get_event_loop()

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: AscendExExchange = AscendExExchange(
            ascend_ex_api_key=API_KEY,
            ascend_ex_secret_key=API_SECRET,
            trading_pairs=[cls.trading_pair],
            trading_required=True)
        print(
            "Initializing AscendEx exchange... 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 _place_order(self, is_buy, amount, order_type, price,
                     ex_order_id) -> 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_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 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("0.0002"))
        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)
        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("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.assertGreater(order_completed_event.fee_amount, Decimal(0))
        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.ev_loop.run_until_complete(asyncio.sleep(1))
        self.ev_loop.run_until_complete(self.connector._update_balances())
        self.assertAlmostEqual(
            expected_quote_bal,
            self.connector.get_available_balance(self.quote_token), 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("0.0002"))
        order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2)
        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("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.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.ev_loop.run_until_complete(asyncio.sleep(1))
        self.ev_loop.run_until_complete(self.connector._update_balances())
        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("0.0002"))
        quote_bal = self.connector.get_available_balance(self.quote_token)

        cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER,
                                        price, 1)
        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.ev_loop.run_until_complete(self.connector._update_balances())
        # self.ev_loop.run_until_complete(asyncio.sleep(2))

        self.assertAlmostEqual(
            expected_quote_bal,
            self.connector.get_available_balance(self.quote_token), 1)
        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("0.0002"))

        cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER,
                                        price, 2)
        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)

    # # @TODO: find a way to create "rejected"
    # 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("0.000001"))
    #     cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1)
    #     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("0.000001"))
    #     cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2)
    #     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.9"))
        ask_price = self.connector.quantize_order_price(
            self.trading_pair, ask_price * Decimal("1.1"))
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("0.0002"))

        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_quantized_values(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

        # 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("USDT"), Decimal("10"))

        # 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 = self.connector.quantize_order_price(
            self.trading_pair, mid_price * Decimal("0.9333192292111341"))
        ask_price = self.connector.quantize_order_price(
            self.trading_pair, mid_price * Decimal("1.1492431474884933"))
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("0.000223456"))

        # Test bid order
        cl_order_id_1 = self._place_order(True, amount, OrderType.LIMIT,
                                          bid_price, 1)
        # Wait for the order created event and examine the order made
        self.ev_loop.run_until_complete(
            self.event_logger.wait_for(BuyOrderCreatedEvent))

        # Test ask order
        cl_order_id_2 = self._place_order(False, amount, OrderType.LIMIT,
                                          ask_price, 1)
        # Wait for the order created event and examine and order made
        self.ev_loop.run_until_complete(
            self.event_logger.wait_for(SellOrderCreatedEvent))

        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.0002")
            amount = self.connector.quantize_order_amount(
                self.trading_pair, amount)

            cl_order_id = self._place_order(True, amount,
                                            OrderType.LIMIT_MAKER, price, 1)
            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 = AscendExExchange(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.ev_loop.run_until_complete(self.wait_til_ready(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))
                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("0.0002"))

            order_id = self._place_order(True, amount, OrderType.LIMIT, price,
                                         1)
            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("0.0002"))
            order_id = self._place_order(False, amount, OrderType.LIMIT, price,
                                         2)
            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 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,
            trading_pairs=["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"]), s_decimal_0)
        self.assertGreaterEqual((balances["WETH"]), s_decimal_0)

    def test_single_limit_order_cancel(self):
        trading_pair: str = "ZRX-WETH"
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal(10)
        expires = int(time.time() + 60 * 5)
        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.8"),
                                       expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        self.assertEqual("ZRX-WETH", buy_order_opened_event.trading_pair)
        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):
        trading_pair: str = "ZRX-WETH"
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal(10)
        expires = int(time.time() + 60 * 5)
        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.8"),
                                       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.trading_pair)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)

        # Reset the logs
        self.market_logger.clear()

        sell_order_id = self.market.sell(trading_pair=trading_pair,
                                         amount=amount,
                                         order_type=OrderType.LIMIT,
                                         price=current_price * Decimal("1.2"),
                                         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.trading_pair)
        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):
        trading_pair: str = "ZRX-WETH"
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount: Decimal = Decimal(10)
        expires = int(time.time() + 60 * 2)  # expires in 2 min
        self.market.buy(trading_pair=trading_pair,
                        amount=amount,
                        order_type=OrderType.LIMIT,
                        price=current_price * Decimal("0.8"),
                        expiration_ts=expires)
        [buy_order_opened_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))

        self.assertEqual("ZRX-WETH", 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(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 = 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(amount_to_wrap, 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(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"
        trading_pair: 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: 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: float = Decimal("0.05") / bid_price
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, Decimal(amount))

            expires = int(time.time() + 60 * 5)
            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: RadarRelayMarket = RadarRelayMarket(
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_web3_provider_list[0],
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                trading_pairs=["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(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(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(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 = "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(trading_pair, True)
            amount: Decimal = Decimal("0.05") / 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 = Decimal(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)
示例#19
0
class LatokenExchangeUnitTest(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: LatokenExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: LatokenExchange = LatokenExchange(
            API_KEY, API_SECRET, trading_pairs=[trading_pair], domain=domain)
        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())

    @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__, "../latoken_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: TradeFeeBase = self.market.get_fee(base_asset, quote_asset,
                                                      OrderType.LIMIT,
                                                      TradeType.BUY,
                                                      Decimal("1"),
                                                      Decimal("1"))
        self.assertGreater(limit_fee.percent, Decimal("0"))
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFeeBase = self.market.get_fee(base_asset, quote_asset,
                                                       OrderType.MARKET,
                                                       TradeType.BUY,
                                                       Decimal("1"))
        self.assertGreater(market_fee.percent, Decimal("0"))
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_minimum_order_size(self):
        amount = Decimal("0.000001")
        quantized_amount = self.market.quantize_order_amount(
            trading_pair, amount)
        self.assertEqual(quantized_amount, Decimal("0"))

    def test_get_balance(self):
        balance = self.market.get_balance(quote_asset)
        self.assertGreater(balance, 10)

    def test_limit_buy(self):
        amount: Decimal = Decimal("0.04")
        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        # no fill
        bid_price: Decimal = Decimal("0.9") * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)

        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT,
                                   quantize_ask_price)

        # Wait for order creation event
        self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))

        # Cancel order. Automatically asserts that order is tracked
        self.market.cancel(trading_pair, order_id)

        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(order_cancelled_event.order_id, order_id)
        # # Reset the logs
        self.market_logger.clear()

    def test_limit_sell(self):
        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        # for no fill
        ask_price: Decimal = Decimal("1.1") * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price)
        amount: Decimal = Decimal("0.02")
        order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT,
                                    quantize_ask_price)
        # Wait for order creation event
        self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent))

        # Cancel order. Automatically asserts that order is tracked
        self.market.cancel(trading_pair, order_id)

        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))

        self.assertEqual(order_cancelled_event.order_id, order_id)

        # Reset the logs
        self.market_logger.clear()

    #
    # # WARNING AUTOMATICALLY EXECUTES ORDER
    # def test_execute_limit_buy(self):
    #     amount: Decimal = Decimal("0.04")
    #     quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair,
    #                                                                   amount)
    #
    #     # bid_entries = self.market.order_books[trading_pair].bid_entries()
    #     ask_entries = self.market.order_books[trading_pair].ask_entries()
    #     # most_top_bid = next(bid_entries)
    #     most_top_ask = next(ask_entries)
    #     # bid_price: Decimal = Decimal(most_top_bid.price)
    #     # quantize_bid_price = self.market.quantize_order_price(trading_pair, bid_price) * Decimal("1.1")
    #
    #     ask_price: Decimal = Decimal(most_top_ask.price)
    #     min_price_increment = self.market.trading_rules[trading_pair].min_price_increment
    #     ask_price_to_be_lifted = self.market.quantize_order_price(trading_pair, ask_price - min_price_increment)
    #
    #     order_id_sell = self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, ask_price_to_be_lifted)
    #     print(order_id_sell)
    #     _ = self.run_parallel(
    #         self.market_logger.wait_for(SellOrderCreatedEvent))
    #
    #     order_id_buy = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, ask_price_to_be_lifted)
    #
    #     # [order_completed_event_sell] = self.run_parallel(
    #     #     self.market_logger.wait_for(SellOrderCompletedEvent))
    #
    #     order_completed_event_buy, order_completed_event_sell = self.run_parallel(
    #         self.market_logger.wait_for(BuyOrderCompletedEvent), self.market_logger.wait_for(SellOrderCompletedEvent))
    #
    #     order_completed_event_buy: BuyOrderCompletedEvent = order_completed_event_buy
    #     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_buy.order_id)
    #     self.assertAlmostEqual(quantized_amount,
    #                            order_completed_event_buy.base_asset_amount)
    #     self.assertEqual(base_asset, order_completed_event_buy.base_asset)
    #     self.assertEqual(quote_asset, order_completed_event_buy.quote_asset)
    #     self.assertAlmostEqual(base_amount_traded,
    #                            order_completed_event_buy.base_asset_amount + order_completed_event_sell.base_asset_amount)
    #     self.assertAlmostEqual(quote_amount_traded,
    #                            order_completed_event_buy.quote_asset_amount + order_completed_event_sell.quote_asset_amount)
    #     self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id_buy
    #                          for event in self.market_logger.event_log]))
    #     # Reset the logs
    #     self.market_logger.clear()
    #
    # # WARNING AUTOMATICALLY EXECUTES ORDER
    # def test_execute_limit_sell(self):
    #     amount: Decimal = Decimal("0.04")
    #     quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair,
    #                                                                   amount)
    #
    #     bid_entries = self.market.order_books[trading_pair].bid_entries()
    #     # ask_entries = self.market.order_books[trading_pair].ask_entries()
    #     most_top_bid = next(bid_entries)
    #     # most_top_ask = next(ask_entries)
    #     # bid_price: Decimal = Decimal(most_top_bid.price)
    #     # quantize_bid_price = self.market.quantize_order_price(trading_pair, bid_price) * Decimal("1.1")
    #
    #     bid_price: Decimal = Decimal(most_top_bid.price)
    #     min_price_increment = self.market.trading_rules[trading_pair].min_price_increment
    #     bid_price_to_be_lifted = self.market.quantize_order_price(trading_pair, bid_price + min_price_increment)
    #
    #     order_id_sell = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, bid_price_to_be_lifted)
    #     print(order_id_sell)
    #     _ = self.run_parallel(
    #         self.market_logger.wait_for(BuyOrderCreatedEvent))
    #
    #     order_id_buy = self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, bid_price_to_be_lifted)
    #
    #     # [order_completed_event_sell] = self.run_parallel(
    #     #     self.market_logger.wait_for(SellOrderCompletedEvent))
    #
    #     order_completed_event_buy, order_completed_event_sell = self.run_parallel(
    #         self.market_logger.wait_for(SellOrderCompletedEvent), self.market_logger.wait_for(BuyOrderCompletedEvent))
    #
    #     order_completed_event_buy: SellOrderCompletedEvent = order_completed_event_buy
    #     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_buy.order_id)
    #     self.assertAlmostEqual(quantized_amount,
    #                            order_completed_event_buy.base_asset_amount)
    #     self.assertEqual(base_asset, order_completed_event_buy.base_asset)
    #     self.assertEqual(quote_asset, order_completed_event_buy.quote_asset)
    #     self.assertAlmostEqual(base_amount_traded,
    #                            order_completed_event_sell.base_asset_amount + order_completed_event_buy.base_asset_amount)
    #     self.assertAlmostEqual(quote_amount_traded,
    #                            order_completed_event_sell.quote_asset_amount + order_completed_event_buy.quote_asset_amount)
    #     self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id_buy
    #                          for event in self.market_logger.event_log]))
    #     # Reset the logs
    #     self.market_logger.clear()

    # needs manual execution
    # def test_execute_limit_sell(self):
    #     amount: Decimal = Decimal(0.02)
    #     quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair,
    #                                                                   amount)
    #     ask_entries = self.market.order_books[trading_pair].ask_entries()
    #     most_top_ask = next(ask_entries)
    #     ask_price: Decimal = Decimal(most_top_ask.price)
    #
    #     quantize_ask_price = self.market.quantize_order_price(trading_pair, ask_price) * Decimal("0.9")
    #
    #     order_id = self.market.sell(trading_pair,
    #                                 quantized_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: 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(base_asset, order_completed_event.base_asset)
    #     self.assertEqual(quote_asset, 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_orders_saving_and_restoration(self):
        self.tearDownClass()
        self.setUpClass()
        self.setUp()

        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()
        session = sql.get_new_session()
        try:
            self.assertEqual(0, len(self.market.tracking_states))

            amount: Decimal = Decimal(".04")
            current_bid_price: Decimal = self.market.get_price(
                trading_pair, False)
            bid_price: Decimal = Decimal("0.9") * current_bid_price
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                trading_pair, bid_price)
            order_id = self.market.buy(trading_pair, 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, session=session)
            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: LatokenExchange = LatokenExchange(
                API_KEY,
                API_SECRET,
                trading_pairs=[trading_pair],
                domain=domain)
            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,
                                                             session=session)
            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.run_parallel(asyncio.sleep(5.0))
            self.market.cancel(trading_pair, order_id)
            self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
            order_id = None
            recorder.save_market_states(config_path, self.market, session)

            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,
                                                             session=session)
            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))
            session.close()
            recorder.stop()
            self.setUpClass()

    # Place random orders and cancel them all. Beware : there are some time_out values that need
    # to be chosen carefully depending on the throttling policy of the exchange, otherwise it will not work
    def test_place_random_orders_and_cancel_all(self):
        # number of orders to be sent for testing cancellations (2 x order_count orders are sent : buy and sell)
        order_count = 100
        # timeout in seconds due to throttling of TPS coming from the exchange
        time_out_open_orders_sec = 10
        # timeout in seconds due to throttling of TPS coming from the exchange
        time_out_cancellations_sec = 10

        bid_price: Decimal = self.market.get_price(trading_pair, True)
        ask_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)

        # 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_ids = []

        for i in range(order_count):
            # Define a random diff price to change the price of placed orders
            diff_price = self.market.trading_rules[
                trading_pair].min_price_increment * random.randint(0, 9)

            quantize_bid_price = self.market.quantize_order_price(
                trading_pair, quantize_bid_price - diff_price)
            quantize_ask_price = self.market.quantize_order_price(
                trading_pair, quantize_ask_price + diff_price)

            order_id_buy = self.market.buy(trading_pair, quantized_amount,
                                           OrderType.LIMIT, quantize_bid_price)
            order_id_sell = self.market.sell(trading_pair, quantized_amount,
                                             OrderType.LIMIT,
                                             quantize_ask_price)

            order_ids.append(order_id_buy)
            order_ids.append(order_id_sell)

        self.run_parallel(asyncio.sleep(time_out_open_orders_sec))

        all_orders_opened = [
            self.market.in_flight_orders[order_id] for order_id in order_ids
        ]
        self.assertEqual(order_count * 2, len(all_orders_opened))
        are_all_orders_opened = [
            order.current_state == OrderState.OPEN
            for order in all_orders_opened
        ]
        self.assertTrue(all(are_all_orders_opened))
        are_all_orders_with_exchange_id = [
            order.exchange_order_id is not None for order in all_orders_opened
        ]
        self.assertTrue(all(are_all_orders_with_exchange_id))
        [cancellation_results] = self.run_parallel(
            self.market.cancel_all(time_out_cancellations_sec))
        # all_failing_order_ids = [order_id not in self.market.all_orders for order_id in order_ids]
        # failing_order_ids = [order_id for order_id in order_ids if order_id not in self.market.all_orders]
        # failing_order_ids = [self.market.all_orders[order_id].exchange_order_id is None for order_id in order_ids]
        are_all_orders_cancelled = [
            self.market.all_orders[order_id].current_state ==
            OrderState.CANCELED for order_id in order_ids
        ]
        self.assertTrue(all(are_all_orders_cancelled))

        for cr in cancellation_results:
            self.assertEqual(cr.success, True)
示例#20
0
class DDEXMarketUnitTest(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: DDEXMarket
    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_test_private_key_ddex,
                                backend_urls=conf.test_ddex_web3_provider_list,
                                erc20_token_addresses=[
                                    conf.test_ddex_erc20_token_address_1,
                                    conf.test_ddex_erc20_token_address_2
                                ],
                                chain=EthereumChain.MAIN_NET)
        cls.market: DDEXMarket = DDEXMarket(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_ddex_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            symbols=["HOT-WETH"])
        print("Initializing DDEX 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__, "../ddex_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):
        weth_trade_fee: TradeFee = self.market.get_fee("ZRX", "WETH",
                                                       OrderType.LIMIT,
                                                       TradeType.BUY, 10000, 1)
        self.assertGreater(weth_trade_fee.percent, 0)
        self.assertEqual(len(weth_trade_fee.flat_fees), 1)
        self.assertEqual(weth_trade_fee.flat_fees[0][0], "WETH")
        dai_trade_fee: TradeFee = self.market.get_fee("WETH", "DAI",
                                                      OrderType.MARKET,
                                                      TradeType.BUY, 10000)
        self.assertGreater(dai_trade_fee.percent, 0)
        self.assertEqual(len(dai_trade_fee.flat_fees), 1)
        self.assertEqual(dai_trade_fee.flat_fees[0][0], "DAI")

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

    def test_list_orders(self):
        [orders] = self.run_parallel(self.market.list_orders())
        self.assertGreaterEqual(len(orders), 0)

    def test_list_locked_balances(self):
        [locked_balances
         ] = self.run_parallel(self.market.list_locked_balances())
        self.assertGreaterEqual(len(locked_balances), 0)

    @unittest.skipUnless(
        any("test_bad_orders_are_not_tracked" in arg for arg in sys.argv),
        "bad_orders_are_not_tracked test requires manual action.")
    def test_bad_orders_are_not_tracked(self):
        # Should fail due to insufficient balance
        order_id = self.market.buy("WETH-DAI", 10000, OrderType.LIMIT, 1)
        self.assertEqual(self.market.in_flight_orders.get(order_id), None)

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

        # Intentionally setting invalid price to prevent getting filled
        client_order_id = self.market.buy(symbol, amount, OrderType.LIMIT,
                                          bid_price * 0.7)
        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.1)
        self.assertGreater(self.market.get_balance("HOT"), 2000)

        # Try to buy 2000 HOT from the exchange, and watch for completion event.
        symbol = "HOT-WETH"
        bid_price: float = self.market.get_price(symbol, True)
        amount: float = 2000
        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))
        exchange_order_id: str = self.market.in_flight_orders.get(
            buy_order_id).exchange_order_id
        buy_order = self.run_parallel(self.market.get_order(exchange_order_id))
        self.assertEqual(buy_order[0].get('id'), exchange_order_id)
        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 back the same amount of HOT to the exchange, and watch for completion 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))
        exchange_order_id: str = self.market.in_flight_orders.get(
            sell_order_id).exchange_order_id
        sell_order = self.run_parallel(
            self.market.get_order(exchange_order_id))
        self.assertEqual(sell_order[0].get('id'), exchange_order_id)
        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_limit_buy_and_sell_get_matched" in arg for arg in sys.argv),
        "test_limit_buy_and_sell_get_matched test requires manual action.")
    def test_limit_buy_and_sell_get_matched(self):
        self.assertGreater(self.market.get_balance("WETH"), 0.01)

        # Try to buy 0.01 WETH worth of HOT from the exchange, and watch for completion event.
        current_price: float = self.market.get_price("HOT-WETH", True)
        amount: float = 2000
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "HOT-WETH", amount)
        order_id = self.market.buy("HOT-WETH", amount, OrderType.LIMIT,
                                   current_price)
        [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.LIMIT for evt in order_filled_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         order_completed_event.base_asset_amount)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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 HOT to the exchange, and watch for completion event.
        current_price: float = self.market.get_price("HOT-WETH", False)
        amount = float(order_completed_event.base_asset_amount)
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.market.sell("HOT-WETH", amount, OrderType.LIMIT,
                                    current_price)
        [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.LIMIT for evt in order_filled_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertEqual(float(quantized_amount),
                         order_completed_event.base_asset_amount)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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_market_buy_and_sell(self):
        self.assertGreater(self.market.get_balance("WETH"), 0.01)

        amount: float = 2000.0  # Min order size is 1000 HOT
        quantized_amount: Decimal = self.market.quantize_order_amount(
            "HOT-WETH", amount)

        order_id = self.market.buy("HOT-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)

        # This is because some of the tokens are deducted in the trading fees.
        self.assertTrue(
            float(quantized_amount) > order_completed_event.base_asset_amount >
            float(quantized_amount) * 0.9)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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 HOT to the exchange, and watch for completion event.
        amount = float(order_completed_event.base_asset_amount)
        quantized_amount = order_completed_event.base_asset_amount
        order_id = self.market.sell("HOT-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),
                         order_completed_event.base_asset_amount)
        self.assertEqual("HOT", order_completed_event.base_asset)
        self.assertEqual("WETH", order_completed_event.quote_asset)
        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
            ]))

    @unittest.skipUnless(any("test_wrap_eth" in arg for arg in sys.argv),
                         "Wrap Eth test requires manual action.")
    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)

    @unittest.skipUnless(any("test_unwrap_eth" in arg for arg in sys.argv),
                         "Unwrap Eth test requires manual action.")
    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_cancel_all_happy_case(self):
        symbol = "HOT-WETH"
        bid_price: float = self.market.get_price(symbol, True)
        ask_price: float = self.market.get_price(symbol, False)
        amount = 2000

        self.assertGreater(self.market.get_balance("WETH"), 0.02)
        self.assertGreater(self.market.get_balance("HOT"), amount)

        # Intentionally setting invalid price to prevent getting filled
        self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7)
        self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(10))
        print(cancellation_results)
        self.assertGreater(len(cancellation_results), 0)
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_cancel_all_failure_case(self):
        symbol = "HOT-WETH"
        bid_price: float = self.market.get_price(symbol, True)
        ask_price: float = self.market.get_price(symbol, False)
        # order submission should fail due to insufficient balance
        amount = 200000

        self.assertLess(self.market.get_balance("WETH"), 100)
        self.assertLess(self.market.get_balance("HOT"), amount)

        self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7)
        self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(10))
        print(cancellation_results)
        self.assertGreater(len(cancellation_results), 0)
        for cr in cancellation_results:
            self.assertEqual(cr.success, False)

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "HOT-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))

            # Try to put limit buy order for 0.05 ETH worth of HOT, 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.05 / bid_price
            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.market_events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: DDEXMarket = DDEXMarket(
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_ddex_web3_provider_list[0],
                order_book_tracker_data_source_type=
                OrderBookTrackerDataSourceType.EXCHANGE_API,
                symbols=[symbol])
            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))
            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(1, len(self.market.tracking_states))
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.assertEqual(1, 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 = "HOT-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 HOT from the exchange, and watch for completion event.
            current_price: float = self.market.get_price(symbol, True)
            amount: float = 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 HOT 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)
class HuobiMarketUnitTest(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: HuobiMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: HuobiMarket = HuobiMarket(conf.huobi_api_key,
                                              conf.huobi_secret_key,
                                              symbols=["ethusdt"])
        # Need 2nd instance of market to prevent events mixing up across tests
        cls.market_2: HuobiMarket = HuobiMarket(conf.huobi_api_key,
                                                conf.huobi_secret_key,
                                                symbols=["ethusdt"])
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        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()

    @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__, "../huobi_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: TradeFee = self.market.get_fee("eth", "usdt",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, 1, 10)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("eth", "usdt",
                                                   OrderType.MARKET,
                                                   TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)
        sell_trade_fee: TradeFee = self.market.get_fee("eth", "usdt",
                                                       OrderType.LIMIT,
                                                       TradeType.SELL, 1, 10)
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

    def test_limit_buy(self):
        self.assertGreater(self.market.get_balance("eth"), 0.1)
        symbol = "ethusdt"
        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("usdt", 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.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()

    def test_limit_sell(self):
        symbol = "ethusdt"
        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: 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, 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("usdt", 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.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
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_market_buy(self):
        symbol = "ethusdt"
        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)
        [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.MARKET 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_market_sell(self):
        symbol = "ethusdt"
        amount: float = 0.02
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0)
        [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.MARKET 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_order(self):
        symbol = "ethusdt"

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

        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)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        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 = "ethusdt"

        bid_price: float = self.market_2.get_price(symbol, True) * 0.5
        ask_price: float = self.market_2.get_price(symbol, False) * 2
        amount: float = 0.05
        quantized_amount: Decimal = self.market_2.quantize_order_amount(
            symbol, amount)

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

        self.market_2.buy(symbol, quantized_amount, OrderType.LIMIT,
                          quantize_bid_price)
        self.market_2.sell(symbol, quantized_amount, OrderType.LIMIT,
                           quantize_ask_price)
        self.run_parallel(asyncio.sleep(1))
        [cancellation_results] = self.run_parallel(self.market_2.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"
        symbol: str = "ethusdt"
        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: HuobiMarket = HuobiMarket(
                huobi_api_key=conf.huobi_api_key,
                huobi_secret_key=conf.huobi_secret_key,
                symbols=["ethusdt", "btcusdt"])
            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 = "ethusdt"
        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: 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)
示例#22
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

    @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)
示例#23
0
class HuobiMarketUnitTest(AioHTTPTestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled, MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled
    ]

    market: HuobiMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.stack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)

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

    # get_application overrides the aiohttp.web and allows mocking api endpoints
    async def get_application(self):
        app = web.Application()
        self.mock_api = HuobiMockAPI()
        app.router.add_get("/mockSnapshot", self.mock_api.get_mock_snapshot)
        app.router.add_get("/market/tickers", self.mock_api.get_market_tickers)
        app.router.add_get("/account/accounts",
                           self.mock_api.get_account_accounts)
        app.router.add_get("/common/timestamp",
                           self.mock_api.get_common_timestamp)
        app.router.add_get("/common/symbols", self.mock_api.get_common_symbols)
        app.router.add_get("/account/accounts/{user_id}/balance",
                           self.mock_api.get_user_balance)
        app.router.add_post("/order/orders/place",
                            self.mock_api.post_order_place)
        app.router.add_get("/order/orders/{order_id}",
                           self.mock_api.get_order_update)
        app.router.add_post("/order/orders/{order_id}/submitcancel",
                            self.mock_api.post_submit_cancel)
        app.router.add_post("/order/orders/batchcancel",
                            self.mock_api.post_batch_cancel)
        return app

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

    # setUp function from unittests is called before get_application so this needs
    # to be called manually before every test
    def customSetUp(self):
        self.market: HuobiMarket = HuobiMarket(MOCK_HUOBI_API_KEY,
                                               MOCK_HUOBI_SECRET_KEY,
                                               symbols=["ethusdt"])

        # replace regular aiohttp client with test client
        self.market.shared_client: TestClient = self.client
        # replace default data source with mock data source
        mock_data_source: MockAPIOrderBookDataSource = MockAPIOrderBookDataSource(
            self.client, HuobiOrderBook, ["ethusdt"])
        self.market.order_book_tracker.data_source = mock_data_source

        self.clock.add_iterator(self.market)
        self.run_parallel(self.wait_til_ready(self.market, self._clock))
        self.db_path: str = realpath(join(__file__, "../huobi_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(0.5)
        return future.result()

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

    def test_get_fee(self):
        self.customSetUp()
        limit_fee: TradeFee = self.market.get_fee("eth", "usdt",
                                                  OrderType.LIMIT,
                                                  TradeType.BUY, 1, 10)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: TradeFee = self.market.get_fee("eth", "usdt",
                                                   OrderType.MARKET,
                                                   TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)
        sell_trade_fee: TradeFee = self.market.get_fee("eth", "usdt",
                                                       OrderType.LIMIT,
                                                       TradeType.SELL, 1, 10)
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

    def test_limit_buy(self):
        self.customSetUp()
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_BUY_ORDER_ID
        symbol = "ethusdt"
        amount: Decimal = Decimal(0.02)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)
        current_bid_price: Decimal = self.market.get_price(symbol, True)
        bid_price: Decimal = current_bid_price + Decimal(
            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: Decimal = Decimal(
            sum(t.amount for t in trade_events))
        quote_amount_traded: Decimal = 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.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()

    def test_limit_sell(self):
        self.customSetUp()
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_SELL_ORDER_ID
        symbol = "ethusdt"
        amount: Decimal = Decimal(0.02)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        current_ask_price: Decimal = self.market.get_price(symbol, False)
        ask_price: Decimal = current_ask_price - Decimal(
            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: 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, 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.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
            ]))
        # Reset the logs
        self.market_logger.clear()

    def test_market_buy(self):
        self.customSetUp()
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_BUY_ORDER_ID
        symbol = "ethusdt"
        amount: Decimal = Decimal(0.02)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET,
                                   0)
        [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 = Decimal(
            sum(t.amount for t in trade_events))
        quote_amount_traded: Decimal = 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, 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.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_market_sell(self):
        self.customSetUp()
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_SELL_ORDER_ID
        symbol = "ethusdt"
        amount: Decimal = Decimal(0.02)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            symbol, amount)

        order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0)
        [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 = 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, 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):
        self.customSetUp()
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_CANCEL_ORDER_ID
        symbol = "ethusdt"

        current_bid_price: Decimal = self.market.get_price(symbol, True)
        amount: Decimal = Decimal(0.02)

        bid_price: Decimal = current_bid_price * Decimal(0.9)
        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)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        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):
        self.customSetUp()
        self.mock_api.cancel_all_order_ids = [
            self.mock_api.MOCK_HUOBI_LIMIT_BUY_ORDER_ID,
            self.mock_api.MOCK_HUOBI_LIMIT_SELL_ORDER_ID,
        ]
        symbol = "ethusdt"

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

        # Intentionally setting high price to prevent getting filled
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            symbol, bid_price * Decimal(0.7))
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            symbol, ask_price * Decimal(1.5))
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_BUY_ORDER_ID
        self.market.buy(symbol, quantized_amount, OrderType.LIMIT,
                        quantize_bid_price)
        self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_SELL_ORDER_ID
        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)

    def test_orders_saving_and_restoration(self):
        self.customSetUp()
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "ethusdt"
        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(symbol, True)
            bid_price: Decimal = current_bid_price * Decimal(0.8)
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                symbol, bid_price)

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

            self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_OPEN_ORDER_ID
            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: HuobiMarket = HuobiMarket(
                huobi_api_key=MOCK_HUOBI_API_KEY,
                huobi_secret_key=MOCK_HUOBI_SECRET_KEY,
                symbols=["ethusdt", "btcusdt"])
            self.market.shared_client: TestClient = self.client
            mock_data_source: MockAPIOrderBookDataSource = MockAPIOrderBookDataSource(
                self.client, HuobiOrderBook, ["ethusdt"])
            self.market.order_book_tracker.data_source = mock_data_source

            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.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_OPEN_ORDER_ID
            self.mock_api.order_response_dict[
                self.mock_api.
                MOCK_HUOBI_LIMIT_OPEN_ORDER_ID]["data"]["state"] = "canceled"
            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):
        self.customSetUp()
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = "ethusdt"
        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.04)
            self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_BUY_ORDER_ID
            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.
            self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_SELL_ORDER_ID
            amount: Decimal = 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)
示例#24
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: 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_fee_overrides_config(self):
        fee_overrides_config_map["beaxy_taker_fee"].value = None
        taker_fee: TradeFee = 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: TradeFee = 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: TradeFee = 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: TradeFee = 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')
示例#25
0
class IDEXMarketUnitTest(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: IDEXMarket
    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_test_private_key_idex,
                                backend_urls=conf.test_web3_provider_list,
                                erc20_token_addresses=[conf.test_idex_erc20_token_address_1,
                                                       conf.test_idex_erc20_token_address_2],
                                chain=EthereumChain.MAIN_NET)
        cls.market: IDEXMarket = IDEXMarket(
            idex_api_key=conf.idex_api_key,
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API,
            symbols=[ETH_QNT]
        )
        print("Initializing IDEX 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__, "../idex_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))
        await self.market.start_network()
        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_wallet_balances(self):
        balances = self.market.get_all_balances()
        self.assertGreaterEqual((balances["ETH"]), s_decimal_0)

    def test_quantize_order_amount(self):
        amount = self.market.quantize_order_amount("ETH_QNT", Decimal(0.01))
        self.assertEqual(amount, 0)
        amount = self.market.quantize_order_amount("ETH_QNT", Decimal(100000))
        self.assertEqual(amount, 100000)

    def test_place_limit_buy_and_cancel(self):
        symbol = ETH_QNT
        buy_amount: Decimal = Decimal("16000000")
        buy_price = Decimal("0.00000001")
        buy_order_id: str = self.market.buy(symbol, buy_amount, OrderType.LIMIT, buy_price)
        [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(buy_amount, buy_order_opened_event.amount)
        self.assertEqual(ETH_QNT, buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)

        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)

    def test_place_limit_sell_and_cancel(self):
        symbol = ETH_QNT
        sell_amount: Decimal = Decimal(5)
        sell_price = Decimal(100000000)
        sell_order_id: str = self.market.sell(symbol, sell_amount, OrderType.LIMIT, sell_price)
        [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(sell_amount, float(sell_order_opened_event.amount))
        self.assertEqual(ETH_QNT, sell_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type)

        self.run_parallel(self.market.cancel_order(sell_order_id))
        [sell_order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(sell_order_opened_event.order_id, sell_order_cancelled_event.order_id)

    def test_cancel_all_happy_case(self):
        symbol = ETH_QNT
        buy_amount: Decimal = Decimal(17000000)
        buy_price = Decimal("0.00000001")
        buy_order_id: str = self.market.buy(symbol, buy_amount, OrderType.LIMIT, buy_price)
        [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(buy_amount, float(buy_order_opened_event.amount))
        self.assertEqual(ETH_QNT, buy_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type)
        symbol = ETH_QNT
        sell_amount: Decimal = Decimal(5)
        sell_price = Decimal(110000000)
        sell_order_id: str = self.market.sell(symbol, sell_amount, OrderType.LIMIT, sell_price)
        [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(sell_amount, float(sell_order_opened_event.amount))
        self.assertEqual(ETH_QNT, sell_order_opened_event.symbol)
        self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type)

        [cancellation_results] = self.run_parallel(self.market.cancel_all(30))
        self.assertGreater(len(cancellation_results), 0)
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

    def test_market_buy(self):
        symbol = ETH_QNT
        current_price: Decimal = Decimal(self.market.get_price(symbol, True))
        buy_amount: Decimal = Decimal(0.16) / current_price
        buy_order_id: str = self.market.buy(symbol, buy_amount, OrderType.MARKET)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent))
        order_completed_event: BuyOrderCompletedEvent = order_completed_event
        self.assertEqual(buy_order_id, order_completed_event.order_id)

    def test_market_sell(self):
        symbol = ETH_QNT
        current_price: Decimal = Decimal(self.market.get_price(symbol, False))
        sell_amount: Decimal = Decimal(0.155) / current_price
        sell_order_id: str = self.market.sell(symbol, sell_amount, OrderType.MARKET)
        [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent))
        order_completed_event: SellOrderCompletedEvent = order_completed_event
        self.assertEqual(sell_order_id, order_completed_event.order_id)

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        symbol: str = ETH_QNT
        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.05 ETH worth of QNT, and watch for order creation event.
            bid_price = Decimal("0.00000002")
            quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price)

            amount: Decimal = Decimal("18000000")
            quantized_amount: Decimal = self.market.quantize_order_amount(symbol, 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))
            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.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.market_events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: IDEXMarket = IDEXMarket(
                idex_api_key=conf.idex_api_key,
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_web3_provider_list[0],
                order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API,
                symbols=[ETH_QNT]
            )
            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))
            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(1, len(self.market.tracking_states))
            saved_market_states = recorder.get_market_states(config_path, self.market)
            self.assertEqual(1, 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_QNT
        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.16 ETH worth of QNT from the exchange, and watch for completion event.
            current_price: Decimal = Decimal(self.market.get_price(symbol, True))
            amount: Decimal = Decimal(0.16) / 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 QNT 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)
示例#26
0
class BittrexMarketUnitTest(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: 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, 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.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["bittrex_taker_fee"].value = None
        taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, 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.MARKET, 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, 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, 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.MARKET:
                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_buy(self):
        self.assertGreater(self.market.get_balance("USDT"), 20)
        trading_pair = "ETH-USDT"

        self.run_parallel(asyncio.sleep(3))
        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        bid_price: Decimal = current_bid_price * Decimal('1.005')
        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, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_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_sell(self):
        trading_pair = "ETH-USDT"

        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        ask_price: Decimal = current_ask_price - Decimal('0.005') * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, ask_price)

        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, quantize_ask_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_market_buy(self):
        self.assertGreater(self.market.get_balance("USDT"), 20)
        trading_pair = "ETH-USDT"

        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.MARKET, 0, 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.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("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_market_sell(self):
        trading_pair = "ETH-USDT"
        self.assertGreater(self.market.get_balance("ETH"), 0.06)

        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.MARKET, 0, 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.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("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,
                                                   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,
                                              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,
                                              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)

    @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("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)

        self.market.buy(trading_pair, quantized_bid_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()
        [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:

            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.MARKET, 0, 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.MARKET, 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)
示例#27
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_CANCELLED.copy()
            resp["order_id"] = exch_order_id
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.1)
            resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.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_CANCELLED)
        [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_CANCELLED.copy()
            resp["order_id"] = exch_order_id
            MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL,
                                                            resp,
                                                            delay=0.1)
            resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.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_CANCELLED)
            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_CANCELLED)
                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_CANCELLED)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
示例#28
0
class BitfinexExchangeUnitTest(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: BitfinexExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BitfinexExchange = BitfinexExchange(
            API_KEY, API_SECRET, trading_pairs=[trading_pair])
        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())

    @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__, "../bitfinex_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(
            base_asset, quote_asset, 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(
            base_asset, quote_asset, OrderType.MARKET, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)

    def test_minimum_order_size(self):
        amount = Decimal("0.001")
        quantized_amount = self.market.quantize_order_amount(
            trading_pair, amount)
        self.assertEqual(quantized_amount, 0)

    def test_get_balance(self):
        balance = self.market.get_balance(quote_asset)
        self.assertGreater(balance, 10)

    def test_limit_buy(self):
        amount: Decimal = Decimal("0.04")
        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        # no fill
        bid_price: Decimal = Decimal("0.9") * current_ask_price
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)

        order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT,
                                   quantize_ask_price)

        # Wait for order creation event
        self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent))

        # Cancel order. Automatically asserts that order is tracked
        self.market.cancel(trading_pair, order_id)

        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))
        self.assertEqual(order_cancelled_event.order_id, order_id)
        # # Reset the logs
        self.market_logger.clear()

    def test_limit_sell(self):
        amount: Decimal = Decimal("0.02")
        current_ask_price: Decimal = self.market.get_price(trading_pair, False)
        # for no fill
        ask_price: Decimal = Decimal("1.1") * 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)
        # Wait for order creation event
        self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent))

        # Cancel order. Automatically asserts that order is tracked
        self.market.cancel(trading_pair, order_id)

        [order_cancelled_event] = self.run_parallel(
            self.market_logger.wait_for(OrderCancelledEvent))

        self.assertEqual(order_cancelled_event.order_id, order_id)

        # Reset the logs
        self.market_logger.clear()

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

        bid_entries = self.market.order_books[trading_pair].bid_entries()

        most_top_bid = next(bid_entries)
        bid_price: Decimal = Decimal(most_top_bid.price)
        quantize_bid_price: Decimal = \
            self.market.quantize_order_price(trading_pair, bid_price)
        quantize_bid_price = quantize_bid_price * Decimal("1.1")

        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(base_asset, order_completed_event.base_asset)
        self.assertEqual(quote_asset, 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_execute_limit_sell(self):
        amount: Decimal = Decimal(0.02)
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        ask_entries = self.market.order_books[trading_pair].ask_entries()
        most_top_ask = next(ask_entries)
        ask_price: Decimal = Decimal(most_top_ask.price)
        quantize_ask_price: Decimal = \
            self.market.quantize_order_price(trading_pair, ask_price)
        quantize_ask_price = quantize_ask_price * Decimal("0.9")

        order_id = self.market.sell(
            trading_pair,
            quantized_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: 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(base_asset, order_completed_event.base_asset)
        self.assertEqual(quote_asset, 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_orders_saving_and_restoration(self):
        self.tearDownClass()
        self.setUpClass()
        self.setUp()

        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))

            amount: Decimal = Decimal("0.04")
            current_ask_price: Decimal = self.market.get_price(
                trading_pair, False)
            bid_price: Decimal = Decimal("0.9") * current_ask_price
            quantize_ask_price: Decimal = self.market.quantize_order_price(
                trading_pair, bid_price)
            order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT,
                                       quantize_ask_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: BitfinexExchange = BitfinexExchange(
                API_KEY, API_SECRET, trading_pairs=[trading_pair])
            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.run_parallel(asyncio.sleep(5.0))
            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()
            self.setUpClass()

    def test_cancel_all(self):
        bid_price: Decimal = self.market.get_price(trading_pair, True)
        ask_price: Decimal = self.market.get_price(trading_pair, False)
        amount: Decimal = Decimal("0.04")
        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"))

        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(5))
        [cancellation_results] = self.run_parallel(self.market.cancel_all(45))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)
示例#29
0
class DigifinexExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]
    connector: DigifinexExchange
    event_logger: EventLogger
    trading_pair = "BTC-USDT"
    base_token, quote_token = trading_pair.split("-")
    stack: contextlib.ExitStack
    sql: SQLConnectionManager

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.ev_loop = asyncio.get_event_loop()

        if API_MOCK_ENABLED:
            raise NotImplementedError()
            # cls.web_app = HummingWebApp.get_instance()
            # cls.web_app.add_host_to_mock(BASE_API_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", BASE_API_URL, "/v2/public/get-ticker", fixture.TICKERS)
            # cls.web_app.update_response("get", BASE_API_URL, "/v2/public/get-instruments", fixture.INSTRUMENTS)
            # cls.web_app.update_response("get", BASE_API_URL, "/v2/public/get-book", fixture.GET_BOOK)
            # cls.web_app.update_response("post", BASE_API_URL, "/v2/private/get-account-summary", fixture.BALANCES)
            # cls.web_app.update_response("post", BASE_API_URL, "/v2/private/cancel-order", fixture.CANCEL)

            # HummingWsServerFactory.start_new_server(WSS_PRIVATE_URL)
            # HummingWsServerFactory.start_new_server(WSS_PUBLIC_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

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: DigifinexExchange = DigifinexExchange(
            digifinex_api_key=API_KEY,
            digifinex_secret_key=API_SECRET,
            trading_pairs=[cls.trading_pair],
            trading_required=True)
        print(
            "Initializing Digifinex 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)
        # if API_MOCK_ENABLED:
        #     HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_INITIATED, delay=0.5)
        #     HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_SUBSCRIBE, delay=0.51)
        #     HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_HEARTBEAT, delay=0.52)

        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._ws_patcher.stop()

    @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:
            # on windows cannot unlink the sqlite db file before closing the db
            if os.name != 'nt':
                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
        # self.sql._engine.dispose()

    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 API_MOCK_ENABLED:
        #     data = fixture.PLACE_ORDER.copy()
        #     data["result"]["order_id"] = str(ex_order_id)
        #     self.web_app.update_response("post", BASE_API_URL, "/v2/private/create-order", data)
        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)
        # if API_MOCK_ENABLED:
        #     if get_order_fixture is not None:
        #         data = get_order_fixture.copy()
        #         data["result"]["order_info"]["client_oid"] = cl_order_id
        #         data["result"]["order_info"]["order_id"] = ex_order_id
        #         self.web_app.update_response("post", BASE_API_URL, "/v2/private/get-order-detail", data)
        #     if ws_trade_fixture is not None:
        #         data = ws_trade_fixture.copy()
        #         data["result"]["data"][0]["order_id"] = str(ex_order_id)
        #         HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.1)
        #     if ws_order_fixture is not None:
        #         data = ws_order_fixture.copy()
        #         data["result"]["data"][0]["order_id"] = str(ex_order_id)
        #         data["result"]["data"][0]["client_oid"] = cl_order_id
        #         HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.12)
        return cl_order_id

    def _cancel_order(self, cl_order_id):
        self.connector.cancel(self.trading_pair, cl_order_id)
        # if API_MOCK_ENABLED:
        #     data = fixture.WS_ORDER_CANCELED.copy()
        #     data["result"]["data"][0]["client_oid"] = cl_order_id
        #     HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.1)

    def test_buy_and_sell(self):
        self.ev_loop.run_until_complete(self.connector.cancel_all(0))

        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("0.0001"))
        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("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.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),
                               delta=0.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("0.0001"))
        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("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)
        # todo: get fee
        # 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("0.00005"))
        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), 1)
        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("0.0001"))

        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):
    #     if API_MOCK_ENABLED:
    #         available = float(available)
    #         data = fixture.WS_BALANCE.copy()
    #         data["result"]["data"][0]["currency"] = token
    #         data["result"]["data"][0]["available"] = available
    #         HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_BALANCE, delay=0.1)

    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("0.0001"))
        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("0.0001"))
        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("0.0001"))

        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))
        # if API_MOCK_ENABLED:
        #     data = fixture.WS_ORDER_CANCELED.copy()
        #     data["result"]["data"][0]["client_oid"] = buy_id
        #     data["result"]["data"][0]["order_id"] = 1
        #     HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.1)
        #     self.ev_loop.run_until_complete(asyncio.sleep(1))
        #     data = fixture.WS_ORDER_CANCELED.copy()
        #     data["result"]["data"][0]["client_oid"] = sell_id
        #     data["result"]["data"][0]["order_id"] = 2
        #     HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.11)
        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("USDT"), Decimal("10"))

        # 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 = DigifinexExchange(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)
            if not API_MOCK_ENABLED:
                self.ev_loop.run_until_complete(
                    self.wait_til_ready(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()
            # sql._engine.dispose()
            # on windows cannot unlink the sqlite db file before closing the db
            if os.name != 'nt':
                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("0.0001"))

            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("0.0001"))
            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()
            # sql._engine.dispose()
            # on windows cannot unlink the sqlite db file before closing the db
            if os.name != 'nt':
                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,
        MarketEvent.OrderCancelled
    ]

    market: CoinbaseProMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @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)
        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()

    @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)

    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(
                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"]
            )
            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)