class BitfinexOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[BitfinexOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = [
        "BTC-USD",
    ]
    integrity_test_max_volume = 5  # Max volume in asks and bids for the book to be ready for tests
    daily_volume = 2500  # Approximate total daily volume in BTC for this exchange for sanity test
    book_enties = 5  # Number of asks and bids (each) for the book to be ready for tests

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: BitfinexOrderBookTracker = BitfinexOrderBookTracker(
            trading_pairs=cls.trading_pairs)
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(
            cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

    @classmethod
    async def wait_til_tracker_ready(cls):
        '''
        Wait until the order book under test fills as needed
        '''
        print("Waiting for order book to fill...")
        while True:
            book_present = cls.trading_pairs[
                0] in cls.order_book_tracker.order_books
            enough_asks = False
            enough_bids = False
            enough_ask_rows = False
            enough_bid_rows = False
            if book_present:
                ask_volume = sum(i.amount
                                 for i in cls.order_book_tracker.order_books[
                                     cls.trading_pairs[0]].ask_entries())
                ask_count = sum(1 for i in cls.order_book_tracker.order_books[
                    cls.trading_pairs[0]].ask_entries())

                bid_volume = sum(i.amount
                                 for i in cls.order_book_tracker.order_books[
                                     cls.trading_pairs[0]].bid_entries())
                bid_count = sum(1 for i in cls.order_book_tracker.order_books[
                    cls.trading_pairs[0]].bid_entries())

                enough_asks = ask_volume >= cls.integrity_test_max_volume
                enough_bids = bid_volume >= cls.integrity_test_max_volume

                enough_ask_rows = ask_count >= cls.book_enties
                enough_bid_rows = bid_count >= cls.book_enties

                print(
                    "Bid volume in book: %f (in %d bids), ask volume in book: %f (in %d asks)"
                    % (bid_volume, bid_count, ask_volume, ask_count))

            if book_present and enough_asks and enough_bids and enough_ask_rows and enough_bid_rows:
                print("Initialized real-time order books.")
                return
            await asyncio.sleep(1)

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

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

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

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

    def test_tracker_integrity(self):
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        sut_book: OrderBook = order_books[self.trading_pairs[0]]

        # # 1 - test that best bid is less than best ask
        # self.assertGreater(sut_book.get_price(False), sut_book.get_price(True))

        # 2 - test that price to buy integrity_test_max_volume BTC is is greater than or equal to best ask
        self.assertGreaterEqual(
            sut_book.get_price_for_volume(
                True, self.integrity_test_max_volume).result_price,
            sut_book.get_price(True))

        # 3 - test that price to sell integrity_test_max_volume BTC is is less than or equal to best bid
        self.assertLessEqual(
            sut_book.get_price_for_volume(
                False, self.integrity_test_max_volume).result_price,
            sut_book.get_price(False))

        # 4 - test that all bids in order book are sorted by price in descending order
        previous_price = sys.float_info.max
        for bid_row in sut_book.bid_entries():
            self.assertTrue(previous_price >= bid_row.price)
            previous_price = bid_row.price

        # 5 - test that all asks in order book are sorted by price in ascending order
        previous_price = 0
        for ask_row in sut_book.ask_entries():
            self.assertTrue(previous_price <= ask_row.price)
            previous_price = ask_row.price

        # 6 - test that total volume in first   orders in book is less than 10 times
        # daily traded volumes for this exchange
        total_volume = 0
        count = 0
        for bid_row in sut_book.bid_entries():
            total_volume += bid_row.amount
            count += 1
            if count > self.book_enties:
                break
        count = 0
        for ask_row in sut_book.ask_entries():
            total_volume += ask_row.amount
            count += 1
            if count > self.book_enties:
                break
        self.assertLessEqual(total_volume, 10 * self.daily_volume)
Ejemplo n.º 2
0
class RadarRelayMarketUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderExpired,
        MarketEvent.OrderFilled,
    ]

    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(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            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(
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_web3_provider_list[0],
                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)
class AscendExOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[AscendExOrderBookTracker] = None
    events: List[OrderBookEvent] = [
        OrderBookEvent.TradeEvent
    ]
    trading_pairs: List[str] = [
        "BTC-USDT",
        "ETH-USDT",
    ]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        cls.order_book_tracker: AscendExOrderBookTracker = AscendExOrderBookTracker(cls.throttler, cls.trading_pairs)
        cls.order_book_tracker.start()
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

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

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

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

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

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

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

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

    market: PaperTradeMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: PaperTradeMarket = PaperTradeMarket(
            order_book_tracker=BinanceOrderBookTracker(
                trading_pairs=["ETHUSDT", "BTCUSDT"]),
            config=MarketConfig.default_config(),
            target_market=BinanceMarket)
        print(
            "Initializing PaperTrade execute orders market... this will take about a minute."
        )
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.market)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        safe_ensure_future(delay_trigger_event2())

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        matched_order_cancel_events = TestUtils.get_match_events(
            self.market_logger.event_log, OrderCancelledEvent, {})
        # Market should emit cancel event
        self.assertEqual(2, len(matched_order_cancel_events))
Ejemplo n.º 5
0
class UniswapConnectorUnitTest(unittest.TestCase):
    event_logger: EventLogger
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]
    connector: UniswapConnector
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls._gas_price_patcher = unittest.mock.patch(
            "hummingbot.connector.connector.uniswap.uniswap_connector.get_gas_price"
        )
        cls._gas_price_mock = cls._gas_price_patcher.start()
        cls._gas_price_mock.return_value = 50
        cls.ev_loop = asyncio.get_event_loop()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: UniswapConnector = UniswapConnector([
            trading_pair
        ], "0xdc393a78a366ac53ffbd5283e71785fd2097807fef1bc5b73b8ec84da47fb8de",
                                                           "")
        print(
            "Initializing CryptoCom market... this will take about a minute.")
        cls.clock.add_iterator(cls.connector)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

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

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

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

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

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

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

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

    async def _test_approve(self):
        uniswap = self.connector
        ret_val = await uniswap.approve_uniswap_spender("DAI")
        print(ret_val)

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

    async def _test_get_quote_price(self):
        uniswap = self.connector
        buy_price = await uniswap.get_quote_price(trading_pair, True,
                                                  Decimal("1"))
        self.assertTrue(buy_price > 0)
        print(f"buy_price: {buy_price}")
        sell_price = await uniswap.get_quote_price(trading_pair, False,
                                                   Decimal("1"))
        self.assertTrue(sell_price > 0)
        print(f"sell_price: {sell_price}")
        self.assertTrue(buy_price != sell_price)
        # try to get price for non existing pair, this should return None
        # sell_price = await uniswap.get_quote_price("AAA-BBB", False, Decimal("1"))
        # self.assertTrue(sell_price is None)

    def test_buy(self):
        uniswap = self.connector
        amount = Decimal("0.1")
        price = Decimal("20")
        order_id = uniswap.buy(trading_pair, amount, OrderType.LIMIT, price)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(BuyOrderCompletedEvent))
        self.assertTrue(event.order_id is not None)
        self.assertEqual(order_id, event.order_id)
        self.assertEqual(event.base_asset_amount, amount)
        print(event.order_id)

    def test_sell(self):
        uniswap = self.connector
        amount = Decimal("0.1")
        price = Decimal("1")
        order_id = uniswap.sell(trading_pair, amount, OrderType.LIMIT, price)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(SellOrderCompletedEvent))
        self.assertTrue(event.order_id is not None)
        self.assertEqual(order_id, event.order_id)
        self.assertEqual(event.base_asset_amount, amount)
        print(event.order_id)

    def test_sell_failure(self):
        uniswap = self.connector
        # Since we don't have 1000 WETH, this should trigger order failure
        amount = Decimal("100")
        price = Decimal("1")
        order_id = uniswap.sell(trading_pair, amount, OrderType.LIMIT, price)
        event = self.ev_loop.run_until_complete(
            self.event_logger.wait_for(MarketOrderFailureEvent))
        self.assertEqual(order_id, event.order_id)

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

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

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

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

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

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

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

        finally:
            recorder.stop()
            os.unlink(self.db_path)
class EterbaseExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.OrderCancelled,
        MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure
    ]

    market: EterbaseExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: EterbaseExchange = EterbaseExchange(
            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: EterbaseExchange = EterbaseExchange(
                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)
Ejemplo n.º 7
0
class BambooRelayMarketUncoordinatedUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderExpired,
        MarketEvent.OrderFilled,
    ]

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

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

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

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

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

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

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

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

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

    def test_get_fee(self):
        maker_buy_trade_fee: TradeFee = self.market.get_fee(
            conf.test_bamboo_relay_base_token_symbol,
            conf.test_bamboo_relay_quote_token_symbol, 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), 1)
        taker_buy_trade_fee: TradeFee = self.market.get_fee(
            conf.test_bamboo_relay_base_token_symbol,
            conf.test_bamboo_relay_quote_token_symbol, 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[self.quote_token_asset]),
                                s_decimal_0)

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

        # Reset the logs
        self.market_logger.clear()

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

        # Reset the logs
        self.market_logger.clear()

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

        [cancellation_results, order_cancelled_event] = self.run_parallel(
            self.market.cancel_all(60 * 3),
            self.market_logger.wait_for(OrderCancelledEvent))
        is_buy_cancelled = False
        is_sell_cancelled = False
        for cancellation_result in cancellation_results:
            if cancellation_result == CancellationResult(buy_order_id, True):
                is_buy_cancelled = True
            if cancellation_result == CancellationResult(sell_order_id, True):
                is_sell_cancelled = True
        self.assertEqual(is_buy_cancelled, True)
        self.assertEqual(is_sell_cancelled, 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_expire(self):
        trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset
        current_price: Decimal = self.market.get_price(trading_pair, True)
        amount = Decimal("0.003")
        expires = int(time.time() + 60)  # expires in 1 min
        self.market.buy(trading_pair=trading_pair,
                        amount=amount,
                        order_type=OrderType.LIMIT,
                        price=current_price - Decimal("0.2") * current_price,
                        expiration_ts=expires)

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

        # Reset the logs
        self.market_logger.clear()

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

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

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

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

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

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

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

        self.market_logger.clear()

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

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

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

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

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

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

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

        self.market_logger.clear()

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

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

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

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

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

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

        try:
            self.assertEqual(0,
                             len(self.market.tracking_states["limit_orders"]))

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

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

            expires = int(time.time() + 60 * 3)
            order_id = self.market.buy(trading_pair,
                                       quantized_amount,
                                       OrderType.LIMIT,
                                       quantize_bid_price,
                                       expiration_ts=expires)
            [order_created_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event
            self.assertEqual(order_id, order_created_event.order_id)

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

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

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

            # Close out the current market and start another market.
            self.clock.remove_iterator(self.market)
            for event_tag in self.market_events:
                self.market.remove_listener(event_tag, self.market_logger)
            self.market: BambooRelayMarket = BambooRelayMarket(
                wallet=self.wallet,
                ethereum_rpc_url=conf.test_web3_provider_list[0],
                trading_pairs=[
                    self.base_token_asset + "-" + self.quote_token_asset
                ],
                use_coordinator=False,
                pre_emptive_soft_cancels=False)
            for event_tag in self.market_events:
                self.market.add_listener(event_tag, self.market_logger)
            recorder.stop()
            recorder = MarketsRecorder(sql, [self.market], config_path,
                                       strategy_name)
            recorder.start()
            saved_market_states = recorder.get_market_states(
                config_path, self.market)
            self.clock.add_iterator(self.market)
            self.assertEqual(0, len(self.market.limit_orders))
            self.assertEqual(0,
                             len(self.market.tracking_states["limit_orders"]))
            self.market.restore_tracking_states(
                saved_market_states.saved_state)
            self.assertEqual(1, len(self.market.limit_orders))
            self.assertEqual(1,
                             len(self.market.tracking_states["limit_orders"]))

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

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

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

        try:
            # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event.
            current_price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal("0.005") / current_price
            order_id = self.market.buy(trading_pair, amount)
            [buy_order_completed_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCompletedEvent))

            # Reset the logs
            self.market_logger.clear()

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

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

            order_id = None

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

            recorder.stop()
            os.unlink(self.db_path)
Ejemplo n.º 8
0
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)
Ejemplo n.º 9
0
class RadarRelayOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[RadarRelayOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["USDC-DAI", "WETH-DAI", "USDC-WETH"]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: RadarRelayOrderBookTracker = RadarRelayOrderBookTracker(
            trading_pairs=cls.trading_pairs)
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(
            cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

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

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

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

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

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

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        weth_dai_book: OrderBook = order_books["WETH-DAI"]
        usdc_dai_book: OrderBook = order_books["USDC-DAI"]
        # print(weth_dai_book.snapshot)
        # print(zrx_weth_book.snapshot)
        self.assertGreaterEqual(
            weth_dai_book.get_price_for_volume(True, 10).result_price,
            weth_dai_book.get_price(True))
        self.assertLessEqual(
            weth_dai_book.get_price_for_volume(False, 10).result_price,
            weth_dai_book.get_price(False))
        self.assertGreaterEqual(
            usdc_dai_book.get_price_for_volume(True, 10).result_price,
            usdc_dai_book.get_price(True))
        self.assertLessEqual(
            usdc_dai_book.get_price_for_volume(False, 10).result_price,
            usdc_dai_book.get_price(False))
Ejemplo n.º 10
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 = 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):
        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_get_available_balances(self):
        balance = self.market.get_available_balance("ETH")
        self.assertGreaterEqual(balance, 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)
Ejemplo n.º 11
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"]), 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)
Ejemplo n.º 12
0
class BittrexExchangeUnitTest(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: BittrexExchange
    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, [])
            cls.web_app.start()
            cls.ev_loop.run_until_complete(cls.web_app.wait_til_started())
            cls._patcher = mock.patch("aiohttp.client.URL")
            cls._url_mock = cls._patcher.start()
            cls._url_mock.side_effect = cls.web_app.reroute_local
            cls.web_app.update_response("get", API_BASE_URL, "/v3/ping",
                                        FixtureBittrex.PING)
            cls.web_app.update_response("get", API_BASE_URL, "/v3/markets",
                                        FixtureBittrex.MARKETS)
            cls.web_app.update_response("get", API_BASE_URL,
                                        "/v3/markets/tickers",
                                        FixtureBittrex.MARKETS_TICKERS)
            cls.web_app.update_response("get", API_BASE_URL, "/v3/balances",
                                        FixtureBittrex.BALANCES)
            cls.web_app.update_response("get", API_BASE_URL, "/v3/orders/open",
                                        FixtureBittrex.ORDERS_OPEN)
            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.bittrex.bittrex_exchange.get_tracking_nonce"
            )
            cls._t_nonce_mock = cls._t_nonce_patcher.start()

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

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

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

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

        print("Initializing Bittrex market... this will take about a minute. ")
        cls.clock.add_iterator(cls.market)
        cls.stack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

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

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

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

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

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

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

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

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

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

    def place_order(self, is_buy, trading_pair, amount, order_type, price,
                    nonce, post_resp, ws_resp):
        global EXCHANGE_ORDER_ID
        order_id, exch_order_id = None, None
        if API_MOCK_ENABLED:
            exch_order_id = f"BITTREX_{EXCHANGE_ORDER_ID}"
            EXCHANGE_ORDER_ID += 1
            self._t_nonce_mock.return_value = nonce
            resp = post_resp.copy()
            resp["id"] = exch_order_id
            side = 'buy' if is_buy else 'sell'
            resp["direction"] = side.upper()
            resp["type"] = order_type.name.upper()
            if order_type == OrderType.LIMIT:
                del resp["limit"]
            self.web_app.update_response("post", API_BASE_URL, "/v3/orders",
                                         resp)
        if is_buy:
            order_id = self.market.buy(trading_pair, amount, order_type, price)
        else:
            order_id = self.market.sell(trading_pair, amount, order_type,
                                        price)
        if API_MOCK_ENABLED:
            resp = ws_resp.copy()
            resp["content"]["o"]["OU"] = exch_order_id
            MockWebSocketServerFactory.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.CANCEL_ORDER.copy()
            resp["id"] = exch_order_id
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id}", resp)
        self.market.cancel(trading_pair, order_id)

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

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

        self.market_logger.clear()

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

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

    def test_limit_makers_unfilled(self):
        self.assertGreater(self.market.get_balance("USDT"), 20)
        trading_pair = "ETH-USDT"
        current_bid_price: Decimal = self.market.get_price(
            trading_pair, True) * Decimal('0.80')
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_bid_price)
        bid_amount: Decimal = Decimal('0.06')
        quantized_bid_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, bid_amount)

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

        order_id, exch_order_id_1 = self.place_order(
            True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureBittrex.FILLED_BUY_LIMIT_ORDER,
            FixtureBittrex.WS_AFTER_BUY_2)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id, order_created_event.order_id)

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

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

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

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

        order_id, _ = self.place_order(True, trading_pair, quantized_amount,
                                       OrderType.LIMIT, price, 10001,
                                       FixtureBittrex.FILLED_BUY_LIMIT_ORDER,
                                       FixtureBittrex.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.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDT", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, BuyOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

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

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

        order_id, _ = self.place_order(False, trading_pair, quantized_amount,
                                       OrderType.LIMIT, price, 10001,
                                       FixtureBittrex.FILLED_BUY_LIMIT_ORDER,
                                       FixtureBittrex.WS_AFTER_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 for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("USDT", order_completed_event.quote_asset)
        self.assertAlmostEqual(base_amount_traded,
                               order_completed_event.base_asset_amount)
        self.assertAlmostEqual(quote_amount_traded,
                               order_completed_event.quote_asset_amount)
        self.assertTrue(
            any([
                isinstance(event, SellOrderCreatedEvent)
                and event.order_id == order_id
                for event in self.market_logger.event_log
            ]))
        # Reset the logs
        self.market_logger.clear()

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

        current_bid_price: Decimal = self.market.get_price(
            trading_pair, True) * Decimal('0.80')
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, current_bid_price)

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

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

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

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

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

        _, exch_order_id_1 = self.place_order(
            True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER,
            quantize_bid_price, 10001, FixtureBittrex.OPEN_BUY_LIMIT_ORDER,
            FixtureBittrex.WS_AFTER_BUY_1)
        _, exch_order_id_2 = self.place_order(
            False, trading_pair, quantized_ask_amount, OrderType.LIMIT_MAKER,
            quantize_ask_price, 10002, FixtureBittrex.OPEN_BUY_LIMIT_ORDER,
            FixtureBittrex.WS_AFTER_BUY_1)
        self.run_parallel(asyncio.sleep(1))
        if API_MOCK_ENABLED:
            resp = FixtureBittrex.CANCEL_ORDER.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.CANCEL_ORDER.copy()
            resp["id"] = exch_order_id_2
            self.web_app.update_response("delete", API_BASE_URL,
                                         f"/v3/orders/{exch_order_id_2}", resp)
        [cancellation_results] = self.run_parallel(self.market.cancel_all(5))
        for cr in cancellation_results:
            self.assertEqual(cr.success, True)

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

        try:
            self.assertEqual(0, len(self.market.tracking_states))
            current_bid_price: Decimal = self.market.get_price(
                trading_pair, True) * Decimal('0.80')
            quantize_bid_price: Decimal = self.market.quantize_order_price(
                trading_pair, current_bid_price)
            bid_amount: Decimal = Decimal('0.06')
            quantized_bid_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, bid_amount)

            order_id, exch_order_id = self.place_order(
                True, trading_pair, quantized_bid_amount, OrderType.LIMIT,
                quantize_bid_price, 10001, FixtureBittrex.OPEN_BUY_LIMIT_ORDER,
                FixtureBittrex.WS_AFTER_BUY_1)
            [order_created_event] = self.run_parallel(
                self.market_logger.wait_for(BuyOrderCreatedEvent))
            order_created_event: BuyOrderCreatedEvent = order_created_event
            self.assertEqual(order_id, order_created_event.order_id)

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

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

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

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

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

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

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

        try:

            price: Decimal = self.market.get_price(trading_pair, True)
            amount: Decimal = Decimal("0.06")
            quantized_amount: Decimal = self.market.quantize_order_amount(
                trading_pair, amount)
            order_id, _ = self.place_order(
                True, trading_pair, quantized_amount, OrderType.LIMIT, price,
                10001, FixtureBittrex.FILLED_BUY_LIMIT_ORDER,
                FixtureBittrex.WS_AFTER_BUY_2)
            [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)
            price: Decimal = self.market.get_price(trading_pair, False)
            order_id, _ = self.place_order(
                False, trading_pair, amount, OrderType.LIMIT, price, 10001,
                FixtureBittrex.FILLED_BUY_LIMIT_ORDER,
                FixtureBittrex.WS_AFTER_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.market.cancel(trading_pair, order_id)
                self.run_parallel(
                    self.market_logger.wait_for(OrderCancelledEvent))

            recorder.stop()
            os.unlink(self.db_path)
Ejemplo n.º 13
0
class CryptoComExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled, MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled, MarketEvent.OrderFailure
    ]
    connector: CryptoComExchange
    event_logger: EventLogger
    trading_pair = "BTC-USDT"
    base_token, quote_token = trading_pair.split("-")
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.ev_loop = asyncio.get_event_loop()

        if API_MOCK_ENABLED:
            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: CryptoComExchange = CryptoComExchange(
            crypto_com_api_key=API_KEY,
            crypto_com_api_secret=API_SECRET,
            trading_pairs=[cls.trading_pair],
            trading_required=True)
        print(
            "Initializing CryptoCom 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:
            os.unlink(self.db_path)
        except FileNotFoundError:
            pass

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

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

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

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

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

    def _place_order(self,
                     is_buy,
                     amount,
                     order_type,
                     price,
                     ex_order_id,
                     get_order_fixture=None,
                     ws_trade_fixture=None,
                     ws_order_fixture=None) -> str:
        if 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_CANCELLED.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):
        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.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._mock_ws_bal_update(self.quote_token, expected_quote_bal)
        self.ev_loop.run_until_complete(asyncio.sleep(1))
        self.assertAlmostEqual(
            expected_quote_bal,
            self.connector.get_available_balance(self.quote_token))

        # Reset the logs
        self.event_logger.clear()

        # Try to sell back the same amount to the exchange, and watch for completion event.
        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("0.95")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("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)
        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.0001"))
        quote_bal = self.connector.get_available_balance(self.quote_token)

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

        price = self.connector.get_price(self.trading_pair,
                                         True) * Decimal("1.2")
        price = self.connector.quantize_order_price(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair,
                                                      Decimal("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_CANCELLED)
        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_CANCELLED)
        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_CANCELLED.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_CANCELLED.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 = CryptoComExchange(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()
            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()
            os.unlink(self.db_path)
Ejemplo n.º 14
0
class CoinbaseProOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[CoinbaseProOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["BTC-USD"]

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

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

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

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

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

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

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        test_order_book: OrderBook = order_books["BTC-USD"]
        # print("test_order_book")
        # print(test_order_book.snapshot)
        self.assertGreaterEqual(
            test_order_book.get_price_for_volume(True, 10).result_price,
            test_order_book.get_price(True))
        self.assertLessEqual(
            test_order_book.get_price_for_volume(False, 10).result_price,
            test_order_book.get_price(False))

        test_active_order_tracker = self.order_book_tracker._active_order_trackers[
            "BTC-USD"]
        self.assertTrue(len(test_active_order_tracker.active_asks) > 0)
        self.assertTrue(len(test_active_order_tracker.active_bids) > 0)

    def test_order_book_data_source(self):
        self.assertTrue(
            isinstance(self.order_book_tracker.data_source,
                       OrderBookTrackerDataSource))

    def test_get_active_exchange_markets(self):
        [active_markets_df] = self.run_parallel(
            self.order_book_tracker.data_source.get_active_exchange_markets())
        # print(active_markets_df)
        self.assertGreater(active_markets_df.size, 0)
        self.assertTrue("baseAsset" in active_markets_df)
        self.assertTrue("quoteAsset" in active_markets_df)
        self.assertTrue("USDVolume" in active_markets_df)

    def test_get_trading_pairs(self):
        [trading_pairs] = self.run_parallel(
            self.order_book_tracker.data_source.get_trading_pairs())
        # print(trading_pairs)
        self.assertEqual(len(trading_pairs), len(self.trading_pairs))

    def test_diff_msg_get_added_to_order_book(self):
        test_active_order_tracker = self.order_book_tracker._active_order_trackers[
            "BTC-USD"]

        price = "200"
        order_id = "test_order_id"
        product_id = "BTC-USD"
        remaining_size = "1.00"

        # Test open message diff
        raw_open_message = {
            "type": "open",
            "time": datetime.now().isoformat(),
            "product_id": product_id,
            "sequence": 20000000000,
            "order_id": order_id,
            "price": price,
            "remaining_size": remaining_size,
            "side": "buy"
        }
        open_message = CoinbaseProOrderBook.diff_message_from_exchange(
            raw_open_message)
        self.order_book_tracker._order_book_diff_stream.put_nowait(
            open_message)
        self.run_parallel(asyncio.sleep(5))

        test_order_book_row = test_active_order_tracker.active_bids[Decimal(
            price)]
        self.assertEqual(test_order_book_row[order_id]["remaining_size"],
                         remaining_size)

        # Test change message diff
        new_size = "2.00"
        raw_change_message = {
            "type": "change",
            "time": datetime.now().isoformat(),
            "product_id": product_id,
            "sequence": 20000000001,
            "order_id": order_id,
            "price": price,
            "new_size": new_size,
            "old_size": remaining_size,
            "side": "buy",
        }
        change_message = CoinbaseProOrderBook.diff_message_from_exchange(
            raw_change_message)
        self.order_book_tracker._order_book_diff_stream.put_nowait(
            change_message)
        self.run_parallel(asyncio.sleep(5))

        test_order_book_row = test_active_order_tracker.active_bids[Decimal(
            price)]
        self.assertEqual(test_order_book_row[order_id]["remaining_size"],
                         new_size)

        # Test match message diff
        match_size = "0.50"
        raw_match_message = {
            "type": "match",
            "trade_id": 10,
            "sequence": 20000000002,
            "maker_order_id": order_id,
            "taker_order_id": "test_order_id_2",
            "time": datetime.now().isoformat(),
            "product_id": "BTC-USD",
            "size": match_size,
            "price": price,
            "side": "buy"
        }
        match_message = CoinbaseProOrderBook.diff_message_from_exchange(
            raw_match_message)
        self.order_book_tracker._order_book_diff_stream.put_nowait(
            match_message)
        self.run_parallel(asyncio.sleep(5))

        test_order_book_row = test_active_order_tracker.active_bids[Decimal(
            price)]
        self.assertEqual(
            Decimal(test_order_book_row[order_id]["remaining_size"]),
            Decimal(new_size) - Decimal(match_size))

        # Test done message diff
        raw_done_message = {
            "type": "done",
            "time": datetime.now().isoformat(),
            "product_id": "BTC-USD",
            "sequence": 20000000003,
            "price": price,
            "order_id": order_id,
            "reason": "filled",
            "remaining_size": 0,
            "side": "buy",
        }
        done_message = CoinbaseProOrderBook.diff_message_from_exchange(
            raw_done_message)
        self.order_book_tracker._order_book_diff_stream.put_nowait(
            done_message)
        self.run_parallel(asyncio.sleep(5))

        test_order_book_row = test_active_order_tracker.active_bids[Decimal(
            price)]
        self.assertTrue(order_id not in test_order_book_row)
Ejemplo n.º 15
0
class BitcoinComMarketUnitTest(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: BitcoinComMarket
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: BitcoinComMarket = BitcoinComMarket(
            bitcoin_com_api_key=conf.bitcoin_com_api_key,
            bitcoin_com_secret_key=conf.bitcoin_com_secret_key,
            trading_pairs=["ETHBTC", "LTCBTC"])
        print(
            "Initializing BitcoinCom 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__, "../bitcoin_com_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", "BTC",
                                                  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", "BTC",
                                                   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"), Decimal("0.01"))
        trading_pair = "ETHBTC"
        amount: Decimal = Decimal("0.0001")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        current_bid_price: Decimal = self.market.get_price(trading_pair, True)
        bid_price: Decimal = current_bid_price + Decimal(
            "0.05") * current_bid_price
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)

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

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("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):
        trading_pair = "ETHBTC"
        amount: Decimal = Decimal("0.0001")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

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

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

        self.assertTrue(
            [evt.order_type == OrderType.LIMIT for evt in trade_events])
        self.assertEqual(order_id, order_completed_event.order_id)
        self.assertAlmostEqual(quantized_amount,
                               order_completed_event.base_asset_amount)
        self.assertEqual("ETH", order_completed_event.base_asset)
        self.assertEqual("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_market_buy(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.01"))
        trading_pair = "ETHBTC"
        amount: Decimal = Decimal("0.0001")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id = self.market.buy(trading_pair, quantized_amount,
                                   OrderType.MARKET, Decimal(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("ETH", 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):
        trading_pair = "ETHBTC"
        amount: Decimal = Decimal("0.0001")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)

        order_id = self.market.sell(trading_pair, amount, OrderType.MARKET,
                                    Decimal(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("ETH", 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):
        trading_pair = "ETHBTC"

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

        bid_price: Decimal = current_bid_price * Decimal("0.5")
        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 = "ETHBTC"
        # Intentionally setting invalid price to prevent getting filled
        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")
        amount: Decimal = Decimal("0.0001")
        quantized_amount: Decimal = self.market.quantize_order_amount(
            trading_pair, amount)
        quantize_bid_price: Decimal = self.market.quantize_order_price(
            trading_pair, bid_price)
        quantize_ask_price: Decimal = self.market.quantize_order_price(
            trading_pair, ask_price)

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

    @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv),
                         "List order test requires manual action.")
    def test_list_orders(self):
        self.assertGreater(self.market.get_balance("ETH"), Decimal("0.01"))
        trading_pair = "ETHBTC"
        amount: Decimal = Decimal("0.0001")
        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.8")
        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):
        wallet = conf.test_erc20_token_address
        currency = "ETH"
        withdraw_amount = Decimal('0.01')

        # Ensure the market account has enough balance for withdraw testing.
        # this is checked by 'self.market.withdraw'
        # self.assertGreaterEqual(required_balance, withdraw_amount)

        # Withdraw ETH from BitcoinCom to test wallet.
        self.market.withdraw(wallet, currency, withdraw_amount)
        [withdraw_asset_event] = self.run_parallel(
            self.market_logger.wait_for(MarketWithdrawAssetEvent))
        withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event
        self.assertEqual(wallet, withdraw_asset_event.to_address)
        self.assertEqual(currency, withdraw_asset_event.asset_name)
        self.assertEqual(withdraw_amount, withdraw_asset_event.amount)

    def test_orders_saving_and_restoration(self):
        config_path: str = "test_config"
        strategy_name: str = "test_strategy"
        trading_pair: str = "ETHBTC"
        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.0001 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.0001")
            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: BitcoinComMarket = BitcoinComMarket(
                bitcoin_com_api_key=conf.bitcoin_com_api_key,
                bitcoin_com_secret_key=conf.bitcoin_com_secret_key,
                trading_pairs=["ETHBTC", "LTCBTC"])
            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 = "ETHBTC"
        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.0001 ETH from the exchange, and watch for completion event.
            amount: Decimal = Decimal("0.0001")
            order_id = self.market.buy(trading_pair, amount, OrderType.MARKET)
            [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.market.sell(trading_pair, amount, OrderType.MARKET)
            [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)
Ejemplo n.º 16
0
class BeaxyExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.ReceivedAsset,
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.WithdrawAsset,
        MarketEvent.OrderFilled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure,
    ]
    market: BeaxyExchange
    market_logger: EventLogger
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):

        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()

        if API_MOCK_ENABLED:

            cls.web_app = MockWebServer.get_instance()
            cls.web_app.add_host_to_mock(PRIVET_API_BASE_URL, [])
            cls.web_app.add_host_to_mock(PUBLIC_API_BASE_URL, [])
            cls.web_app.start()
            cls.ev_loop.run_until_complete(cls.web_app.wait_til_started())
            cls._patcher = mock.patch("aiohttp.client.URL")
            cls._url_mock = cls._patcher.start()
            cls._url_mock.side_effect = cls.web_app.reroute_local
            cls.web_app.update_response("get", PUBLIC_API_BASE_URL,
                                        "/api/v2/symbols",
                                        FixtureBeaxy.SYMBOLS)
            cls.web_app.update_response("get", PUBLIC_API_BASE_URL,
                                        "/api/v2/symbols/DASHBTC/book",
                                        FixtureBeaxy.TRADE_BOOK)
            cls.web_app.update_response("get", PUBLIC_API_BASE_URL,
                                        "/api/v2/symbols/DASHBTC/rate",
                                        FixtureBeaxy.EXCHANGE_RATE)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/health", FixtureBeaxy.HEALTH)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/wallets",
                                        FixtureBeaxy.BALANCES)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/tradingsettings",
                                        FixtureBeaxy.TRADE_SETTINGS)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/orders/open",
                                        FixtureBeaxy.ORDERS_OPEN_EMPTY)
            cls.web_app.update_response("get", PRIVET_API_BASE_URL,
                                        "/api/v2/orders/closed",
                                        FixtureBeaxy.ORDERS_CLOSED_EMPTY)

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

            MockWebSocketServerFactory.url_host_only = True
            MockWebSocketServerFactory.start_new_server(
                BeaxyConstants.TradingApi.WS_BASE_URL)
            MockWebSocketServerFactory.start_new_server(
                BeaxyConstants.PublicApi.WS_BASE_URL)

            cls._ws_patcher = unittest.mock.patch("websockets.connect",
                                                  autospec=True)
            cls._ws_mock = cls._ws_patcher.start()
            cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect

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

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

        if API_MOCK_ENABLED:

            async def mock_status_polling_task():
                pass

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

        print("Initializing Beaxy market... this will take about a minute.")
        cls.clock.add_iterator(cls.market)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

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

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

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

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

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

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

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

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

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

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

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

    def test_limit_buy(self):

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

        amount: Decimal = Decimal("0.01")

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

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

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

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

    def test_limit_sell(self):

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

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

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

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

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

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

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

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

        self.market_logger.clear()

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

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

    def test_limit_makers_unfilled(self):

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

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

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

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

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

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

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

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

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

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

    def test_market_buy(self):

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

        amount: Decimal = Decimal("0.01")

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

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

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

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

    def test_market_sell(self):

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

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

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

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

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

    def test_cancel_order(self):

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

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

        amount: Decimal = Decimal("0.01")

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

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

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

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

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

    def test_cancel_all(self):

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

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

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

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

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

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

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

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

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

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

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

    def test_cancel_empty(self):
        trading_pair = "DASH-BTC"
        self.cancel_order(trading_pair, '123', '123')
Ejemplo n.º 17
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)
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.001')
        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.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,
                    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.04")
        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.04")
        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.04")
        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.04")
        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.04")

        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.05")
        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.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,
                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.04")
            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)
