class CoinbaseProExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: CoinbaseProExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() trading_pair = "ETH-USDC" if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock( API_BASE_URL, ["/time", "/products", f"/products/{trading_pair}/book"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/accounts", FixtureCoinbasePro.BALANCES) cls.web_app.update_response("get", API_BASE_URL, "/fees", FixtureCoinbasePro.TRADE_FEES) cls.web_app.update_response("get", API_BASE_URL, "/orders", FixtureCoinbasePro.ORDERS_STATUS) MockWebSocketServerFactory.start_new_server(WS_BASE_URL) cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.coinbase_pro.coinbase_pro_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: CoinbaseProExchange = CoinbaseProExchange( API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=[trading_pair]) print( "Initializing Coinbase Pro market... this will take about a minute." ) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath( join(__file__, "../coinbase_pro_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDC", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["coinbase_pro_taker_fee"].value = None taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), taker_fee.percent) fee_overrides_config_map["coinbase_pro_taker_fee"].value = Decimal( '0.2') taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["coinbase_pro_maker_fee"].value = None maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) fee_overrides_config_map["coinbase_pro_maker_fee"].value = Decimal( '0.75') maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0075"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, fixture_resp, fixture_ws): order_id, exch_order_id = None, None if API_MOCK_ENABLED: self._t_nonce_mock.return_value = nonce side = 'buy' if is_buy else 'sell' resp = fixture_resp.copy() exch_order_id = resp["id"] resp["side"] = side self.web_app.update_response("post", API_BASE_URL, "/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = fixture_ws.copy() resp["order_id"] = exch_order_id resp["side"] = side MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exchange_order_id, fixture_ws): if API_MOCK_ENABLED: self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exchange_order_id}", exchange_order_id) self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = fixture_ws.copy() resp["order_id"] = exchange_order_id MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDC" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.02")) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.02")) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDC" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) order_id_2, exch_order_id_2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureCoinbasePro.OPEN_SELL_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id_2, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id}", exch_order_id) self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id_2}", exch_order_id_2) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) if API_MOCK_ENABLED: resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy() resp["order_id"] = exch_order_id MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy() resp["order_id"] = exch_order_id_2 MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.11) for cr in cancellation_results: self.assertEqual(cr.success, True) # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETH-USDC" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureCoinbasePro.BUY_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT_MAKER for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_limit_taker_sell(self): trading_pair = "ETH-USDC" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureCoinbasePro.BUY_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT_MAKER for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-USDC" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.2") self.assertGreater(self.market.get_balance("ETH"), amount) bid_price: Decimal = current_bid_price - Decimal( "0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELED) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): trading_pair = "ETH-USDC" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) _, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) _, exch_order_id_2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureCoinbasePro.OPEN_SELL_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id}", exch_order_id) self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id_2}", exch_order_id_2) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) if API_MOCK_ENABLED: resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy() resp["order_id"] = exch_order_id MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) resp = FixtureCoinbasePro.WS_ORDER_CANCELED.copy() resp["order_id"] = exch_order_id_2 MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.11) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETH-USDC" amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: CoinbaseProExchange = CoinbaseProExchange( coinbase_pro_api_key=API_KEY, coinbase_pro_secret_key=API_SECRET, coinbase_pro_passphrase=API_PASSPHRASE, trading_pairs=["ETH-USDC"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELED) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELED) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.02") order_id, exch_order_id = self.place_order( True, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureCoinbasePro.BUY_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount = buy_order_completed_event.base_asset_amount order_id, exch_order_id = self.place_order( False, trading_pair, amount, OrderType.LIMIT, price, 10002, FixtureCoinbasePro.SELL_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELED) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BinancePerpetualOrderBookTrackerUnitTest(unittest.TestCase): order_book_tracker: Optional[BinancePerpetualOrderBookTracker] = None events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent] trading_pairs: List[str] = ["BTC-USDT", "ETH-USDT"] @classmethod def setUpClass(cls) -> None: cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.order_book_tracker: BinancePerpetualOrderBookTracker = BinancePerpetualOrderBookTracker( trading_pairs=cls.trading_pairs, base_url="https://testnet.binancefuture.com", stream_url="wss://stream.binancefuture.com") cls.order_book_tracker_task: asyncio.Task = safe_ensure_future( cls.order_book_tracker.start()) cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @classmethod async def wait_til_tracker_ready(cls): while True: if len(cls.order_book_tracker.order_books) > 0: print("Initialized real-time order books.") return await asyncio.sleep(1) def setUp(self) -> None: self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items( ): order_book.add_listener(event_tag, self.event_logger) @staticmethod async def run_parallel_async(*tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): await asyncio.sleep(1) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_order_book_trade_occurs(self): self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent)) for ob_trade_event in self.event_logger.event_log: self.assertEqual(type(ob_trade_event), OrderBookTradeEvent) self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs) self.assertEqual(type(ob_trade_event.timestamp), float) self.assertEqual(type(ob_trade_event.amount), float) self.assertEqual(type(ob_trade_event.price), float) self.assertEqual(type(ob_trade_event.type), TradeType) self.assertTrue(ob_trade_event.amount > 0) self.assertTrue(ob_trade_event.price > 0) def test_tracker_adv(self): self.ev_loop.run_until_complete(asyncio.sleep(10)) order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books btcusdt_book: OrderBook = order_books[self.trading_pairs[0]] ethusdt_book: OrderBook = order_books[self.trading_pairs[1]] print("BTC-USDT SNAPSHOT: ") print(btcusdt_book.snapshot) print("ETH-USDT SNAPSHOT: ") print(ethusdt_book.snapshot) self.assertGreaterEqual( btcusdt_book.get_price_for_volume(True, 10).result_price, btcusdt_book.get_price(True)) self.assertLessEqual( btcusdt_book.get_price_for_volume(False, 10).result_price, btcusdt_book.get_price(False)) self.assertGreaterEqual( ethusdt_book.get_price_for_volume(True, 10).result_price, ethusdt_book.get_price(True)) self.assertLessEqual( ethusdt_book.get_price_for_volume(False, 10).result_price, ethusdt_book.get_price(False))
def setUp(self): self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items( ): order_book.add_listener(event_tag, self.event_logger)
class BambooRelayMarketCoordinatedUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderExpired, MarketEvent.OrderFilled, MarketEvent.WithdrawAsset ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: BambooRelayMarket market_logger: EventLogger wallet_logger: EventLogger @classmethod def setUpClass(cls): if conf.test_bamboo_relay_chain_id == 3: chain = EthereumChain.ROPSTEN elif conf.test_bamboo_relay_chain_id == 4: chain = EthereumChain.RINKEBY elif conf.test_bamboo_relay_chain_id == 42: chain = EthereumChain.KOVAN elif conf.test_bamboo_relay_chain_id == 1337: chain = EthereumChain.ZEROEX_TEST else: chain = EthereumChain.MAIN_NET cls.chain = chain cls.base_token_asset = conf.test_bamboo_relay_base_token_symbol cls.quote_token_asset = conf.test_bamboo_relay_quote_token_symbol cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_private_key_bamboo, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[ conf.test_bamboo_relay_base_token_address, conf.test_bamboo_relay_quote_token_address ], chain=chain) cls.market: BambooRelayMarket = BambooRelayMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, trading_pairs=[ conf.test_bamboo_relay_base_token_symbol + "-" + conf.test_bamboo_relay_quote_token_symbol ], use_coordinator=True, pre_emptive_soft_cancels=True) print("Initializing Bamboo Relay market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath( join(__file__, "../bamboo_relay_uncordinated_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, Decimal(20), Decimal(0.01)) self.assertEqual(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, Decimal(20)) self.assertEqual(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 2) self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH") self.assertEqual(taker_buy_trade_fee.flat_fees[1][0], "ETH") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), s_decimal_0) self.assertGreaterEqual((balances[self.quote_token_asset]), s_decimal_0) def test_limit_order_amount_modified(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset current_price: Decimal = self.market.get_price(trading_pair, True) amount = Decimal("0.001") expires = int(time.time() + 60 * 3) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price - Decimal("0.2") * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset, buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.assertEqual(float(quantized_amount), float(buy_order_opened_event.amount)) [cancellation_results, buy_order_cancelled_event] = self.run_parallel( self.market.cancel_order(buy_order_id), self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) # Reset the logs self.market_logger.clear() def test_single_limit_order_cancel(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset current_price: Decimal = self.market.get_price(trading_pair, True) amount = Decimal("0.001") expires = int(time.time() + 60 * 3) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price - Decimal("0.2") * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset, buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.assertEqual(float(quantized_amount), float(buy_order_opened_event.amount)) [cancellation_results, buy_order_cancelled_event] = self.run_parallel( self.market.cancel_order(buy_order_id), self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) # Reset the logs self.market_logger.clear() def test_limit_buy_and_sell_and_cancel_all(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset current_price: Decimal = self.market.get_price(trading_pair, True) amount = Decimal("0.001") expires = int(time.time() + 60 * 3) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price - Decimal("0.2") * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(float(quantized_amount), float(buy_order_opened_event.amount)) self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset, buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) # Reset the logs self.market_logger.clear() current_price: Decimal = self.market.get_price(trading_pair, False) sell_order_id = self.market.sell(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price + Decimal("0.2") * current_price, expiration_ts=expires) [sell_order_opened_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(float(quantized_amount), float(sell_order_opened_event.amount)) self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset, sell_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) [cancellation_results, order_cancelled_event] = self.run_parallel( self.market.cancel_all(60 * 3), self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cancellation_results[0], CancellationResult(buy_order_id, True)) self.assertEqual(cancellation_results[1], CancellationResult(sell_order_id, True)) # Wait for the order book source to also register the cancellation self.assertTrue( (buy_order_opened_event.order_id == order_cancelled_event.order_id or sell_order_opened_event.order_id == order_cancelled_event.order_id)) # Reset the logs self.market_logger.clear() def test_order_pre_emptive_cancel(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset current_price: Decimal = self.market.get_price(trading_pair, True) amount = Decimal("0.003") expires = int(time.time() + 60) # expires in 1 min quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price - Decimal("0.2") * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(self.base_token_asset + "-" + self.quote_token_asset, buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) [buy_order_expired_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent, 75)) self.assertEqual(buy_order_opened_event.order_id, buy_order_expired_event.order_id) # Reset the logs self.market_logger.clear() def test_market_buy(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset amount = Decimal("0.002") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.buy( self.base_token_asset + "-" + self.quote_token_asset, amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_asset, order_completed_event.base_asset) self.assertEqual(self.quote_token_asset, order_completed_event.quote_asset) self.market_logger.clear() def test_batch_market_buy(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset amount = Decimal("0.002") current_price: Decimal = self.market.get_price(trading_pair, True) expires = int(time.time() + 60 * 3) sell_order_id = self.market.sell(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price - Decimal("0.2") * current_price, expiration_ts=expires) self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) amount = Decimal("0.004") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.buy( self.base_token_asset + "-" + self.quote_token_asset, amount, OrderType.MARKET) [order_completed_event, _] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent), self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_asset, order_completed_event.base_asset) self.assertEqual(self.quote_token_asset, order_completed_event.quote_asset) self.market_logger.clear() def test_market_sell(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset amount = Decimal("0.001") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.sell(trading_pair, amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_asset, order_completed_event.base_asset) self.assertEqual(self.quote_token_asset, order_completed_event.quote_asset) self.market_logger.clear() def test_batch_market_sell(self): trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset amount = Decimal("0.002") current_price: Decimal = self.market.get_price(trading_pair, False) expires = int(time.time() + 60 * 3) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price + Decimal("0.2") * current_price, expiration_ts=expires) self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) amount = Decimal("0.005") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.sell( self.base_token_asset + "-" + self.quote_token_asset, amount, OrderType.MARKET) [order_completed_event, _] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent), self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_asset, order_completed_event.base_asset) self.assertEqual(self.quote_token_asset, order_completed_event.quote_asset) self.market_logger.clear() def test_wrap_eth(self): amount_to_wrap = Decimal("0.01") tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(float(amount_to_wrap), float(tx_completed_event.amount)) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_unwrap_eth(self): amount_to_unwrap = Decimal("0.01") tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(float(amount_to_unwrap), float(tx_completed_event.amount)) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_z_orders_saving_and_restoration(self): self.market.reset_state() config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) # Try to put limit buy order for 0.05 Quote Token worth of Base Token, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.005") / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) expires = int(time.time() + 60 * 3) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, expiration_ts=expires) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) self.assertEqual( order_id, list(self.market.tracking_states["limit_orders"].keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertIsInstance( saved_market_states.saved_state["limit_orders"], dict) self.assertGreater( len(saved_market_states.saved_state["limit_orders"]), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: BambooRelayMarket = BambooRelayMarket( wallet=self.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, trading_pairs=[ self.base_token_asset + "-" + self.quote_token_asset ], use_coordinator=True, pre_emptive_soft_cancels=True) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) # Cancel the order and verify that the change is saved. self.run_parallel(self.market.cancel(trading_pair, order_id), self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual( 1, len(saved_market_states.saved_state["limit_orders"])) finally: if order_id is not None: self.run_parallel( self.market.cancel(trading_pair, order_id), self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = self.base_token_asset + "-" + self.quote_token_asset sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.005") / current_price order_id = self.market.buy(trading_pair, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = buy_order_completed_event.base_asset_amount order_id = self.market.sell(trading_pair, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.run_parallel( self.market.cancel(trading_pair, order_id), self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class DydxOrderBookTrackerUnitTest(unittest.TestCase): order_book_tracker: Optional[DydxOrderBookTracker] = None events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent] trading_pairs: List[str] = ["WETH-USDC", "WETH-DAI"] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.order_book_tracker: DydxOrderBookTracker = DydxOrderBookTracker( trading_pairs=cls.trading_pairs, ) cls.order_book_tracker.start() cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @classmethod async def wait_til_tracker_ready(cls): while True: if len(cls.order_book_tracker.order_books) > 0: print("Initialized real-time order books.") return await asyncio.sleep(1) async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def setUp(self): self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items( ): order_book.add_listener(event_tag, self.event_logger) def test_order_book_trade_event_emission(self): """ Test if order book tracker is able to retrieve order book trade message from exchange and emit order book trade events after correctly parsing the trade messages """ self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent)) for ob_trade_event in self.event_logger.event_log: self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent) self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs) self.assertTrue(type(ob_trade_event.timestamp) == float) self.assertTrue(type(ob_trade_event.amount) == float) self.assertTrue(type(ob_trade_event.price) == float) self.assertTrue(type(ob_trade_event.type) == TradeType) self.assertTrue( math.ceil(math.log10(ob_trade_event.timestamp)) == 10) self.assertTrue(ob_trade_event.amount > 0) self.assertTrue(ob_trade_event.price > 0) def test_tracker_integrity(self): # Wait 5 seconds to process some diffs. self.ev_loop.run_until_complete(asyncio.sleep(5.0)) order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books lrc_eth_book: OrderBook = order_books["WETH-DAI"] self.assertGreaterEqual( lrc_eth_book.get_price_for_volume(True, 0.1).result_price, lrc_eth_book.get_price(True)) def test_mid_price(self): data_source = self.order_book_tracker.data_source mid_price = data_source.get_mid_price(self.trading_pairs[0]) self.assertGreater(mid_price, 100)
class PaperTradeExchangeTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: PaperTradeExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: PaperTradeExchange = PaperTradeExchange( order_book_tracker=OrderBookTracker( data_source=BinanceAPIOrderBookDataSource( trading_pairs=["ETH-USDT", "BTC-USDT"]), trading_pairs=["ETH-USDT", "BTC-USDT"]), target_market=BinanceExchange, exchange_name="binance", ) print( "Initializing PaperTrade execute orders market... this will take about a minute." ) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../binance_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) for trading_pair, orderbook in self.market.order_books.items(): orderbook.clear_traded_order_book() def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_place_market_orders(self): self.market.sell("ETH-USDT", 30, OrderType.MARKET) list_queued_orders: List[QueuedOrder] = self.market.queued_orders first_queued_order: QueuedOrder = list_queued_orders[0] self.assertFalse(first_queued_order.is_buy, msg="Market order is not sell") self.assertEqual(first_queued_order.trading_pair, "ETH-USDT", msg="Trading pair is incorrect") self.assertEqual(first_queued_order.amount, 30, msg="Quantity is incorrect") self.assertEqual(len(list_queued_orders), 1, msg="First market order did not get added") # Figure out why this test is failing self.market.buy("BTC-USDT", 30, OrderType.MARKET) list_queued_orders: List[QueuedOrder] = self.market.queued_orders second_queued_order: QueuedOrder = list_queued_orders[1] self.assertTrue(second_queued_order.is_buy, msg="Market order is not buy") self.assertEqual(second_queued_order.trading_pair, "BTC-USDT", msg="Trading pair is incorrect") self.assertEqual(second_queued_order.amount, 30, msg="Quantity is incorrect") self.assertEqual(second_queued_order.amount, 30, msg="Quantity is incorrect") self.assertEqual(len(list_queued_orders), 2, msg="Second market order did not get added") def test_market_order_simulation(self): self.market.set_balance("ETH", 20) self.market.set_balance("USDT", 100) self.market.sell("ETH-USDT", 10, OrderType.MARKET) self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Get diff between composite bid entries and original bid entries compare_df = OrderBookUtils.get_compare_df( self.market.order_books['ETH-USDT'].original_bid_entries(), self.market.order_books['ETH-USDT'].bid_entries(), diffs_only=True).sort_index().round(10) filled_bids = OrderBookUtils.ob_rows_data_frame( list(self.market.order_books['ETH-USDT'].traded_order_book. bid_entries())).sort_index().round(10) # assert filled orders matches diff diff_bid = compare_df["diff"] - filled_bids["amount"] self.assertFalse(diff_bid.to_numpy().any()) self.assertEquals(10, self.market.get_balance("ETH"), msg="Balance was not updated.") self.market.buy("ETH-USDT", 5, OrderType.MARKET) self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Get diff between composite bid entries and original bid entries compare_df = OrderBookUtils.get_compare_df( self.market.order_books['ETH-USDT'].original_ask_entries(), self.market.order_books['ETH-USDT'].ask_entries(), diffs_only=True).sort_index().round(10) filled_asks = OrderBookUtils.ob_rows_data_frame( list(self.market.order_books['ETH-USDT'].traded_order_book. ask_entries())).sort_index().round(10) # assert filled orders matches diff diff_ask = compare_df["diff"] - filled_asks["amount"] self.assertFalse(diff_ask.to_numpy().any()) self.assertEquals(15, self.market.get_balance("ETH"), msg="Balance was not updated.") def test_limit_order_crossed(self): starting_base_balance = 20 starting_quote_balance = 1000 self.market.set_balance("ETH", starting_base_balance) self.market.set_balance("USDT", starting_quote_balance) self.market.sell("ETH-USDT", 10, OrderType.LIMIT, 100) self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) self.assertEquals(starting_base_balance - 10, self.market.get_balance("ETH"), msg="ETH Balance was not updated.") self.assertEquals(starting_quote_balance + 1000, self.market.get_balance("USDT"), msg="USDT Balance was not updated.") self.market.buy("ETH-USDT", 1, OrderType.LIMIT, 500) self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) self.assertEquals(11, self.market.get_balance("ETH"), msg="ETH Balance was not updated.") self.assertEquals(1500, self.market.get_balance("USDT"), msg="USDT Balance was not updated.") def test_bid_limit_order_trade_match(self): """ Test bid limit order fill and balance simulation, and market events emission """ trading_pair = TradingPair("ETH-USDT", "ETH", "USDT") base_quantity = 2.0 starting_base_balance = 200 starting_quote_balance = 2000 self.market.set_balance(trading_pair.base_asset, starting_base_balance) self.market.set_balance(trading_pair.quote_asset, starting_quote_balance) best_bid_price = self.market.order_books[ trading_pair.trading_pair].get_price(True) client_order_id = self.market.buy(trading_pair.trading_pair, base_quantity, OrderType.LIMIT, best_bid_price) matched_limit_orders = TestUtils.get_match_limit_orders( self.market.limit_orders, { "client_order_id": client_order_id, "trading_pair": trading_pair.trading_pair, "is_buy": True, "base_currency": trading_pair.base_asset, "quote_currency": trading_pair.quote_asset, "price": best_bid_price, "quantity": base_quantity }) # Market should track limit orders self.assertEqual(1, len(matched_limit_orders)) # Market should on hold balance for the created order self.assertAlmostEqual( float(self.market.on_hold_balances[trading_pair.quote_asset]), base_quantity * best_bid_price) # Market should reflect on hold balance in available balance self.assertAlmostEqual( float(self.market.get_available_balance(trading_pair.quote_asset)), starting_quote_balance - base_quantity * best_bid_price) matched_order_create_events = TestUtils.get_match_events( self.market_logger.event_log, BuyOrderCreatedEvent, { "type": OrderType.LIMIT, "amount": base_quantity, "price": best_bid_price, "order_id": client_order_id }) # Market should emit BuyOrderCreatedEvent self.assertEqual(1, len(matched_order_create_events)) async def delay_trigger_event1(): await asyncio.sleep(1) trade_event1 = OrderBookTradeEvent(trading_pair="ETH-USDT", timestamp=time.time(), type=TradeType.SELL, price=best_bid_price + 1, amount=1.0) self.market.order_books['ETH-USDT'].apply_trade(trade_event1) safe_ensure_future(delay_trigger_event1()) self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) placed_bid_orders: List[LimitOrder] = [ o for o in self.market.limit_orders if o.is_buy ] # Market should delete limit order when it is filled self.assertEqual(0, len(placed_bid_orders)) matched_order_complete_events = TestUtils.get_match_events( self.market_logger.event_log, BuyOrderCompletedEvent, { "order_type": OrderType.LIMIT, "quote_asset_amount": base_quantity * best_bid_price, "order_id": client_order_id }) # Market should emit BuyOrderCompletedEvent self.assertEqual(1, len(matched_order_complete_events)) matched_order_fill_events = TestUtils.get_match_events( self.market_logger.event_log, OrderFilledEvent, { "order_type": OrderType.LIMIT, "trade_type": TradeType.BUY, "trading_pair": trading_pair.trading_pair, "order_id": client_order_id }) # Market should emit OrderFilledEvent self.assertEqual(1, len(matched_order_fill_events)) # Market should have no more on hold balance self.assertAlmostEqual( float(self.market.on_hold_balances[trading_pair.quote_asset]), 0) # Market should update balance for the filled order self.assertAlmostEqual( float(self.market.get_available_balance(trading_pair.quote_asset)), starting_quote_balance - base_quantity * best_bid_price) def test_ask_limit_order_trade_match(self): """ Test ask limit order fill and balance simulation, and market events emission """ trading_pair = TradingPair("ETH-USDT", "ETH", "USDT") base_quantity = 2.0 starting_base_balance = 200 starting_quote_balance = 2000 self.market.set_balance(trading_pair.base_asset, starting_base_balance) self.market.set_balance(trading_pair.quote_asset, starting_quote_balance) best_ask_price = self.market.order_books[ trading_pair.trading_pair].get_price(False) client_order_id = self.market.sell(trading_pair.trading_pair, base_quantity, OrderType.LIMIT, best_ask_price) matched_limit_orders = TestUtils.get_match_limit_orders( self.market.limit_orders, { "client_order_id": client_order_id, "trading_pair": trading_pair.trading_pair, "is_buy": False, "base_currency": trading_pair.base_asset, "quote_currency": trading_pair.quote_asset, "price": best_ask_price, "quantity": base_quantity }) # Market should track limit orders self.assertEqual(1, len(matched_limit_orders)) # Market should on hold balance for the created order self.assertAlmostEqual( float(self.market.on_hold_balances[trading_pair.base_asset]), base_quantity) # Market should reflect on hold balance in available balance self.assertAlmostEqual( self.market.get_available_balance(trading_pair.base_asset), starting_base_balance - base_quantity) matched_order_create_events = TestUtils.get_match_events( self.market_logger.event_log, SellOrderCreatedEvent, { "type": OrderType.LIMIT, "amount": base_quantity, "price": best_ask_price, "order_id": client_order_id }) # Market should emit BuyOrderCreatedEvent self.assertEqual(1, len(matched_order_create_events)) async def delay_trigger_event2(): await asyncio.sleep(1) trade_event = OrderBookTradeEvent( trading_pair=trading_pair.trading_pair, timestamp=time.time(), type=TradeType.BUY, price=best_ask_price - 1, amount=base_quantity) self.market.order_books[trading_pair.trading_pair].apply_trade( trade_event) safe_ensure_future(delay_trigger_event2()) self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) placed_ask_orders: List[LimitOrder] = [ o for o in self.market.limit_orders if not o.is_buy ] # Market should delete limit order when it is filled self.assertEqual(0, len(placed_ask_orders)) matched_order_complete_events = TestUtils.get_match_events( self.market_logger.event_log, SellOrderCompletedEvent, { "order_type": OrderType.LIMIT, "quote_asset_amount": base_quantity * base_quantity, "order_id": client_order_id }) # Market should emit BuyOrderCompletedEvent self.assertEqual(1, len(matched_order_complete_events)) matched_order_fill_events = TestUtils.get_match_events( self.market_logger.event_log, OrderFilledEvent, { "order_type": OrderType.LIMIT, "trade_type": TradeType.SELL, "trading_pair": trading_pair.trading_pair, "order_id": client_order_id }) # Market should emit OrderFilledEvent self.assertEqual(1, len(matched_order_fill_events)) # Market should have no more on hold balance self.assertAlmostEqual( float(self.market.on_hold_balances[trading_pair.base_asset]), 0) # Market should update balance for the filled order self.assertAlmostEqual( self.market.get_available_balance(trading_pair.base_asset), starting_base_balance - base_quantity) def test_order_cancellation(self): trading_pair = TradingPair("ETH-USDT", "ETH", "USDT") base_quantity = 2.0 starting_base_balance = 200 starting_quote_balance = 2000 self.market.set_balance(trading_pair.base_asset, starting_base_balance) self.market.set_balance(trading_pair.quote_asset, starting_quote_balance) best_ask_price = self.market.order_books[ trading_pair.trading_pair].get_price(False) ask_client_order_id = self.market.sell(trading_pair.trading_pair, base_quantity, OrderType.LIMIT, best_ask_price) best_bid_price = self.market.order_books[ trading_pair.trading_pair].get_price(True) self.market.buy(trading_pair.trading_pair, base_quantity, OrderType.LIMIT, best_bid_price) # Market should track limit orders self.assertEqual(2, len(self.market.limit_orders)) self.market.cancel(trading_pair.trading_pair, ask_client_order_id) matched_limit_orders = TestUtils.get_match_limit_orders( self.market.limit_orders, { "client_order_id": ask_client_order_id, "trading_pair": trading_pair.trading_pair, "is_buy": False, "base_currency": trading_pair.base_asset, "quote_currency": trading_pair.quote_asset, "price": best_ask_price, "quantity": base_quantity }) # Market should remove canceled orders self.assertEqual(0, len(matched_limit_orders)) matched_order_cancel_events = TestUtils.get_match_events( self.market_logger.event_log, OrderCancelledEvent, {"order_id": ask_client_order_id}) # Market should emit cancel event self.assertEqual(1, len(matched_order_cancel_events)) def test_order_cancel_all(self): trading_pair = TradingPair("ETH-USDT", "ETH", "USDT") base_quantity = 2.0 starting_base_balance = 200 starting_quote_balance = 2000 self.market.set_balance(trading_pair.base_asset, starting_base_balance) self.market.set_balance(trading_pair.quote_asset, starting_quote_balance) best_ask_price = self.market.order_books[ trading_pair.trading_pair].get_price(False) self.market.sell(trading_pair.trading_pair, base_quantity, OrderType.LIMIT, best_ask_price) best_bid_price = self.market.order_books[ trading_pair.trading_pair].get_price(True) self.market.buy(trading_pair.trading_pair, base_quantity, OrderType.LIMIT, best_bid_price) # Market should track limit orders self.assertEqual(2, len(self.market.limit_orders)) asyncio.get_event_loop().run_until_complete(self.market.cancel_all(0)) # Market should remove all canceled orders self.assertEqual(0, len(self.market.limit_orders)) matched_order_cancel_events = TestUtils.get_match_events( self.market_logger.event_log, OrderCancelledEvent, {}) # Market should emit cancel event self.assertEqual(2, len(matched_order_cancel_events))
class KucoinExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: KucoinExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, ["/api/v1/timestamp", "/api/v1/symbols", "/api/v1/bullet-public", "/api/v2/market/orderbook/level2"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/api/v1/accounts", FixtureKucoin.BALANCES) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.kucoin.kucoin_exchange.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls._exch_order_id = 20001 cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready and cls.market_2.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../kucoin_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market_2_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) self.market_2.add_listener(event_tag, self.market_2_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_2.remove_listener(event_tag, self.market_2_logger) self.market_logger = None self.market_2_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10 ) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def order_response(self, fixture_data, nonce): self._t_nonce_mock.return_value = nonce order_resp = fixture_data.copy() return order_resp def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, post_resp, get_resp): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if API_MOCK_ENABLED: exch_order_id = f"KUCOIN_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 resp = self.order_response(post_resp, nonce) resp["data"]["orderId"] = exch_order_id self.web_app.update_response("post", API_BASE_URL, "/api/v1/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = get_resp.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) return order_id, exch_order_id def test_fee_overrides_config(self): fee_overrides_config_map["kucoin_taker_fee"].value = None taker_fee: AddedToCostTradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["kucoin_taker_fee"].value = Decimal('0.2') taker_fee: AddedToCostTradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["kucoin_maker_fee"].value = None maker_fee: AddedToCostTradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["kucoin_maker_fee"].value = Decimal('0.5') maker_fee: AddedToCostTradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDT" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDT" bid_price = self.market.get_price(trading_pair, True) * Decimal("0.8") quantized_bid_price = self.market.quantize_order_price(trading_pair, bid_price) quantized_bid_amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id, _ = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER, quantized_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.ORDER_GET_BUY_UNMATCHED) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) ask_price = self.market.get_price(trading_pair, True) * Decimal("1.2") quatized_ask_price = self.market.quantize_order_price(trading_pair, ask_price) quatized_ask_amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id, _ = self.place_order(False, trading_pair, quatized_ask_amount, OrderType.LIMIT_MAKER, quatized_ask_price, 10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.ORDER_GET_SELL_UNMATCHED) [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(0.01) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.BUY_MARKET_ORDER) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("ETH", buy_order_completed_event.base_asset) self.assertEqual("USDT", buy_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(buy_order_completed_event.base_asset_amount), places=4) self.assertAlmostEqual(quote_amount_traded, float(buy_order_completed_event.quote_asset_amount), places=4) self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_limit_taker_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal(0.011) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.SELL_MARKET_ORDER) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), sell_order_completed_event.base_asset_amount) self.assertEqual("ETH", sell_order_completed_event.base_asset) self.assertEqual("USDT", sell_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(sell_order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(sell_order_completed_event.quote_asset_amount)) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_cancel(self): trading_pair = "ETH-USDT" current_price: float = self.market.get_price(trading_pair, False) amount: Decimal = Decimal(0.01) price: Decimal = Decimal(current_price) * Decimal(1.1) quantized_price: Decimal = self.market.quantize_order_price(trading_pair, price) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantized_price, 10001, FixtureKucoin.ORDER_PLACE_2, FixtureKucoin.OPEN_SELL_LIMIT_ORDER) [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) if API_MOCK_ENABLED: resp = FixtureKucoin.CANCEL_ORDER.copy() resp["data"]["cancelledOrderIds"] = [exch_order_id] self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = FixtureKucoin.GET_CANCELLED_ORDER.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) self.market_logger.clear() def test_cancel_all(self): trading_pair = "ETH-USDT" bid_price: Decimal = Decimal(self.market_2.get_price(trading_pair, True)) ask_price: Decimal = Decimal(self.market_2.get_price(trading_pair, False)) amount: Decimal = Decimal(0.01) quantized_amount: Decimal = self.market_2.quantize_order_amount(trading_pair, amount) # Intentionally setting high price to prevent getting filled quantize_bid_price: Decimal = self.market_2.quantize_order_price(trading_pair, bid_price * Decimal(0.8)) quantize_ask_price: Decimal = self.market_2.quantize_order_price(trading_pair, ask_price * Decimal(1.2)) _, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_BUY_LIMIT_ORDER) _, exch_order_id2 = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_SELL_LIMIT_ORDER) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = FixtureKucoin.ORDERS_BATCH_CANCELLED.copy() resp["data"]["cancelledOrderIds"] = [exch_order_id, exch_order_id2] self.web_app.update_response("delete", API_BASE_URL, "/api/v1/orders", resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) self.market_2_logger.clear() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: float = self.market.get_price(trading_pair, True) bid_price: Decimal = Decimal(current_bid_price * Decimal(0.8)) quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) amount: Decimal = Decimal(0.04) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_BUY_LIMIT_ORDER) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) if API_MOCK_ENABLED: resp = FixtureKucoin.CANCEL_ORDER.copy() resp["data"]["cancelledOrderIds"] = exch_order_id self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = FixtureKucoin.GET_CANCELLED_ORDER.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) self.market_logger.clear() def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.01 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(0.01) order_id, _ = self.place_order(True, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.BUY_MARKET_ORDER) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal(buy_order_completed_event.base_asset_amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.SELL_MARKET_ORDER) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) self.market_logger.clear() def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertFalse(math.isnan(order_book.last_trade_price))
class OkexExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: OkexExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def strip_host_from_okex_url(cls, url): HOST = "https://www.okex.com" return url.split(HOST)[-1] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if MOCK_API_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, []) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local # mock_account_id = FixtureOKEx.GET_ACCOUNTS["data"][0]["id"] # warning: second parameter starts with / cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_INSTRUMENTS_URL), FixtureOKEx.OKEX_INSTRUMENTS_URL) cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_PRICE_URL).format( trading_pair='ETH-USDT'), FixtureOKEx.INSTRUMENT_TICKER) cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_DEPTH_URL).format( trading_pair='ETH-USDT'), FixtureOKEx.OKEX_ORDER_BOOK) cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_TICKERS_URL), FixtureOKEx.OKEX_TICKERS) cls.web_app.update_response("get", API_BASE_URL, '/' + OKEX_BALANCE_URL, FixtureOKEx.OKEX_BALANCE_URL) cls.web_app.update_response("get", API_BASE_URL, '/' + OKEX_SERVER_TIME, FixtureOKEx.TIMESTAMP) # cls.web_app.update_response("POST", API_BASE_URL, '/' + OKEX_PLACE_ORDER, FixtureOKEx.ORDER_PLACE) # cls.web_app.update_response("get", OKEX_BASE_URL, f"/v1/account/accounts/{mock_account_id}/balance", # FixtureOKEx.GET_BALANCES) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.okex.okex_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: OkexExchange = OkexExchange(API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=["ETH-USDT"]) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: OkexExchange = OkexExchange(API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=["ETH-USDT"]) cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() if MOCK_API_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready and cls.market_2.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../okex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market_2_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) self.market_2.add_listener(event_tag, self.market_2_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_2.remove_listener(event_tag, self.market_2_logger) self.market_logger = None self.market_2_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["okex_taker_fee"].value = None taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0015"), taker_fee.percent) fee_overrides_config_map["okex_taker_fee"].value = Decimal('0.1') taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["okex_maker_fee"].value = None maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["okex_maker_fee"].value = Decimal('0.5') maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, get_resp, market_connector=None): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if MOCK_API_ENABLED: exch_order_id = f"OKEX_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 self._t_nonce_mock.return_value = nonce resp = FixtureOKEx.ORDER_PLACE.copy() resp["data"][0]["ordId"] = exch_order_id # resp = exch_order_id side = 'buy' if is_buy else 'sell' order_id = f"{side}-{trading_pair}-{nonce}" self.web_app.update_response("post", API_BASE_URL, "/" + OKEX_PLACE_ORDER, resp) market = self.market if market_connector is None else market_connector if is_buy: order_id = market.buy(trading_pair, amount, order_type, price) else: order_id = market.sell(trading_pair, amount, order_type, price) if MOCK_API_ENABLED: resp = get_resp.copy() # resp is the response passed by parameter resp["data"][0]["ordId"] = exch_order_id resp["data"][0]["clOrdId"] = order_id self.web_app.update_response( "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format( ordId=exch_order_id, trading_pair="ETH-USDT"), resp) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exchange_order_id, get_resp): if MOCK_API_ENABLED: resp = FixtureOKEx.ORDER_CANCEL.copy() resp["data"][0]["ordId"] = exchange_order_id resp["data"][0]["clOrdId"] = order_id self.web_app.update_response("post", API_BASE_URL, '/' + OKEX_ORDER_CANCEL, resp, params={"ordId": exchange_order_id}) self.market.cancel(trading_pair, order_id) if MOCK_API_ENABLED: resp = get_resp.copy() resp["data"][0]["ordId"] = exchange_order_id resp["data"][0]["clOrdId"] = order_id self.web_app.update_response( "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format( ordId=exchange_order_id, trading_pair="ETH-USDT"), resp) def test_limit_maker_rejections(self): if MOCK_API_ENABLED: return trading_pair = "ETH-USDT" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.06")) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.06")) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if MOCK_API_ENABLED: return trading_pair = "ETH-USDT" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.1")) order_id1, exch_order_id1 = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id1, order_created_event.order_id) order_id2, exch_order_id2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureOKEx.ORDER_GET_LIMIT_SELL_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id2, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) if MOCK_API_ENABLED: resp = FixtureOKEx.ORDERS_BATCH_CANCELED.copy() resp["data"]["success"] = [exch_order_id1, exch_order_id2] self.web_app.update_response("post", API_BASE_URL, "/v1/order/orders/batchcancel", resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) # Reset the logs self.market_logger.clear() def test_limit_taker_buy(self): trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureOKEx.ORDER_GET_MARKET_BUY) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("ETH", buy_order_completed_event.base_asset) self.assertEqual("USDT", buy_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, buy_order_completed_event.base_asset_amount, places=4) self.assertAlmostEqual(quote_amount_traded, buy_order_completed_event.quote_asset_amount, places=4) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_taker_sell(self): trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureOKEx.ORDER_GET_MARKET_SELL) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, sell_order_completed_event.base_asset_amount) self.assertEqual("ETH", sell_order_completed_event.base_asset) self.assertEqual("USDT", sell_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, sell_order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, sell_order_completed_event.quote_asset_amount) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.05") bid_price: Decimal = current_bid_price - Decimal( "0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.cancel_order(trading_pair, order_id, exch_order_id, FixtureOKEx.ORDER_GET_CANCELED) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): trading_pair = "ETH-USDT" bid_price: Decimal = self.market_2.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market_2.get_price(trading_pair, False) * 2 amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market_2.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market_2.quantize_order_price( trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market_2.quantize_order_price( trading_pair, ask_price * Decimal("1.1")) _, exch_order_id1 = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 1001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED, self.market_2) _, exch_order_id2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 1002, FixtureOKEx.ORDER_GET_LIMIT_BUY_FILLED, self.market_2) self.run_parallel(asyncio.sleep(1)) if MOCK_API_ENABLED: resp = FixtureOKEx.ORDERS_BATCH_CANCELED.copy() resp["data"][0]["ordId"] = exch_order_id1 self.web_app.update_response("post", API_BASE_URL, '/' + OKEX_BATCH_ORDER_CANCEL, resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, '0') def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event # self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: OkexExchange = OkexExchange( API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=["ETH-USDT", "BTC-USDT"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id, FixtureOKEx.ORDER_GET_CANCELED) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.06") order_id, _ = self.place_order(True, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureOKEx.ORDER_GET_MARKET_BUY) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount = buy_order_completed_event.base_asset_amount order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10002, FixtureOKEx.ORDER_GET_MARKET_SELL) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertFalse(math.isnan(order_book.last_trade_price))
def initialize_event_loggers(self): self.event_logger = EventLogger() # TODO subscribe to other events? for event in self.exchange.MARKET_EVENTS: self.exchange.add_listener(event, self.event_logger) print("subscribing event_logger to ", event)
class BinanceMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: BinanceMarket market_logger: EventLogger stack: contextlib.ExitStack base_api_url = "api.binance.com" @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.ev_loop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = HummingWebApp.get_instance() cls.web_app.add_host_to_mock( cls.base_api_url, ["/api/v1/ping", "/api/v1/time", "/api/v1/ticker/24hr"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls._req_patcher = unittest.mock.patch.object(requests.Session, "request", autospec=True) cls._req_url_mock = cls._req_patcher.start() cls._req_url_mock.side_effect = HummingWebApp.reroute_request cls.web_app.update_response("get", cls.base_api_url, "/api/v3/account", FixtureBinance.BALANCES) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/exchangeInfo", FixtureBinance.MARKETS) cls.web_app.update_response("get", cls.base_api_url, "/wapi/v3/tradeFee.html", FixtureBinance.TRADE_FEES) cls.web_app.update_response("post", cls.base_api_url, "/api/v1/userDataStream", FixtureBinance.LISTEN_KEY) cls.web_app.update_response("put", cls.base_api_url, "/api/v1/userDataStream", FixtureBinance.LISTEN_KEY) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/depth", FixtureBinance.LINKETH_SNAP, params={'symbol': 'LINKETH'}) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/depth", FixtureBinance.ZRXETH_SNAP, params={'symbol': 'ZRXETH'}) ws_base_url = "wss://stream.binance.com:9443/ws" cls._ws_user_url = f"{ws_base_url}/{FixtureBinance.LISTEN_KEY['listenKey']}" HummingWsServerFactory.start_new_server(cls._ws_user_url) HummingWsServerFactory.start_new_server( f"{ws_base_url}/linketh@depth/zrxeth@depth") cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.binance.binance_market.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BinanceMarket = BinanceMarket(API_KEY, API_SECRET, ["LINK-ETH", "ZRX-ETH"], True) print("Initializing Binance market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._req_patcher.stop() cls._ws_patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../binance_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal(4000)) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.LIMIT, TradeType.BUY, Decimal(1)) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["binance_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["binance_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["binance_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["binance_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def test_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) bid_price: Decimal = self.market.get_price("LINK-ETH", True) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "LINK-ETH", amount) order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, 10001, FixtureBinance.BUY_MARKET_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. ask_price: Decimal = self.market.get_price("LINK-ETH", False) amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT, ask_price, 10002, FixtureBinance.SELL_MARKET_ORDER, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_limit_maker_rejections(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price("LINK-ETH", True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, 10001, FixtureBinance.LIMIT_MAKER_ERROR) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price("LINK-ETH", True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, 10002, FixtureBinance.LIMIT_MAKER_ERROR) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): price = self.market.get_price("LINK-ETH", True) * Decimal("0.8") price = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, 10001, FixtureBinance.OPEN_BUY_ORDER) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) price = self.market.get_price("LINK-ETH", True) * Decimal("1.2") price = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, 10002, FixtureBinance.OPEN_SELL_ORDER) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def fixture(self, fixture_data, **overwrites): data = fixture_data.copy() for key, value in overwrites.items(): if key not in data: raise Exception(f"{key} not found in fixture_data") data[key] = value return data def order_response(self, fixture_data, nonce, side, trading_pair): self._t_nonce_mock.return_value = nonce order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}" order_resp = fixture_data.copy() order_resp["clientOrderId"] = order_id return order_resp def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, fixture_resp, fixture_ws_1=None, fixture_ws_2=None): order_id = None if API_MOCK_ENABLED: resp = self.order_response(fixture_resp, nonce, 'buy' if is_buy else 'sell', trading_pair) self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED and fixture_ws_1 is not None and fixture_ws_2 is not None: data = self.fixture(fixture_ws_1, c=order_id) HummingWsServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.1) data = self.fixture(fixture_ws_2, c=order_id) HummingWsServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.11) return order_id def test_cancel_all(self): trading_pair = "LINK-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) buy_id = self.place_order(True, "LINK-ETH", quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBinance.OPEN_BUY_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) sell_id = self.place_order(False, "LINK-ETH", quantized_amount, OrderType.LIMIT, quantize_ask_price, 10002, FixtureBinance.OPEN_SELL_ORDER, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=buy_id, side="BUY") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': buy_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=sell_id, side="SELL") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': sell_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_order_price_precision(self): # As of the day this test was written, the min order size (base) is 1 LINK, the min order size (quote) is # 0.01 ETH, and order step size is 1 LINK. trading_pair = "LINK-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) mid_price: Decimal = (bid_price + ask_price) / 2 amount: Decimal = Decimal("1.23123216") binance_client = self.market.binance_client # Make sure there's enough balance to make the limit orders. self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) self.assertGreater(self.market.get_balance("LINK"), amount * 2) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price: Decimal = mid_price * Decimal("0.9333192292111341") ask_price: Decimal = mid_price * Decimal("1.0492431474884933") # This is needed to get around the min quote amount limit. bid_amount: Decimal = Decimal("1.23123216") if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_BUY_PRECISION, 1000001, "buy", "LINK-ETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) # Test bid order bid_order_id: str = self.market.buy(trading_pair, Decimal(bid_amount), OrderType.LIMIT, Decimal(bid_price)) if API_MOCK_ENABLED: resp = FixtureBinance.ORDER_BUY_PRECISION_GET resp["clientOrderId"] = bid_order_id self.web_app.update_response("get", self.base_api_url, "/api/v3/order", resp) # Wait for the order created event and examine the order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent, timeout_seconds=10)) order_data: Dict[str, any] = binance_client.get_order( symbol=trading_pair, origClientOrderId=bid_order_id) quantized_bid_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(bid_price)) bid_size_quantum: Decimal = self.market.get_order_size_quantum( trading_pair, Decimal(bid_amount)) self.assertEqual(quantized_bid_price, Decimal(order_data["price"])) self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0) # Test ask order if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_SELL_PRECISION, 1000002, "sell", "LINK-ETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) ask_order_id: str = self.market.sell(trading_pair, Decimal(amount), OrderType.LIMIT, Decimal(ask_price)) if API_MOCK_ENABLED: resp = FixtureBinance.ORDER_SELL_PRECISION_GET resp["clientOrderId"] = ask_order_id self.web_app.update_response("get", self.base_api_url, "/api/v3/order", resp) # Wait for the order created event and examine and order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent, timeout_seconds=10)) order_data = binance_client.get_order(symbol=trading_pair, origClientOrderId=ask_order_id) quantized_ask_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(ask_price)) quantized_ask_size: Decimal = self.market.quantize_order_amount( trading_pair, Decimal(amount)) self.assertEqual(quantized_ask_price, Decimal(order_data["price"])) self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"])) # Cancel all the orders if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=bid_order_id, side="BUY") self.web_app.update_response( "delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': bid_order_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=ask_order_id, side="SELL") self.web_app.update_response( "delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': ask_order_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_server_time_offset(self): time_obj: BinanceTime = binance_client_module.time old_check_interval: float = time_obj._server_time_offset_check_interval time_obj._server_time_offset_check_interval = 1.0 time_obj.stop() time_obj.start() try: local_time_offset = (time.time() - time.perf_counter()) * 1e3 with patch( "hummingbot.connector.exchange.binance.binance_time.time" ) as market_time: def delayed_time(): return time.perf_counter() - 30.0 market_time.perf_counter = delayed_time self.run_parallel(asyncio.sleep(3.0)) raw_time_offset = BinanceTime.get_instance().time_offset_ms time_offset_diff = raw_time_offset - local_time_offset # check if it is less than 5% off self.assertTrue(time_offset_diff > 10000) self.assertTrue(abs(time_offset_diff - 30.0 * 1e3) < 1.5 * 1e3) finally: time_obj._server_time_offset_check_interval = old_check_interval time_obj.stop() time_obj.start() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( "LINK-ETH", True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( "LINK-ETH", bid_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "LINK-ETH", amount) if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.OPEN_BUY_ORDER, 1000001, "buy", "LINK-ETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) order_id = self.market.buy("LINK-ETH", quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BinanceMarket = BinanceMarket(API_KEY, API_SECRET, ["LINK-ETH", "ZRX-ETH"], True) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=order_id, side="BUY") self.web_app.update_response( "delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': order_id}) self.market.cancel("LINK-ETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel("LINK-ETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 1 LINK from the exchange, and watch for completion event. bid_price: Decimal = self.market.get_price("LINK-ETH", True) amount: Decimal = 1 order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, 10001, FixtureBinance.BUY_LIMIT_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of LINK to the exchange, and watch for completion event. ask_price: Decimal = self.market.get_price("LINK-ETH", False) amount = buy_order_completed_event.base_asset_amount order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT, ask_price, 10002, FixtureBinance.SELL_LIMIT_ORDER, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel("LINK-ETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_pair_convesion(self): if API_MOCK_ENABLED: return for pair in self.market.trading_rules: exchange_pair = convert_to_exchange_trading_pair(pair) self.assertTrue(exchange_pair in self.market.order_books)
def setUp(self): self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp) self.mid_price = 100 self.time_delay = 15 self.cancel_order_wait_time = 45 self.market: MockPaperExchange = MockPaperExchange() self.market.set_balanced_order_book(trading_pair=self.trading_pair, mid_price=self.mid_price, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) self.market.set_balance("COINALPHA", 500) self.market.set_balance("WETH", 5000) self.market.set_quantization_param( QuantizationParams(self.trading_pair, 6, 6, 6, 6)) self.market_info: MarketTradingPairTuple = MarketTradingPairTuple( self.market, self.trading_pair, self.base_asset, self.quote_asset) # Define strategies to test self.buy_mid_price_strategy: PerformTradeStrategy = PerformTradeStrategy( exchange=self.market, trading_pair=self.trading_pair, is_buy=True, spread=self.spread, order_amount=Decimal("1.0"), price_type=PriceType.MidPrice) self.sell_mid_price_strategy: PerformTradeStrategy = PerformTradeStrategy( exchange=self.market, trading_pair=self.trading_pair, is_buy=False, spread=self.spread, order_amount=Decimal("1.0"), price_type=PriceType.MidPrice) self.buy_last_price_strategy: PerformTradeStrategy = PerformTradeStrategy( exchange=self.market, trading_pair=self.trading_pair, is_buy=True, spread=self.spread, order_amount=Decimal("1.0"), price_type=PriceType.LastTrade) self.sell_last_price_strategy: PerformTradeStrategy = PerformTradeStrategy( exchange=self.market, trading_pair=self.trading_pair, is_buy=False, spread=self.spread, order_amount=Decimal("1.0"), price_type=PriceType.LastTrade) self.buy_last_own_trade_price_strategy: PerformTradeStrategy = PerformTradeStrategy( exchange=self.market, trading_pair=self.trading_pair, is_buy=True, spread=self.spread, order_amount=Decimal("1.0"), price_type=PriceType.LastOwnTrade) self.sell_last_own_trade_price_strategy: PerformTradeStrategy = PerformTradeStrategy( exchange=self.market, trading_pair=self.trading_pair, is_buy=False, spread=self.spread, order_amount=Decimal("1.0"), price_type=PriceType.LastOwnTrade) self.clock.add_iterator(self.market) self.maker_order_fill_logger: EventLogger = EventLogger() self.cancel_order_logger: EventLogger = EventLogger() self.buy_order_completed_logger: EventLogger = EventLogger() self.sell_order_completed_logger: EventLogger = EventLogger() self.market.add_listener(MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger) self.market.add_listener(MarketEvent.SellOrderCompleted, self.sell_order_completed_logger) self.market.add_listener(MarketEvent.OrderFilled, self.maker_order_fill_logger) self.market.add_listener(MarketEvent.OrderCancelled, self.cancel_order_logger)
def setUp(self): self.logger_a = EventLogger() self.logger_b = EventLogger() for event_tag in self.events: self.wallet_a.add_listener(event_tag, self.logger_a) self.wallet_b.add_listener(event_tag, self.logger_b)
class Web3WalletUnitTest(unittest.TestCase): wallet_a: Optional[Web3Wallet] = None wallet_b: Optional[Web3Wallet] = None erc20_token: Optional[ERC20Token] = None events: List[WalletEvent] = [ WalletEvent.ReceivedAsset, WalletEvent.GasUsed, WalletEvent.TokenApproved, WalletEvent.TransactionFailure ] logger_a: EventLogger logger_b: EventLogger @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.erc20_token_address = conf.test_erc20_token_address cls.w3 = Web3(Web3.HTTPProvider(conf.test_web3_provider_list[0])) cls.wallet_a = Web3Wallet( conf.web3_test_private_key_a, conf.test_web3_provider_list, [cls.erc20_token_address]) cls.wallet_b = Web3Wallet( conf.web3_test_private_key_b, conf.test_web3_provider_list, [cls.erc20_token_address]) cls.erc20_token: ERC20Token = list(cls.wallet_a.current_backend.erc20_tokens.values())[0] cls.clock.add_iterator(cls.wallet_a) cls.clock.add_iterator(cls.wallet_b) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() next_iteration = (time.time() // 5.0 + 1) * 5 cls.ev_loop.run_until_complete(cls.clock.run_til(next_iteration)) def setUp(self): self.logger_a = EventLogger() self.logger_b = EventLogger() for event_tag in self.events: self.wallet_a.add_listener(event_tag, self.logger_a) self.wallet_b.add_listener(event_tag, self.logger_b) def tearDown(self): for event_tag in self.events: self.wallet_a.remove_listener(event_tag, self.logger_a) self.wallet_b.remove_listener(event_tag, self.logger_b) self.logger_a = None self.logger_b = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_send_balances(self): # Check the initial conditions. There should be a certain number of initial tokens before the test can be # carried out. self.assertGreater(self.wallet_a.get_balance("ETH"), 1.0) self.assertGreater(self.wallet_b.get_balance("ETH"), 1.0) self.assertGreater(self.wallet_a.get_balance("BNB"), 0.1) self.assertGreater(self.wallet_b.get_balance("BNB"), 0.1) # Send some Ether between wallets. eth_tx_hash: str = self.wallet_a.send(self.wallet_b.address, "ETH", 0.1) bnb_tx_hash: str = self.wallet_b.send(self.wallet_a.address, "BNB", 0.01) bnb_asset_received, eth_asset_received, eth_gas_used, bnb_gas_used = self.run_parallel( self.logger_a.wait_for(WalletReceivedAssetEvent), self.logger_b.wait_for(WalletReceivedAssetEvent), self.logger_a.wait_for(EthereumGasUsedEvent), self.logger_b.wait_for(EthereumGasUsedEvent) ) eth_asset_received: WalletReceivedAssetEvent = eth_asset_received eth_gas_used: EthereumGasUsedEvent = eth_gas_used self.assertEqual(eth_tx_hash, eth_asset_received.tx_hash) self.assertEqual(self.wallet_a.address, eth_asset_received.from_address) self.assertEqual(self.wallet_b.address, eth_asset_received.to_address) self.assertEqual("ETH", eth_asset_received.asset_name) self.assertEqual(0.1, eth_asset_received.amount_received) self.assertEqual(int(1e17), eth_asset_received.raw_amount_received) self.assertEqual(eth_tx_hash, eth_gas_used.tx_hash) self.assertEqual(21000, eth_gas_used.gas_used) bnb_asset_received: WalletReceivedAssetEvent = bnb_asset_received bnb_gas_used: EthereumGasUsedEvent = bnb_gas_used self.assertEqual(bnb_tx_hash, bnb_asset_received.tx_hash) self.assertEqual(self.wallet_b.address, bnb_asset_received.from_address) self.assertEqual(self.wallet_a.address, bnb_asset_received.to_address) self.assertEqual("BNB", bnb_asset_received.asset_name) self.assertEqual(0.01, bnb_asset_received.amount_received) self.assertEqual(int(1e16), bnb_asset_received.raw_amount_received) self.assertEqual(bnb_tx_hash, bnb_gas_used.tx_hash) self.assertTrue(bnb_gas_used.gas_used > 21000) # Send out the reverse transactions. self.wallet_b.send(self.wallet_a.address, "ETH", 0.1) self.wallet_a.send(self.wallet_b.address, "BNB", 0.01) def test_transaction_failure(self): # Produce a transfer failure, by not transferring more than the account has. erc20_token_contract: Contract = self.erc20_token.contract failure_hash: str = self.wallet_a.execute_transaction( erc20_token_contract.functions.transfer(self.wallet_b.address, int(1e30)), gas=500000 ) failure_tx, gas_used_event = self.run_parallel( self.logger_a.wait_for(str), self.logger_a.wait_for(EthereumGasUsedEvent) ) failure_tx: str = failure_tx gas_used_event: EthereumGasUsedEvent = gas_used_event self.assertEqual(failure_hash, failure_tx) self.assertGreater(gas_used_event.gas_used, 21000) def test_token_approval(self): approval_hash: str = self.wallet_a.approve_token_transfer(self.erc20_token.symbol, self.wallet_b.address, 1.0) approval_event, gas_used_event = self.run_parallel( self.logger_a.wait_for(TokenApprovedEvent), self.logger_a.wait_for(EthereumGasUsedEvent) ) approval_event: TokenApprovedEvent = approval_event gas_used_event: EthereumGasUsedEvent = gas_used_event self.assertEqual(approval_hash, approval_event.tx_hash) self.assertEqual(approval_hash, gas_used_event.tx_hash) self.assertEqual(self.wallet_a.address, approval_event.owner_address) self.assertEqual(self.wallet_b.address, approval_event.spender_address) self.assertEqual(self.erc20_token.symbol, approval_event.asset_name) self.assertEqual(1.0, approval_event.amount) self.assertEqual(int(1e18), approval_event.raw_amount) self.wallet_a.approve_token_transfer(self.erc20_token.symbol, self.wallet_b.address, 0.0) self.run_parallel( self.logger_a.wait_for(TokenApprovedEvent), self.logger_a.wait_for(EthereumGasUsedEvent) )
class CoinbaseProMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: CoinbaseProMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: CoinbaseProMarket = CoinbaseProMarket( conf.coinbase_pro_api_key, conf.coinbase_pro_secret_key, conf.coinbase_pro_passphrase, symbols=["ETH-USDC", "ETH-USD"] ) print("Initializing Coinbase Pro market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../coinbase_pro_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) current_ask_price: float = self.market.get_price(symbol, False) ask_price: float = current_ask_price - 0.05 * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price(symbol, ask_price) order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_sell(self): symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): self.assertGreater(self.market.get_balance("ETH"), 10) symbol = "ETH-USDC" current_bid_price: float = self.market.get_price(symbol, True) amount: float = 10 / current_bid_price bid_price: float = current_bid_price - 0.1 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): symbol = "ETH-USDC" bid_price: float = self.market.get_price(symbol, True) * 0.5 ask_price: float = self.market.get_price(symbol, False) * 2 amount: float = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price * 0.7) quantize_ask_price: Decimal = self.market.quantize_order_price(symbol, ask_price * 1.5) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() def test_deposit_info(self): [deposit_info] = self.run_parallel( self.market.get_deposit_info("ETH") ) deposit_info: DepositInfo = deposit_info self.assertIsInstance(deposit_info, DepositInfo) self.assertGreater(len(deposit_info.address), 0) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), 1) # Withdraw ZRX from Coinbase Pro to test wallet. self.market.withdraw(self.wallet.address, "ZRX", 1) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent) ) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(self.wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(1, withdraw_asset_event.amount) self.assertEqual(withdraw_asset_event.fee_amount, 0) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price * 0.8 quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) amount: float = 0.04 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: CoinbaseProMarket = CoinbaseProMarket( coinbase_pro_api_key=conf.coinbase_pro_api_key, coinbase_pro_secret_key=conf.coinbase_pro_secret_key, coinbase_pro_passphrase=conf.coinbase_pro_passphrase, symbols=["ETH-USDC", "ETH-USD"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. current_price: float = self.market.get_price(symbol, True) amount: float = 0.04 order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. amount = float(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BambooRelayOrderBookTrackerUnitTest(unittest.TestCase): order_book_tracker: Optional[BambooRelayOrderBookTracker] = None events: List[OrderBookEvent] = [ OrderBookEvent.TradeEvent ] trading_pairs: List[str] = [ "WETH-DAI", "ZRX-WETH", "WETH-USDC", "DAI-USDC" ] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.order_book_tracker: BambooRelayOrderBookTracker = BambooRelayOrderBookTracker( trading_pairs=cls.trading_pairs ) cls.order_book_tracker_task: asyncio.Task = safe_ensure_future(cls.order_book_tracker.start()) cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @classmethod async def wait_til_tracker_ready(cls): await cls.order_book_tracker._order_books_initialized.wait() async def run_parallel_async(self, *tasks, timeout=None): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) timer = 0 while not future.done(): if timeout and timer > timeout: raise Exception("Time out running parallel async task in tests.") timer += 1 await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def setUp(self): self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items(): order_book.add_listener(event_tag, self.event_logger) @unittest.skipUnless(any("test_order_book_trade_event_emission" in arg for arg in sys.argv), "test_order_book_trade_event_emission test requires waiting or manual trade.") def test_order_book_trade_event_emission(self): """ Test if order book tracker is able to retrieve order book trade message from exchange and emit order book trade events after correctly parsing the trade messages """ self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent)) for ob_trade_event in self.event_logger.event_log: self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent) self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs) self.assertTrue(type(ob_trade_event.timestamp) in [float, int]) self.assertTrue(type(ob_trade_event.amount) == float) self.assertTrue(type(ob_trade_event.price) == float) self.assertTrue(type(ob_trade_event.type) == TradeType) self.assertTrue(math.ceil(math.log10(ob_trade_event.timestamp)) == 10) self.assertTrue(ob_trade_event.amount > 0) self.assertTrue(ob_trade_event.price > 0) def test_tracker_integrity(self): # Wait 5 seconds to process some diffs. self.ev_loop.run_until_complete(asyncio.sleep(5.0)) order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books weth_dai_book: OrderBook = order_books["WETH-DAI"] zrx_weth_book: OrderBook = order_books["ZRX-WETH"] # print(weth_dai_book.snapshot) # print(zrx_weth_book.snapshot) self.assertGreaterEqual(weth_dai_book.get_price_for_volume(True, 10).result_price, weth_dai_book.get_price(True)) self.assertLessEqual(weth_dai_book.get_price_for_volume(False, 10).result_price, weth_dai_book.get_price(False)) self.assertGreaterEqual(zrx_weth_book.get_price_for_volume(True, 10).result_price, zrx_weth_book.get_price(True)) self.assertLessEqual(zrx_weth_book.get_price_for_volume(False, 10).result_price, zrx_weth_book.get_price(False))
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 RadarRelayMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderExpired, MarketEvent.OrderFilled, MarketEvent.WithdrawAsset, ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: RadarRelayMarket market_logger: EventLogger wallet_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_private_key_radar, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[ conf.mn_zerox_token_address, conf.mn_weth_token_address ], chain=EthereumChain.MAIN_NET) cls.market: RadarRelayMarket = RadarRelayMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, symbols=["ZRX-WETH"]) print("Initializing Radar Relay market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath( join(__file__, "../radar_relay_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, Decimal(20), Decimal(0.01)) self.assertEqual(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, Decimal(20)) self.assertEqual(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 1) self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), 0) self.assertGreaterEqual((balances["WETH"]), 0) def test_single_limit_order_cancel(self): symbol: str = "ZRX-WETH" current_price: float = self.market.get_price(symbol, True) amount: Decimal = Decimal(10) expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=Decimal(current_price - 0.2 * current_price), expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.assertEqual(quantized_amount, Decimal(buy_order_opened_event.amount)) self.run_parallel(self.market.cancel_order(buy_order_id)) [buy_order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) # Reset the logs self.market_logger.clear() def test_limit_buy_and_sell_and_cancel_all(self): symbol: str = "ZRX-WETH" current_price: float = self.market.get_price(symbol, True) amount: Decimal = Decimal(10) expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=Decimal(current_price - 0.2 * current_price), expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(quantized_amount, Decimal(buy_order_opened_event.amount)) self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) # Reset the logs self.market_logger.clear() sell_order_id = self.market.sell(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=Decimal(current_price + 0.2 * current_price), expiration_ts=expires) [sell_order_opened_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(quantized_amount, Decimal(sell_order_opened_event.amount)) self.assertEqual("ZRX-WETH", sell_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) [cancellation_results ] = self.run_parallel(self.market.cancel_all(60 * 5)) self.assertEqual(cancellation_results[0], CancellationResult(buy_order_id, True)) self.assertEqual(cancellation_results[1], CancellationResult(sell_order_id, True)) # Reset the logs self.market_logger.clear() def test_order_expire(self): symbol: str = "ZRX-WETH" current_price: float = self.market.get_price(symbol, True) amount: Decimal = Decimal(10) expires = int(time.time() + 60 * 2) # expires in 2 min self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=Decimal(current_price - 0.2 * current_price), expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) [buy_order_expired_event] = self.run_parallel( self.market_logger.wait_for(OrderExpiredEvent, 60 * 3)) self.assertEqual(buy_order_opened_event.order_id, buy_order_expired_event.order_id) # Reset the logs self.market_logger.clear() def test_market_buy(self): amount: Decimal = Decimal(5) quantized_amount: Decimal = self.market.quantize_order_amount( "ZRX-WETH", amount) order_id = self.market.buy("ZRX-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.market_logger.clear() def test_market_sell(self): amount: Decimal = Decimal(5) quantized_amount: Decimal = self.market.quantize_order_amount( "ZRX-WETH", amount) order_id = self.market.sell("ZRX-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.market_logger.clear() def test_wrap_eth(self): amount_to_wrap = 0.01 tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_wrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_unwrap_eth(self): amount_to_unwrap = 0.01 tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_unwrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ZRX-WETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) # Try to put limit buy order for 0.05 ETH worth of ZRX, and watch for order creation event. current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price * 0.8 quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, Decimal(bid_price)) amount: float = 0.05 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( symbol, Decimal(amount)) expires = int(time.time() + 60 * 5) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price, expiration_ts=expires) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) self.assertEqual( order_id, list(self.market.tracking_states["limit_orders"].keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertIsInstance( saved_market_states.saved_state["limit_orders"], dict) self.assertGreater( len(saved_market_states.saved_state["limit_orders"]), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: RadarRelayMarket = RadarRelayMarket( wallet=self.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, symbols=["ZRX-WETH"]) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual( 1, len(saved_market_states.saved_state["limit_orders"])) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ZRX-WETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price(symbol, True) amount: Decimal = Decimal("0.05" / current_price) order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = Decimal(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class DolomiteMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: DolomiteMarket market_logger: EventLogger wallet_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet( private_key=conf.dolomite_test_web3_private_key, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[conf.dolomite_test_web3_address], chain=EthereumChain.MAIN_NET, ) cls.market: DolomiteMarket = DolomiteMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, isTestNet=True, symbols=["WETH-DAI"], ) print("Initializing Dolomite market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../dolomite_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) # ==================================================== def test_get_fee(self): limit_trade_fee: TradeFee = self.market.get_fee( "WETH", "DAI", OrderType.LIMIT, TradeType.BUY, 10000, 1) self.assertLess(limit_trade_fee.percent, 0.01) self.assertEqual(len(limit_trade_fee.flat_fees), 0) market_trade_fee: TradeFee = self.market.get_fee( "WETH", "DAI", OrderType.MARKET, TradeType.BUY, 0.1) self.assertGreater(market_trade_fee.percent, 0) self.assertEqual(len(market_trade_fee.flat_fees), 1) self.assertEqual(market_trade_fee.flat_fees[0][0], "DAI") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["WETH"]), 0) self.assertGreaterEqual((balances["DAI"]), 0) def test_get_available_balances(self): balance = self.market.get_available_balance("WETH") self.assertGreaterEqual(balance, 0) def test_limit_orders(self): orders = self.market.limit_orders self.assertGreaterEqual(len(orders), 0) def test_cancel_order(self): symbol = "WETH-DAI" bid_price: float = self.market.get_price(symbol, True) amount = 0.5 # Intentionally setting invalid price to prevent getting filled client_order_id = self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.run_parallel(asyncio.sleep(1.0)) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.run_parallel(asyncio.sleep(6.0)) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(client_order_id, order_cancelled_event.order_id) def test_place_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("WETH"), 0.4) self.assertGreater(self.market.get_balance("DAI"), 60) # Try to buy 0.2 WETH from the exchange, and watch for creation event. symbol = "WETH-DAI" bid_price: float = self.market.get_price(symbol, True) amount: float = 0.4 buy_order_id: str = self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) [buy_order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_created_event.order_id) self.market.cancel(symbol, buy_order_id) [_] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) # Try to sell 0.2 WETH to the exchange, and watch for creation event. ask_price: float = self.market.get_price(symbol, False) sell_order_id: str = self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) [sell_order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_created_event.order_id) self.market.cancel(symbol, sell_order_id) [_] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) @unittest.skipUnless( any("test_place_market_buy_and_sell" in arg for arg in sys.argv), "test_place_market_buy_and_sell test requires manual action.", ) def test_place_market_buy_and_sell(self): # Cannot trade between yourself on Dolomite. Testing this is... hard. # These orders use the same code as limit orders except for fee calculation # and setting a field in the http request to "MARKET" instead of "LIMIT". # Fee calculations for market orders is tested above pass
def setUp(self): self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp) self.market: BacktestMarket = BacktestMarket() self.maker_data: MockOrderBookLoader = MockOrderBookLoader( *self.maker_symbols) self.mid_price = 100 self.time_delay = 15 self.cancel_order_wait_time = 45 self.maker_data.set_balanced_order_book(mid_price=self.mid_price, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) self.market.add_data(self.maker_data) self.market.set_balance("COINALPHA", 500) self.market.set_balance("WETH", 500000000000) self.market.set_balance("QETH", 500) self.market.set_quantization_param( QuantizationParams(self.maker_symbols[0], 6, 6, 6, 6)) self.market_info: MarketTradingPairTuple = MarketTradingPairTuple( *([self.market] + self.maker_symbols)) logging_options: int = ( Dev5TwapTradeStrategy.OPTION_LOG_ALL & (~Dev5TwapTradeStrategy.OPTION_LOG_NULL_ORDER_SIZE)) # Define strategies to test self.limit_buy_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy( [self.market_info], order_type="limit", order_price=Decimal("99"), cancel_order_wait_time=self.cancel_order_wait_time, is_buy=True, time_delay=self.time_delay, is_vwap=True, percent_slippage=50.0, order_percent_of_volume=0.5, order_amount=Decimal("100.0"), logging_options=logging_options) self.limit_sell_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy( [self.market_info], order_type="limit", order_price=Decimal("101"), cancel_order_wait_time=self.cancel_order_wait_time, is_buy=False, time_delay=self.time_delay, is_vwap=True, percent_slippage=50.0, order_percent_of_volume=0.5, order_amount=Decimal("100.0"), logging_options=logging_options) self.market_buy_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy( [self.market_info], order_type="market", order_price=None, cancel_order_wait_time=self.cancel_order_wait_time, is_buy=True, time_delay=self.time_delay, is_vwap=True, percent_slippage=50.0, order_percent_of_volume=0.5, order_amount=Decimal("100.0"), logging_options=logging_options) self.market_sell_strategy: Dev5TwapTradeStrategy = Dev5TwapTradeStrategy( [self.market_info], order_type="market", order_price=None, cancel_order_wait_time=self.cancel_order_wait_time, is_buy=False, time_delay=self.time_delay, is_vwap=True, percent_slippage=50.0, order_percent_of_volume=0.5, order_amount=Decimal("100.0"), logging_options=logging_options) self.logging_options = logging_options self.clock.add_iterator(self.market) self.maker_order_fill_logger: EventLogger = EventLogger() self.cancel_order_logger: EventLogger = EventLogger() self.buy_order_completed_logger: EventLogger = EventLogger() self.sell_order_completed_logger: EventLogger = EventLogger() self.market.add_listener(MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger) self.market.add_listener(MarketEvent.SellOrderCompleted, self.sell_order_completed_logger) self.market.add_listener(MarketEvent.OrderFilled, self.maker_order_fill_logger) self.market.add_listener(MarketEvent.OrderCancelled, self.cancel_order_logger)
class CryptoComOrderBookTrackerUnitTest(unittest.TestCase): order_book_tracker: Optional[CryptoComOrderBookTracker] = None events: List[OrderBookEvent] = [ OrderBookEvent.TradeEvent ] trading_pairs: List[str] = [ "BTC-USDT", "ETH-USDT", ] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.order_book_tracker: CryptoComOrderBookTracker = CryptoComOrderBookTracker(cls.trading_pairs) cls.order_book_tracker.start() cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @classmethod async def wait_til_tracker_ready(cls): while True: if len(cls.order_book_tracker.order_books) > 0: print("Initialized real-time order books.") return await asyncio.sleep(1) async def run_parallel_async(self, *tasks, timeout=None): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) timer = 0 while not future.done(): if timeout and timer > timeout: raise Exception("Timeout running parallel async tasks in tests") timer += 1 now = time.time() _next_iteration = now // 1.0 + 1 # noqa: F841 await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def setUp(self): self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items(): order_book.add_listener(event_tag, self.event_logger) def test_order_book_trade_event_emission(self): """ Tests if the order book tracker is able to retrieve order book trade message from exchange and emit order book trade events after correctly parsing the trade messages """ self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent)) for ob_trade_event in self.event_logger.event_log: self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent) self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs) self.assertTrue(type(ob_trade_event.timestamp) in [float, int]) self.assertTrue(type(ob_trade_event.amount) == float) self.assertTrue(type(ob_trade_event.price) == float) self.assertTrue(type(ob_trade_event.type) == TradeType) # datetime is in seconds self.assertTrue(math.ceil(math.log10(ob_trade_event.timestamp)) == 10) self.assertTrue(ob_trade_event.amount > 0) self.assertTrue(ob_trade_event.price > 0) def test_tracker_integrity(self): # Wait 5 seconds to process some diffs. self.ev_loop.run_until_complete(asyncio.sleep(10.0)) order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books eth_usdt: OrderBook = order_books["ETH-USDT"] self.assertIsNot(eth_usdt.last_diff_uid, 0) self.assertGreaterEqual(eth_usdt.get_price_for_volume(True, 10).result_price, eth_usdt.get_price(True)) self.assertLessEqual(eth_usdt.get_price_for_volume(False, 10).result_price, eth_usdt.get_price(False)) def test_api_get_last_traded_prices(self): prices = self.ev_loop.run_until_complete( CryptoComAPIOrderBookDataSource.get_last_traded_prices(["BTC-USDT", "LTC-BTC"])) for key, value in prices.items(): print(f"{key} last_trade_price: {value}") self.assertGreater(prices["BTC-USDT"], 1000) self.assertLess(prices["LTC-BTC"], 1)
class BittrexMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: BittrexMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = HummingWebApp.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, []) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/v3/ping", FixtureBittrex.PING) cls.web_app.update_response("get", API_BASE_URL, "/v3/markets", FixtureBittrex.MARKETS) cls.web_app.update_response("get", API_BASE_URL, "/v3/markets/tickers", FixtureBittrex.MARKETS_TICKERS) cls.web_app.update_response("get", API_BASE_URL, "/v3/balances", FixtureBittrex.BALANCES) cls.web_app.update_response("get", API_BASE_URL, "/v3/orders/open", FixtureBittrex.ORDERS_OPEN) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.market.bittrex.bittrex_market.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls._us_patcher = unittest.mock.patch( "hummingbot.market.bittrex.bittrex_api_user_stream_data_source." "BittrexAPIUserStreamDataSource._transform_raw_message", autospec=True) cls._us_mock = cls._us_patcher.start() cls._us_mock.side_effect = _transform_raw_message_patch cls._ob_patcher = unittest.mock.patch( "hummingbot.market.bittrex.bittrex_api_order_book_data_source." "BittrexAPIOrderBookDataSource._transform_raw_message", autospec=True) cls._ob_mock = cls._ob_patcher.start() cls._ob_mock.side_effect = _transform_raw_message_patch HummingWsServerFactory.url_host_only = True ws_server = HummingWsServerFactory.start_new_server(WS_BASE_URL) cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect ws_server.add_stock_response( "queryExchangeState", FixtureBittrex.WS_ORDER_BOOK_SNAPSHOT.copy()) cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BittrexMarket = BittrexMarket( bittrex_api_key=API_KEY, bittrex_secret_key=API_SECRET, trading_pairs=["ETH-USDT"]) print("Initializing Bittrex market... this will take about a minute. ") cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() cls._ob_patcher.stop() cls._us_patcher.stop() cls._ws_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../bittrex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["bittrex_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0025"), taker_fee.percent) fee_overrides_config_map["bittrex_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["bittrex_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0025"), maker_fee.percent) fee_overrides_config_map["bittrex_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, post_resp, ws_resp): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if API_MOCK_ENABLED: exch_order_id = f"BITTREX_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 self._t_nonce_mock.return_value = nonce resp = post_resp.copy() resp["id"] = exch_order_id side = 'buy' if is_buy else 'sell' resp["direction"] = side.upper() resp["type"] = order_type.name.upper() if order_type == OrderType.LIMIT: del resp["limit"] self.web_app.update_response("post", API_BASE_URL, "/v3/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = ws_resp.copy() resp["content"]["o"]["OU"] = exch_order_id HummingWsServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=1.0) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exch_order_id): if API_MOCK_ENABLED: resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id}", resp) self.market.cancel(trading_pair, order_id) def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDT" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, 1) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, 1) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price( trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.06') quantized_bid_amount: Decimal = self.market.quantize_order_amount( trading_pair, bid_amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, current_ask_price) ask_amount: Decimal = Decimal('0.06') quantized_ask_amount: Decimal = self.market.quantize_order_amount( trading_pair, ask_amount) order_id, exch_order_id_1 = self.place_order( True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) order_id2, exch_order_id_2 = self.place_order( False, trading_pair, quantized_ask_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id2, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id_1 self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id_1}", resp) resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id_2 self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id_2}", resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_taker_sell(self): trading_pair = "ETH-USDT" self.assertGreater(self.market.get_balance("ETH"), 0.06) price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price( trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, current_bid_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.cancel_order(trading_pair, order_id, exch_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price( trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.06') quantized_bid_amount: Decimal = self.market.quantize_order_amount( trading_pair, bid_amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, current_ask_price) ask_amount: Decimal = Decimal('0.06') quantized_ask_amount: Decimal = self.market.quantize_order_amount( trading_pair, ask_amount) _, exch_order_id_1 = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) _, exch_order_id_2 = self.place_order(False, trading_pair, quantized_ask_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id_1 self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id_1}", resp) resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id_2 self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id_2}", resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) current_bid_price: Decimal = self.market.get_price( trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.06') quantized_bid_amount: Decimal = self.market.quantize_order_amount( trading_pair, bid_amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BittrexMarket = BittrexMarket( bittrex_api_key=API_KEY, bittrex_secret_key=API_SECRET, trading_pairs=["XRP-BTC"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() amount = Decimal(buy_order_completed_event.base_asset_amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, 0, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class CoinbaseProMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated ] market: CoinbaseProMarket market_logger: EventLogger @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: CoinbaseProMarket = CoinbaseProMarket( ethereum_rpc_url=conf.test_web3_provider_list[0], coinbase_pro_api_key=conf.coinbase_pro_api_key, coinbase_pro_secret_key=conf.coinbase_pro_secret_key, coinbase_pro_passphrase=conf.coinbase_pro_passphrase, symbols=["ETH-USDC", "ETH-USD"]) cls.wallet: Web3Wallet = Web3Wallet( private_key=conf.web3_private_key_coinbase_pro, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[ conf.mn_weth_token_address, conf.mn_zerox_token_address ], chain=EthereumChain.MAIN_NET) print( "Initializing Coinbase Pro market... this will take about a minute." ) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.wallet) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_ask_price: float = self.market.get_price(symbol, False) ask_price: float = current_ask_price - 0.05 * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price) order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_sell(self): symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): self.assertGreater(self.market.get_balance("ETH"), 10) symbol = "ETH-USDC" current_bid_price: float = self.market.get_price(symbol, True) amount: float = 10 / current_bid_price bid_price: float = current_bid_price - 0.1 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): symbol = "ETH-USDC" bid_price: float = self.market.get_price(symbol, True) * 0.5 ask_price: float = self.market.get_price(symbol, False) * 2 amount: float = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price * 0.7) quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price * 1.5) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() @unittest.skipUnless(any("test_deposit_eth" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_eth(self): # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(self.wallet.get_balance("ETH"), 0.02) # Deposit ETH to Binance, and wait. tracking_id: str = self.market.deposit(self.wallet, "ETH", 0.01) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800)) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ETH", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(self.wallet.address, received_asset_event.from_address) self.assertAlmostEqual(0.01, received_asset_event.amount_received) @unittest.skipUnless(any("test_deposit_zrx" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_zrx(self): # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(self.wallet.get_balance("ZRX"), 1) # Deposit ZRX to Coinbase Pro, and wait. tracking_id: str = self.market.deposit(self.wallet, "ZRX", 1) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800)) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ZRX", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(self.wallet.address, received_asset_event.from_address) self.assertEqual(1, received_asset_event.amount_received) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), 1) # Withdraw ZRX from Coinbase Pro to test wallet. self.market.withdraw(self.wallet.address, "ZRX", 1) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(self.wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(1, withdraw_asset_event.amount) self.assertEqual(withdraw_asset_event.fee_amount, 0)
def setUp(self): self.clock_tick_size = 1 self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp) self.market: MockPaperExchange = MockPaperExchange() self.mid_price = 100 self.bid_spread = 0.01 self.ask_spread = 0.01 self.order_refresh_time = 30 self.market.set_balanced_order_book(trading_pair=self.trading_pair, mid_price=self.mid_price, min_price=1, max_price=200, price_step_size=1, volume_step_size=10) self.market.set_balance("HBOT", 500) self.market.set_balance("ETH", 5000) self.market.set_quantization_param( QuantizationParams(self.trading_pair, 6, 6, 6, 6)) self.market_info = MarketTradingPairTuple(self.market, self.trading_pair, self.base_asset, self.quote_asset) self.clock.add_iterator(self.market) self.maker_order_fill_logger: EventLogger = EventLogger() self.cancel_order_logger: EventLogger = EventLogger() self.market.add_listener(MarketEvent.OrderFilled, self.maker_order_fill_logger) self.market.add_listener(MarketEvent.OrderCancelled, self.cancel_order_logger) self.one_level_strategy: PureMarketMakingStrategy = PureMarketMakingStrategy( ) self.one_level_strategy.init_params(self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_refresh_time=4, filled_order_delay=8, hanging_orders_enabled=True, hanging_orders_cancel_pct=0.05, order_refresh_tolerance_pct=0) self.multi_levels_strategy: PureMarketMakingStrategy = PureMarketMakingStrategy( ) self.multi_levels_strategy.init_params( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_levels=5, order_level_spread=Decimal("0.01"), order_refresh_time=4, filled_order_delay=8, order_refresh_tolerance_pct=0) self.hanging_order_multiple_strategy = PureMarketMakingStrategy() self.hanging_order_multiple_strategy.init_params( self.market_info, bid_spread=Decimal("0.01"), ask_spread=Decimal("0.01"), order_amount=Decimal("1"), order_levels=5, order_level_spread=Decimal("0.01"), order_refresh_time=4, filled_order_delay=8, order_refresh_tolerance_pct=0, hanging_orders_enabled=True)
def setUp(self): self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger)
class WazirxExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] connector: WazirxExchange event_logger: EventLogger trading_pair = "BTC-INR" base_token, quote_token = trading_pair.split("-") stack: contextlib.ExitStack @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.connector: WazirxExchange = WazirxExchange( wazirx_api_key=API_KEY, wazirx_secret_key=API_SECRET, trading_pairs=[cls.trading_pair], trading_required=True) print("Initializing Waxirx market... this will take about a minute.") cls.clock.add_iterator(cls.connector) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls, connector=None): if connector is None: connector = cls.connector while True: now = time.time() next_iteration = now // 1.0 + 1 if connector.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../connector_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.event_logger = EventLogger() for event_tag in self.events: self.connector.add_listener(event_tag, self.event_logger) def tearDown(self): for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) self.event_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_estimate_fee(self): maker_fee = self.connector.estimate_fee_pct(True) self.assertAlmostEqual(maker_fee, Decimal("0.001")) taker_fee = self.connector.estimate_fee_pct(False) self.assertAlmostEqual(taker_fee, Decimal("0.001")) def _place_order(self, is_buy, amount, order_type, price, ex_order_id, get_order_fixture=None, ws_trade_fixture=None, ws_order_fixture=None) -> str: if is_buy: cl_order_id = self.connector.buy(self.trading_pair, amount, order_type, price) else: cl_order_id = self.connector.sell(self.trading_pair, amount, order_type, price) return cl_order_id def _cancel_order(self, cl_order_id): self.connector.cancel(self.trading_pair, cl_order_id) def test_buy_and_sell(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("1.05") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) quote_bal = self.connector.get_available_balance(self.quote_token) base_bal = self.connector.get_available_balance(self.base_token) order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1, None, fixture.WS_TRADE) order_completed_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(2)) trade_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(amount, order_completed_event.base_asset_amount) self.assertEqual("BTC", order_completed_event.base_asset) self.assertEqual("INR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.event_logger.event_log ])) # check available quote balance gets updated, we need to wait a bit for the balance message to arrive expected_quote_bal = quote_bal - quote_amount_traded self._mock_ws_bal_update(self.quote_token, expected_quote_bal) self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertAlmostEqual( expected_quote_bal, self.connector.get_available_balance(self.quote_token)) # Reset the logs self.event_logger.clear() # Try to sell back the same amount to the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("0.95") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2, None, fixture.WS_TRADE) order_completed_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) trade_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(amount, order_completed_event.base_asset_amount) self.assertEqual("BTC", order_completed_event.base_asset) self.assertEqual("INR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.event_logger.event_log ])) # check available base balance gets updated, we need to wait a bit for the balance message to arrive expected_base_bal = base_bal self._mock_ws_bal_update(self.base_token, expected_base_bal) self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertAlmostEqual( expected_base_bal, self.connector.get_available_balance(self.base_token), 5) def test_limit_makers_unfilled(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("0.8") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) quote_bal = self.connector.get_available_balance(self.quote_token) # order_id = self.connector.buy(self.trading_pair, amount, OrderType.LIMIT_MAKER, price) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1, fixture.UNFILLED_ORDER) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) # check available quote balance gets updated, we need to wait a bit for the balance message to arrive expected_quote_bal = quote_bal - (price * amount) self._mock_ws_bal_update(self.quote_token, expected_quote_bal) self.ev_loop.run_until_complete(asyncio.sleep(2)) self.assertAlmostEqual( expected_quote_bal, self.connector.get_available_balance(self.quote_token)) self._cancel_order(cl_order_id) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) price = self.connector.get_price(self.trading_pair, True) * Decimal("1.2") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2, fixture.UNFILLED_ORDER) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) self._cancel_order(cl_order_id) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) def _mock_ws_bal_update(self, token, available): # TODO: Determine best way to test balance via ws pass def test_limit_maker_rejections(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("1.2") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1, None, None, fixture.WS_ORDER_CANCELED) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) price = self.connector.get_price(self.trading_pair, False) * Decimal("0.8") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2, None, None, fixture.WS_ORDER_CANCELED) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) def test_cancel_all(self): bid_price = self.connector.get_price(self.trading_pair, True) ask_price = self.connector.get_price(self.trading_pair, False) bid_price = self.connector.quantize_order_price( self.trading_pair, bid_price * Decimal("0.7")) ask_price = self.connector.quantize_order_price( self.trading_pair, ask_price * Decimal("1.5")) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("15")) buy_id = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1) sell_id = self._place_order(False, amount, OrderType.LIMIT, ask_price, 2) self.ev_loop.run_until_complete(asyncio.sleep(1)) asyncio.ensure_future(self.connector.cancel_all(3)) self.ev_loop.run_until_complete(asyncio.sleep(3)) cancel_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderCancelledEvent) ] self.assertEqual({buy_id, sell_id}, {o.order_id for o in cancel_events}) def test_order_price_precision(self): bid_price: Decimal = self.connector.get_price(self.trading_pair, True) ask_price: Decimal = self.connector.get_price(self.trading_pair, False) mid_price: Decimal = (bid_price + ask_price) / 2 amount: Decimal = Decimal("0.000123456") # Make sure there's enough balance to make the limit orders. self.assertGreater(self.connector.get_balance("BTC"), Decimal("0.001")) self.assertGreater(self.connector.get_balance("INR"), Decimal("50")) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price = mid_price * Decimal("0.9333192292111341") ask_price = mid_price * Decimal("1.0492431474884933") cl_order_id_1 = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1, fixture.UNFILLED_ORDER) # Wait for the order created event and examine the order made self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) order = self.connector.in_flight_orders[cl_order_id_1] quantized_bid_price = self.connector.quantize_order_price( self.trading_pair, bid_price) quantized_bid_size = self.connector.quantize_order_amount( self.trading_pair, amount) self.assertEqual(quantized_bid_price, order.price) self.assertEqual(quantized_bid_size, order.amount) # Test ask order cl_order_id_2 = self._place_order(False, amount, OrderType.LIMIT, ask_price, 1, fixture.UNFILLED_ORDER) # Wait for the order created event and examine and order made self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCreatedEvent)) order = self.connector.in_flight_orders[cl_order_id_2] quantized_ask_price = self.connector.quantize_order_price( self.trading_pair, Decimal(ask_price)) quantized_ask_size = self.connector.quantize_order_amount( self.trading_pair, Decimal(amount)) self.assertEqual(quantized_ask_price, order.price) self.assertEqual(quantized_ask_size, order.amount) self._cancel_order(cl_order_id_1) self._cancel_order(cl_order_id_2) def test_orders_saving_and_restoration(self): config_path = "test_config" strategy_name = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id = None recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: self.connector._in_flight_orders.clear() self.assertEqual(0, len(self.connector.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.connector.get_price( self.trading_pair, True) price: Decimal = current_bid_price * Decimal("0.8") price = self.connector.quantize_order_price( self.trading_pair, price) amount: Decimal = Decimal("0.0001") amount = self.connector.quantize_order_amount( self.trading_pair, amount) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1, fixture.UNFILLED_ORDER) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.connector.tracking_states)) self.assertEqual(cl_order_id, list(self.connector.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.connector) self.assertEqual(1, len(recorded_orders)) self.assertEqual(cl_order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.connector) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.connector.stop(self._clock) self.ev_loop.run_until_complete(asyncio.sleep(5)) self.clock.remove_iterator(self.connector) for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) new_connector = WazirxExchange(API_KEY, API_SECRET, [self.trading_pair], True) for event_tag in self.events: new_connector.add_listener(event_tag, self.event_logger) recorder.stop() recorder = MarketsRecorder(sql, [new_connector], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, new_connector) self.clock.add_iterator(new_connector) self.assertEqual(0, len(new_connector.limit_orders)) self.assertEqual(0, len(new_connector.tracking_states)) new_connector.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(new_connector.limit_orders)) self.assertEqual(1, len(new_connector.tracking_states)) # Cancel the order and verify that the change is saved. self._cancel_order(cl_order_id) self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(new_connector.limit_orders)) self.assertEqual(0, len(new_connector.tracking_states)) saved_market_states = recorder.get_market_states( config_path, new_connector) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.connector.cancel(self.trading_pair, cl_order_id) self.run_parallel( self.event_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.connector.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_filled_orders_recorded(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id = None recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: # Try to buy some token from the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("1.05") price = self.connector.quantize_order_price( self.trading_pair, price) amount = self.connector.quantize_order_amount( self.trading_pair, Decimal("15")) order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1, None, fixture.WS_TRADE) self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(1)) # Reset the logs self.event_logger.clear() # Try to sell back the same amount to the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("0.95") price = self.connector.quantize_order_price( self.trading_pair, price) amount = self.connector.quantize_order_amount( self.trading_pair, Decimal("15")) order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2, None, fixture.WS_TRADE) self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.connector.cancel(self.trading_pair, order_id) self.run_parallel( self.event_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class PerpetualFinanceDerivativeUnitTest(unittest.TestCase): event_logger: EventLogger events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure, MarketEvent.FundingPaymentCompleted ] connector: PerpetualFinanceDerivative stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.connector: PerpetualFinanceDerivative = PerpetualFinanceDerivative( [trading_pair], "PRIVATE_KEY_HERE", "") print( "Initializing PerpetualFinanceDerivative market... this will take about a minute." ) cls.connector.set_leverage(trading_pair, leverage) cls.connector.set_position_mode(PositionMode.ONEWAY) cls.clock.add_iterator(cls.connector) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.connector.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../connector_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.event_logger = EventLogger() for event_tag in self.events: self.connector.add_listener(event_tag, self.event_logger) def test_update_balances(self): all_bals = self.connector.get_all_balances() for token, bal in all_bals.items(): print(f"{token}: {bal}") self.assertIn(quote, all_bals) self.assertTrue(all_bals["XDAI"] > 0) def test_allowances(self): asyncio.get_event_loop().run_until_complete(self._test_allowances()) async def _test_allowances(self): perfi = self.connector allowances = await perfi.get_allowances() print(allowances) def test_approve(self): asyncio.get_event_loop().run_until_complete(self._test_approve()) async def _test_approve(self): perfi = self.connector ret_val = await perfi.approve_perpetual_finance_spender() print(ret_val) def test_get_quote_price(self): asyncio.get_event_loop().run_until_complete( self._test_get_quote_price()) async def _test_get_quote_price(self): perfi = self.connector buy_price = await perfi.get_quote_price(trading_pair, True, Decimal("1")) self.assertTrue(buy_price > 0) print(f"buy_price: {buy_price}") sell_price = await perfi.get_quote_price(trading_pair, False, Decimal("1")) self.assertTrue(sell_price > 0) print(f"sell_price: {sell_price}") self.assertTrue(buy_price != sell_price) def test_open_and_close_long_position(self): perfi = self.connector amount = Decimal("0.1") price = Decimal("10") order_id = perfi.buy(trading_pair, amount, OrderType.LIMIT, price, position_action=PositionAction.OPEN) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.assertTrue(event.order_id is not None) self.assertEqual(order_id, event.order_id) self.assertEqual(event.base_asset_amount, amount) print(event.order_id) order_id = perfi.sell(trading_pair, amount, OrderType.LIMIT, price, position_action=PositionAction.CLOSE) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) self.assertTrue(event.order_id is not None) self.assertEqual(order_id, event.order_id) self.assertEqual(event.base_asset_amount, amount) print(event.order_id) def test_open_position_failure(self): perfi = self.connector # Since we don't have 1000000 xUSDC, this should trigger order failure amount = Decimal("1000000") price = Decimal("10") order_id = perfi.sell(trading_pair, amount, OrderType.LIMIT, price, position_action=PositionAction.OPEN) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, event.order_id) def test_filled_orders_recorded(self): config_path = "test_config" strategy_name = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: self.connector._in_flight_orders.clear() self.assertEqual(0, len(self.connector.tracking_states)) price: Decimal = Decimal("10") # quote_price * Decimal("0.8") price = self.connector.quantize_order_price(trading_pair, price) amount: Decimal = Decimal("0.1") amount = self.connector.quantize_order_amount(trading_pair, amount) sell_order_id = self.connector.sell( trading_pair, amount, OrderType.LIMIT, price, position_action=PositionAction.OPEN) self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(1)) price: Decimal = Decimal("10") # quote_price * Decimal("0.8") price = self.connector.quantize_order_price(trading_pair, price) buy_order_id = self.connector.buy( trading_pair, amount, OrderType.LIMIT, price, position_action=PositionAction.CLOSE) self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(1)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) # self.assertGreaterEqual(len(trade_fills), 2) fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(fills), 1) self.assertEqual(amount, Decimal(str(fills[0].amount))) # self.assertEqual(price, Decimal(str(fills[0].price))) self.assertEqual(base, fills[0].base_asset) self.assertEqual(quote, fills[0].quote_asset) self.assertEqual(sell_order_id, fills[0].order_id) self.assertEqual(trading_pair, fills[0].symbol) fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] self.assertGreaterEqual(len(fills), 1) self.assertEqual(amount, Decimal(str(fills[0].amount))) # self.assertEqual(price, Decimal(str(fills[0].price))) self.assertEqual(base, fills[0].base_asset) self.assertEqual(quote, fills[0].quote_asset) self.assertEqual(buy_order_id, fills[0].order_id) self.assertEqual(trading_pair, fills[0].symbol) finally: recorder.stop() os.unlink(self.db_path)
class RadarRelayMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderExpired, MarketEvent.OrderFilled, MarketEvent.WithdrawAsset ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: RadarRelayMarket market_logger: EventLogger wallet_logger: EventLogger @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_private_key_radar, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[ conf.mn_zerox_token_address, conf.mn_weth_token_address ], chain=EthereumChain.MAIN_NET) cls.market: RadarRelayMarket = RadarRelayMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, symbols=["ZRX-WETH"]) print("Initializing Radar Relay market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, 20, 0.01) self.assertEqual(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, 20) self.assertEqual(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 1) self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), 0) self.assertGreaterEqual((balances["WETH"]), 0) def test_single_limit_order_cancel(self): symbol: str = "ZRX-WETH" current_price: float = self.market.get_price(symbol, True) amount: float = 10 expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.assertEqual(quantized_amount, Decimal(buy_order_opened_event.amount)) self.run_parallel(self.market.cancel_order(buy_order_id)) [buy_order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) # Reset the logs self.market_logger.clear() def test_limit_buy_and_sell_and_cancel_all(self): symbol: str = "ZRX-WETH" current_price: float = self.market.get_price(symbol, True) amount: float = 10 expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(quantized_amount, Decimal(buy_order_opened_event.amount)) self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) # Reset the logs self.market_logger.clear() sell_order_id = self.market.sell(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price + 0.2 * current_price, expiration_ts=expires) [sell_order_opened_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(quantized_amount, Decimal(sell_order_opened_event.amount)) self.assertEqual("ZRX-WETH", sell_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) [cancellation_results ] = self.run_parallel(self.market.cancel_all(60 * 5)) self.assertEqual(cancellation_results[0], CancellationResult(buy_order_id, True)) self.assertEqual(cancellation_results[1], CancellationResult(sell_order_id, True)) # Reset the logs self.market_logger.clear() def test_order_expire(self): symbol: str = "ZRX-WETH" current_price: float = self.market.get_price(symbol, True) amount: float = 10 expires = int(time.time() + 60 * 3) # expires in 3 min quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual("ZRX-WETH", buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) [buy_order_expired_event] = self.run_parallel( self.market_logger.wait_for(OrderExpiredEvent, 60 * 3)) self.assertEqual(buy_order_opened_event.order_id, buy_order_expired_event.order_id) # Reset the logs self.market_logger.clear() def test_market_buy(self): amount: float = 5 quantized_amount: Decimal = self.market.quantize_order_amount( "ZRX-WETH", amount) order_id = self.market.buy("ZRX-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.market_logger.clear() def test_market_sell(self): amount: float = 5 quantized_amount: Decimal = self.market.quantize_order_amount( "ZRX-WETH", amount) order_id = self.market.sell("ZRX-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.market_logger.clear() def test_wrap_eth(self): amount_to_wrap = 0.01 tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_wrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_unwrap_eth(self): amount_to_unwrap = 0.01 tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_unwrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address)
class HuobiOrderBookTrackerUnitTest(unittest.TestCase): order_book_tracker: Optional[HuobiOrderBookTracker] = None events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent] trading_pairs: List[str] = ["btcusdt", "xrpusdt"] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.order_book_tracker: HuobiOrderBookTracker = HuobiOrderBookTracker( data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API, trading_pairs=cls.trading_pairs) cls.order_book_tracker_task: asyncio.Task = safe_ensure_future( cls.order_book_tracker.start()) cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @classmethod async def wait_til_tracker_ready(cls): while True: if len(cls.order_book_tracker.order_books) > 0: print("Initialized real-time order books.") return await asyncio.sleep(1) async def run_parallel_async(self, *tasks, timeout=None): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) timer = 0 while not future.done(): if timeout and timer > timeout: raise Exception( "Time out running parallel async task in tests.") timer += 1 await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def setUp(self): self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items( ): order_book.add_listener(event_tag, self.event_logger) def test_order_book_trade_event_emission(self): """ Test if order book tracker is able to retrieve order book trade message from exchange and emit order book trade events after correctly parsing the trade messages """ self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent)) for ob_trade_event in self.event_logger.event_log: self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent) self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs) self.assertTrue(type(ob_trade_event.timestamp) in [float, int]) self.assertTrue(type(ob_trade_event.amount) == float) self.assertTrue(type(ob_trade_event.price) == float) self.assertTrue(type(ob_trade_event.type) == TradeType) self.assertTrue( math.ceil(math.log10(ob_trade_event.timestamp)) == 10) self.assertTrue(ob_trade_event.amount > 0) self.assertTrue(ob_trade_event.price > 0) def test_tracker_integrity(self): # Wait 5 seconds to process some diffs. self.ev_loop.run_until_complete(asyncio.sleep(5.0)) order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books btcusdt_book: OrderBook = order_books["btcusdt"] xrpusdt_book: OrderBook = order_books["xrpusdt"] # print(btcusdt_book.snapshot) # print(xrpusdt_book.snapshot) self.assertGreaterEqual( btcusdt_book.get_price_for_volume(True, 10).result_price, btcusdt_book.get_price(True)) self.assertLessEqual( btcusdt_book.get_price_for_volume(False, 10).result_price, btcusdt_book.get_price(False)) self.assertGreaterEqual( xrpusdt_book.get_price_for_volume(True, 10000).result_price, xrpusdt_book.get_price(True)) self.assertLessEqual( xrpusdt_book.get_price_for_volume(False, 10000).result_price, xrpusdt_book.get_price(False))
class EterbaseMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: EterbaseMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: EterbaseMarket = EterbaseMarket(conf.eterbase_api_key, conf.eterbase_secret_key, conf.eterbase_account, trading_pairs=["ETHEUR"]) print("Initializing Eterbase market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../eterbase_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "EUR", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreaterEqual(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "EUR", OrderType.MARKET, TradeType.BUY, 1) self.assertGreaterEqual(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETHEUR" amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("EUR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): trading_pair = "ETHEUR" amount: Decimal = Decimal("0.01") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("EUR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount, 1) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.2")) trading_pair = "ETHEUR" amount: Decimal = Decimal("0.02") quantize_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.buy(trading_pair, quantize_amount, OrderType.MARKET, quantize_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) # not possible to exactly defined, as reuest is in COSTS (amount*price) self.assertAlmostEqual(quantize_amount * quantize_price, order_completed_event.quote_asset_amount, 1) sig_dig = abs( (quantize_amount * quantize_price).as_tuple().exponent) - 1 str_quant_cost = ("{0:." + str(sig_dig) + "g}").format( quantize_amount * quantize_price) str_order_cost = ("{0:." + str(sig_dig) + "g}").format( order_completed_event.quote_asset_amount) quant_cost = None order_cost = None if re.search(r'e+', str(quant_cost)): quant_cost = Decimal("{:.0f}".format(Decimal(str_quant_cost))) order_cost = Decimal("{:.0f}".format(Decimal(str_order_cost))) else: quant_cost = Decimal(str_quant_cost) order_cost = Decimal(str_order_cost) self.assertAlmostEqual(quant_cost, order_cost) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("EUR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_sell(self): trading_pair = "ETHEUR" amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.sell(trading_pair, amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.price * t.amount for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("EUR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount, 1) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETHEUR" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = 10 / current_bid_price self.assertGreater(self.market.get_balance("ETH"), amount) bid_price: Decimal = current_bid_price - Decimal( "0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) client_order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.cancel(trading_pair, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): trading_pair = "ETHEUR" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETHEUR" amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("XBASE"), Decimal('1')) # Withdraw XBASE from Eterbase to test wallet. self.market.withdraw(self.wallet.address, "XBASE", Decimal('1')) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(self.wallet.address, withdraw_asset_event.to_address) self.assertEqual("XBASE", withdraw_asset_event.asset_name) self.assertEqual(Decimal('1'), withdraw_asset_event.amount) self.assertEqual(withdraw_asset_event.fee_amount, Decimal(0)) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETHEUR" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.04") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) print("close out the current market and start another market") # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: EterbaseMarket = EterbaseMarket( eterbase_api_key=conf.eterbase_api_key, eterbase_secret_key=conf.eterbase_secret_key, eterbase_account=conf.eterbase_account, trading_pairs=["ETHUSDT", "ETHEUR"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETHEUR" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. amount: Decimal = Decimal("0.01") current_ask_price: Decimal = self.market.get_price( trading_pair, False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.buy(trading_pair, amount, OrderType.MARKET, quantize_price) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logger self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. amount = buy_order_completed_event.base_asset_amount order_id = self.market.sell(trading_pair, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BinanceOrderBookTrackerUnitTest(unittest.TestCase): order_book_tracker: Optional[BinanceOrderBookTracker] = None events: List[OrderBookEvent] = [OrderBookEvent.TradeEvent] trading_pairs: List[str] = ["BTCUSDT", "XRPUSDT"] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.order_book_tracker: BinanceOrderBookTracker = BinanceOrderBookTracker( trading_pairs=cls.trading_pairs) cls.order_book_tracker_task: asyncio.Task = safe_ensure_future( cls.order_book_tracker.start()) cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready()) @classmethod async def wait_til_tracker_ready(cls): while True: if len(cls.order_book_tracker.order_books) > 0: print("Initialized real-time order books.") return await asyncio.sleep(1) async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def setUp(self): self.event_logger = EventLogger() for event_tag in self.events: for trading_pair, order_book in self.order_book_tracker.order_books.items( ): order_book.add_listener(event_tag, self.event_logger) def test_order_book_trade_event_emission(self): """ Test if order book tracker is able to retrieve order book trade message from exchange and emit order book trade events after correctly parsing the trade messages """ self.run_parallel(self.event_logger.wait_for(OrderBookTradeEvent)) for ob_trade_event in self.event_logger.event_log: print(f"ob_trade_event: {ob_trade_event}") self.assertTrue(type(ob_trade_event) == OrderBookTradeEvent) self.assertTrue(ob_trade_event.trading_pair in self.trading_pairs) self.assertTrue(type(ob_trade_event.timestamp) == float) self.assertTrue(type(ob_trade_event.amount) == float) self.assertTrue(type(ob_trade_event.price) == float) self.assertTrue(type(ob_trade_event.type) == TradeType) self.assertTrue( math.ceil(math.log10(ob_trade_event.timestamp)) == 10) self.assertTrue(ob_trade_event.amount > 0) self.assertTrue(ob_trade_event.price > 0) def test_tracker_integrity(self): # Wait 5 seconds to process some diffs. self.ev_loop.run_until_complete(asyncio.sleep(10.0)) order_books: Dict[str, OrderBook] = self.order_book_tracker.order_books btcusdt_book: OrderBook = order_books["BTCUSDT"] xrpusdt_book: OrderBook = order_books["XRPUSDT"] # print(btcusdt_book.snapshot) # print("xrpusdt") # print(xrpusdt_book.snapshot) self.assertGreaterEqual( btcusdt_book.get_price_for_volume(True, 10).result_price, btcusdt_book.get_price(True)) self.assertLessEqual( btcusdt_book.get_price_for_volume(False, 10).result_price, btcusdt_book.get_price(False)) self.assertGreaterEqual( xrpusdt_book.get_price_for_volume(True, 10000).result_price, xrpusdt_book.get_price(True)) self.assertLessEqual( xrpusdt_book.get_price_for_volume(False, 10000).result_price, xrpusdt_book.get_price(False)) for order_book in self.order_book_tracker.order_books.values(): print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_api_get_last_traded_prices(self): prices = self.ev_loop.run_until_complete( BinanceAPIOrderBookDataSource.get_last_traded_prices( ["BTCUSDT", "LTCBTC"])) for key, value in prices.items(): print(f"{key} last_trade_price: {value}") self.assertGreater(prices["BTCUSDT"], 1000) self.assertLess(prices["LTCBTC"], 1)