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