Ejemplo n.º 19
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: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10)
        self.assertGreater(limit_fee.percent, 0)
        self.assertEqual(len(limit_fee.flat_fees), 0)
        market_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1)
        self.assertGreater(market_fee.percent, 0)
        self.assertEqual(len(market_fee.flat_fees), 0)
        sell_trade_fee: AddedToCostTradeFee = self.market.get_fee(
            "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10)
        self.assertGreater(sell_trade_fee.percent, 0)
        self.assertEqual(len(sell_trade_fee.flat_fees), 0)

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

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

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

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

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

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

        self.market_logger.clear()

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

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

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

        order_id, _ = self.place_order(True, trading_pair,
                                       quantized_bid_amount,
                                       OrderType.LIMIT_MAKER,
                                       quantized_bid_price, 10001,
                                       FixtureKucoin.ORDER_PLACE,
                                       FixtureKucoin.ORDER_GET_BUY_UNMATCHED)
        [order_created_event] = self.run_parallel(
            self.market_logger.wait_for(BuyOrderCreatedEvent))
        order_created_event: BuyOrderCreatedEvent = order_created_event
        self.assertEqual(order_id, order_created_event.order_id)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            # Reset the logs
            self.market_logger.clear()

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

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

            order_id = None

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

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

    def test_update_last_prices(self):
        # This is basic test to see if order_book last_trade_price is initiated and updated.
        for order_book in self.market.order_books.values():
            for _ in range(5):
                self.ev_loop.run_until_complete(asyncio.sleep(1))
                self.assertFalse(math.isnan(order_book.last_trade_price))
Ejemplo n.º 20
0
class BittrexOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[BittrexOrderBookTracker] = None
    events: List[OrderBookEvent] = [
        OrderBookEvent.TradeEvent
    ]

    # TODO: Update trading pair format to V3 WebSocket API
    trading_pairs: List[str] = [  # Trading Pair in v1.1 format(Quote-Base)
        "LTC-BTC",
        "LTC-ETH"
    ]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: BittrexOrderBookTracker = BittrexOrderBookTracker(trading_pairs=cls.trading_pairs)
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

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

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

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

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

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

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        ltcbtc_book: OrderBook = order_books["LTC-BTC"]
        # print(ltcbtc_book)
        self.assertGreaterEqual(ltcbtc_book.get_price_for_volume(True, 10).result_price,
                                ltcbtc_book.get_price(True))
        self.assertLessEqual(ltcbtc_book.get_price_for_volume(False, 10).result_price,
                             ltcbtc_book.get_price(False))
Ejemplo n.º 21
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.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 = 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(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)
Ejemplo n.º 22
0
class DolomiteExchangeUnitTest(unittest.TestCase):
    market_events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
    ]

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

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

    @classmethod
    def setUpClass(cls):
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.wallet = Web3Wallet(
            private_key=conf.dolomite_test_web3_private_key,
            backend_urls=conf.test_web3_provider_list,
            erc20_token_addresses=[conf.dolomite_test_web3_address],
            chain=EthereumChain.MAIN_NET,
        )
        cls.market: DolomiteExchange = DolomiteExchange(
            wallet=cls.wallet,
            ethereum_rpc_url=conf.test_web3_provider_list[0],
            order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.
            EXCHANGE_API,
            isTestNet=True,
            trading_pairs=["WETH-DAI"],
        )
        print("Initializing Dolomite market... ")
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.clock.add_iterator(cls.wallet)
        cls.clock.add_iterator(cls.market)
        cls.stack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Intentionally setting invalid price to prevent getting filled
        client_order_id = self.market.buy(trading_pair, amount,
                                          OrderType.LIMIT, bid_price * 0.7)
        self.run_parallel(asyncio.sleep(1.0))
        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.run_parallel(asyncio.sleep(6.0))
        self.assertEqual(0, len(self.market.limit_orders))
        self.assertEqual(client_order_id, order_cancelled_event.order_id)

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

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

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

    @unittest.skipUnless(
        any("test_place_market_buy_and_sell" in arg for arg in sys.argv),
        "test_place_market_buy_and_sell test requires manual action.",
    )
    def test_place_market_buy_and_sell(self):
        # Cannot trade between yourself on Dolomite. Testing this is... hard.
        # These orders use the same code as limit orders except for fee calculation
        # and setting a field in the http request to "MARKET" instead of "LIMIT".
        # Fee calculations for market orders is tested above
        pass
class LiquidOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[LiquidOrderBookTracker] = None
    events: List[OrderBookEvent] = [
        OrderBookEvent.TradeEvent
    ]

    trading_pairs: List[str] = [
        'ETH-USD',
        'LCX-BTC'
    ]

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

        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

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

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

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

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

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

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        ethusd_book: OrderBook = order_books["ETH-USD"]
        lxcbtc_book: OrderBook = order_books["LCX-BTC"]
        # print("ethusd_book")
        # print(ethusd_book.snapshot)
        # print("lxcbtc_book")
        # print(lxcbtc_book.snapshot)
        self.assertGreaterEqual(ethusd_book.get_price_for_volume(True, 10).result_price,
                                ethusd_book.get_price(True))
        self.assertLessEqual(lxcbtc_book.get_price_for_volume(False, 10).result_price,
                             lxcbtc_book.get_price(False))
        for order_book in self.order_book_tracker.order_books.values():
            print(order_book.last_trade_price)
            self.assertFalse(math.isnan(order_book.last_trade_price))

    def test_api_get_last_traded_prices(self):
        prices = self.ev_loop.run_until_complete(
            LiquidAPIOrderBookDataSource.get_last_traded_prices(["BTC-USD", "ETH-USD"]))
        for key, value in prices.items():
            print(f"{key} last_trade_price: {value}")
        self.assertGreater(prices["BTC-USD"], 1000)
        self.assertLess(prices["ETH-USD"], 1000)
class EterbaseOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[EterbaseOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["ETHEUR"]

    @classmethod
    def setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        cls.order_book_tracker: EterbaseOrderBookTracker = EterbaseOrderBookTracker(
            trading_pairs=cls.trading_pairs)
        cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(
            cls.order_book_tracker.start())
        cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())

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

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

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

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

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

    def test_tracker_integrity(self):
        # Wait 5 seconds to process some diffs.
        self.ev_loop.run_until_complete(asyncio.sleep(5.0))
        order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books
        test_order_book: OrderBook = order_books["ETHEUR"]

        self.assertGreaterEqual(
            test_order_book.get_price_for_volume(True, 10).result_price,
            test_order_book.get_price(True))
        self.assertLessEqual(
            test_order_book.get_price_for_volume(False, 10).result_price,
            test_order_book.get_price(False))

        test_active_order_tracker = self.order_book_tracker._active_order_trackers[
            "ETHEUR"]
        self.assertTrue(len(test_active_order_tracker.active_asks) > 0)
        self.assertTrue(len(test_active_order_tracker.active_bids) > 0)
        for order_book in self.order_book_tracker.order_books.values():
            print(f"last_trade_price: {order_book.last_trade_price}")
            self.assertFalse(math.isnan(order_book.last_trade_price))

    def test_order_book_data_source(self):
        self.assertTrue(
            isinstance(self.order_book_tracker.data_source,
                       OrderBookTrackerDataSource))

    def test_diff_msg_get_added_to_order_book(self):
        test_active_order_tracker = self.order_book_tracker._active_order_trackers[
            "ETHEUR"]

        price = "200"
        order_id = "test_order_id"
        market_id = 51
        size = "1.50"
        remaining_size = "1.00"

        # Test open message diff
        raw_open_message = {
            "type": "o_placed",
            "timestamp": datetime.now().timestamp() * 1000,
            "marketId": market_id,
            "orderId": order_id,
            "limitPrice": price,
            "qty": size,
            "oType": 2,
            "side": 1
        }
        open_message = EterbaseOrderBook.diff_message_from_exchange(
            raw_open_message)
        self.order_book_tracker._order_book_diff_stream.put_nowait(
            open_message)
        self.run_parallel(asyncio.sleep(5))

        test_order_book_row = test_active_order_tracker.active_bids[Decimal(
            price)]
        self.assertEqual(test_order_book_row[order_id]["remaining_size"], size)

        # Test match message diff
        match_size = "0.50"
        raw_match_message = {
            "type": "o_fill",
            "tradeId": 10,
            "orderId": order_id,
            "timestamp": datetime.now().timestamp() * 1000,
            "marketId": market_id,
            "qty": match_size,
            "remainingQty": remaining_size,
            "price": price,
            "side": 1
        }
        match_message = EterbaseOrderBook.diff_message_from_exchange(
            raw_match_message)

        self.order_book_tracker._order_book_diff_stream.put_nowait(
            match_message)
        self.run_parallel(asyncio.sleep(5))

        test_order_book_row = test_active_order_tracker.active_bids[Decimal(
            price)]
        self.assertEqual(
            Decimal(test_order_book_row[order_id]["remaining_size"]),
            Decimal(remaining_size))

        # Test done message diff
        raw_done_message = {
            "type": "o_closed",
            "timestamp": datetime.now().timestamp() * 1000,
            "marketId": market_id,
            "limitPrice": price,
            "orderId": order_id,
            "reason": "FILLED",
            "qty": match_size,
            "remainingQty": "1.00",
            "side": 1
        }
        done_message = EterbaseOrderBook.diff_message_from_exchange(
            raw_done_message)

        self.order_book_tracker._order_book_diff_stream.put_nowait(
            done_message)
        self.run_parallel(asyncio.sleep(5))

        self.assertTrue(
            Decimal(price) not in test_active_order_tracker.active_bids)

    def test_api_get_last_traded_prices(self):
        prices = self.ev_loop.run_until_complete(
            EterbaseAPIOrderBookDataSource.get_last_traded_prices(
                ["BTCEUR", "LTCEUR"]))
        for key, value in prices.items():
            print(f"{key} last_trade_price: {value}")
        self.assertGreater(prices["BTCEUR"], 1000)
        self.assertLess(prices["LTCEUR"], 1000)
Ejemplo n.º 25
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 setUpClass(cls):
        cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
        if MOCK_API_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
            # mock_account_id = FixtureOKEx.GET_ACCOUNTS["data"][0]["id"]

            # warning: second parameter starts with /
            cls.web_app.update_response("get", API_BASE_URL,
                                        '/' + OKEX_SERVER_TIME,
                                        FixtureOKEx.TIMESTAMP)

            cls.web_app.update_response("get", API_BASE_URL,
                                        '/' + OKEX_INSTRUMENTS_URL,
                                        FixtureOKEx.OKEX_INSTRUMENTS_URL)
            cls.web_app.update_response("get", API_BASE_URL,
                                        '/api/spot/v3/instruments/ticker',
                                        FixtureOKEx.INSTRUMENT_TICKER)

            cls.web_app.update_response(
                "get", API_BASE_URL, '/api/spot/v3/instruments/ETH-USDT/book',
                FixtureOKEx.OKEX_ORDER_BOOK)

            cls.web_app.update_response("get", API_BASE_URL,
                                        '/' + OKEX_BALANCE_URL,
                                        FixtureOKEx.OKEX_BALANCE_URL)

            # 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
            # fail = str(cls.market.status_dict)
            # raise ValueError(fail)
            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: 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 test_fee_overrides_config(self):
        fee_overrides_config_map["okex_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.0015"), taker_fee.percent)
        fee_overrides_config_map["okex_taker_fee"].value = Decimal('0.1')
        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["okex_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["okex_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,
                    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["order_id"] = 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["id"] = exch_order_id
            resp["client_oid"] = order_id
            self.web_app.update_response(
                "get", API_BASE_URL, '/' +
                OKEX_ORDER_DETAILS_URL.format(exchange_order_id=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 MOCK_API_ENABLED:
            resp = FixtureOKEx.ORDER_CANCEL.copy()
            resp["order_id"] = exchange_order_id
            self.web_app.update_response(
                "post", API_BASE_URL,
                '/' + OKEX_ORDER_CANCEL.format(exchange_order_id=order_id),
                resp)
        self.market.cancel(trading_pair, order_id)
        if MOCK_API_ENABLED:
            resp = get_resp.copy()
            resp["order_id"] = exchange_order_id
            resp["client_oid"] = order_id
            self.web_app.update_response(
                "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format(
                    exchange_order_id=exchange_order_id), 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["ETH-USDT"] = [exch_order_id1, exch_order_id2]
            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, 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))

            # 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))
Ejemplo n.º 26
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)
Ejemplo n.º 27
0
class CoinbaseProMarketUnitTest(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: CoinbaseProMarket
    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 = HummingWebApp.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)

            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

            cls._t_nonce_patcher = unittest.mock.patch(
                "hummingbot.connector.exchange.coinbase_pro.coinbase_pro_market.get_tracking_nonce")
            cls._t_nonce_mock = cls._t_nonce_patcher.start()
        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.market: CoinbaseProMarket = CoinbaseProMarket(
            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: TradeFee = 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: TradeFee = 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: TradeFee = 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: 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["coinbase_pro_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.005"), maker_fee.percent)
        fee_overrides_config_map["coinbase_pro_maker_fee"].value = Decimal('0.75')
        maker_fee: TradeFee = 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
            HummingWsServerFactory.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
            HummingWsServerFactory.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
            HummingWsServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1)
            resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.copy()
            resp["order_id"] = exch_order_id_2
            HummingWsServerFactory.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
            HummingWsServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1)
            resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.copy()
            resp["order_id"] = exch_order_id_2
            HummingWsServerFactory.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: CoinbaseProMarket = CoinbaseProMarket(
                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)
Ejemplo n.º 28
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)
Ejemplo n.º 29
0
class CoinzoomExchangeUnitTest(unittest.TestCase):
    events: List[MarketEvent] = [
        MarketEvent.BuyOrderCompleted,
        MarketEvent.SellOrderCompleted,
        MarketEvent.OrderFilled,
        MarketEvent.TransactionFailure,
        MarketEvent.BuyOrderCreated,
        MarketEvent.SellOrderCreated,
        MarketEvent.OrderCancelled,
        MarketEvent.OrderFailure
    ]
    connector: CoinzoomExchange
    event_logger: EventLogger
    trading_pair = "BTC-USD"
    base_token, quote_token = trading_pair.split("-")
    stack: contextlib.ExitStack

    @classmethod
    def setUpClass(cls):
        global MAINNET_RPC_URL

        cls.ev_loop = asyncio.get_event_loop()

        cls.clock: Clock = Clock(ClockMode.REALTIME)
        cls.connector: CoinzoomExchange = CoinzoomExchange(
            coinzoom_api_key=API_KEY,
            coinzoom_secret_key=API_SECRET,
            coinzoom_username=API_USERNAME,
            trading_pairs=[cls.trading_pair],
            trading_required=True
        )
        print("Initializing Coinzoom market... this will take about a minute.")
        cls.clock.add_iterator(cls.connector)
        cls.stack: contextlib.ExitStack = contextlib.ExitStack()
        cls._clock = cls.stack.enter_context(cls.clock)
        cls.ev_loop.run_until_complete(cls.wait_til_ready())
        print("Ready.")

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

    @classmethod
    async def wait_til_ready(cls, connector = None):
        if connector is None:
            connector = cls.connector
        async with timeout(90):
            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, connector=None):
        if connector is None:
            connector = self.connector
        return 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.002"))
        taker_fee = self.connector.estimate_fee_pct(False)
        self.assertAlmostEqual(taker_fee, Decimal("0.0026"))

    def test_buy_and_sell(self):
        price = self.connector.get_price(self.trading_pair, True) * Decimal("1.02")
        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)
        order_completed_event = self.ev_loop.run_until_complete(self.event_logger.wait_for(BuyOrderCompletedEvent))
        self.ev_loop.run_until_complete(asyncio.sleep(5))
        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("USD", 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 str(event.order_id) == str(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.98")
        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)
        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("USD", 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.ev_loop.run_until_complete(asyncio.sleep(5))
        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)
        price_quantum = self.connector.get_order_price_quantum(self.trading_pair, price)
        amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001"))
        self.ev_loop.run_until_complete(asyncio.sleep(1))
        self.ev_loop.run_until_complete(self.connector._update_balances())
        self.ev_loop.run_until_complete(asyncio.sleep(2))
        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
        taker_fee = self.connector.estimate_fee_pct(False)
        quote_amount = (math.ceil(((price * amount) * (Decimal("1") + taker_fee)) / price_quantum) * price_quantum)
        expected_quote_bal = quote_bal - quote_amount
        self.ev_loop.run_until_complete(asyncio.sleep(1))
        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), 5)
        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)
        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.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(15))
        self.ev_loop.run_until_complete(self.event_logger.wait_for(OrderCancelledEvent))
        self.ev_loop.run_until_complete(asyncio.sleep(1))
        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.0005"))
        self.assertGreater(self.connector.get_balance("USD"), 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.000123456"))

        # 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.ev_loop.run_until_complete(self.event_logger.wait_for(OrderCancelledEvent))
        self._cancel_order(cl_order_id_2)
        self.ev_loop.run_until_complete(self.event_logger.wait_for(OrderCancelledEvent))

    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)
            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)
            # Clear the event loop
            self.event_logger.clear()
            new_connector = CoinzoomExchange(API_KEY, API_SECRET, API_USERNAME, [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, new_connector)
            self.ev_loop.run_until_complete(self.event_logger.wait_for(OrderCancelledEvent))
            recorder.save_market_states(config_path, new_connector)
            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.0001"))

            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.0001"))
            order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2)
            self.ev_loop.run_until_complete(self.event_logger.wait_for(SellOrderCompletedEvent))
            self.ev_loop.run_until_complete(asyncio.sleep(1))

            # Query the persisted trade logs
            trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path)
            self.assertGreaterEqual(len(trade_fills), 2)
            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 LoopringOrderBookTrackerUnitTest(unittest.TestCase):
    order_book_tracker: Optional[LoopringOrderBookTracker] = None
    events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent]
    trading_pairs: List[str] = ["ETH-USDT", "LRC-ETH"]

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

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

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

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

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

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

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