class DydxExchangeUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, ] market: DydxExchange market_logger: EventLogger stack: contextlib.ExitStack base_api_url = "api.dydx.exchange" @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(cls.base_api_url, []) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._req_patcher = mock.patch.object(requests.Session, "request", autospec=True) cls._req_url_mock = cls._req_patcher.start() cls._req_url_mock.side_effect = MockWebServer.reroute_request cls.web_app.update_response("get", cls.base_api_url, f"/v1/accounts/{WALLET_ADDRESS}", FixtureDydx.BALANCES, params={'number': f'{ACCOUNT_NUMBER}'}) cls.web_app.update_response("get", cls.base_api_url, "/v2/markets", FixtureDydx.MARKETS) cls.web_app.update_response("get", cls.base_api_url, "/v1/orderbook/WETH-USDC", FixtureDydx.WETHUSDC_SNAP) cls._buy_order_exchange_id = "0xb0751a113c759779ff5fd6a53b37b26211a9\ f8845d443323b9f877f32d9aafd9" cls._sell_order_exchange_id = "0x03dfd18edc2f26fc9298edcd28ca6cad4971\ bd1f44d40253d5154b0d1f217680" cls.web_app.update_response( "delete", cls.base_api_url, f"/v2/orders/{cls._buy_order_exchange_id}", FixtureDydx.CANCEL_ORDER_BUY) cls.web_app.update_response( "delete", cls.base_api_url, f"/v2/orders/{cls._sell_order_exchange_id}", FixtureDydx.CANCEL_ORDER_SELL) ws_base_url = "wss://api.dydx.exchange/v1/ws" cls._ws_user_url = f"{ws_base_url}" MockWebSocketServerFactory.start_new_server(cls._ws_user_url) MockWebSocketServerFactory.start_new_server(f"{ws_base_url}") cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.dydx.\ dydx_exchange.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.market: DydxExchange = DydxExchange( dydx_eth_private_key=PRIVATE_KEY, dydx_node_address=NODE_ADDRESS, poll_interval=10.0, trading_pairs=['WETH-USDC'], trading_required=True) print("Initializing Dydx market... ") cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._req_patcher.stop() cls._ws_patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../dydx_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) # ==================================================== def fixture(self, fixture_data, **overwrites): data = fixture_data.copy() for key, value in overwrites.items(): if key not in data: raise Exception(f"{key} not found in fixture_data") data[key] = value return data def order_response(self, fixture_data, nonce, side, trading_pair): self._t_nonce_mock.return_value = nonce order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}" order_resp = fixture_data.copy() order_resp["order"]["clientId"] = order_id return order_resp def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, fixture_resp, fixture_ws_1=None, fixture_ws_2=None, fixture_ws_3=None): order_id = None if API_MOCK_ENABLED: resp = self.order_response(fixture_resp, nonce, 'buy' if is_buy else 'sell', trading_pair) self.web_app.update_response("post", self.base_api_url, "/v2/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED and fixture_ws_1 is not None: self.web_app.update_response("get", self.base_api_url, "/v2/fills", FixtureDydx.FILLS, params={ "orderId": order_id, "limit": 100 }) data = self.fixture(fixture_ws_1, id=order_id) MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.1) if fixture_ws_2 is not None: data = self.fixture(fixture_ws_2, id=order_id) MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, data, delay=0.11) if fixture_ws_3 is not None: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, fixture_ws_3, delay=0.1) return order_id def test_get_fee(self): limit_trade_fee: TradeFee = self.market.get_fee( "WETH", "USDC", OrderType.LIMIT_MAKER, TradeType.SELL, 10000, 1) self.assertLess(limit_trade_fee.percent, 0.01) def test_limit_buy(self): self.assertGreater(self.market.get_balance("USDC"), 16000) # Try to buy 40 ETH from the exchange, and watch for creation event. trading_pair = "WETH-USDC" amount: Decimal = Decimal("40.0") ask_price: Decimal = self.market.get_price(trading_pair, False) buy_order_id: str = self.place_order(True, "WETH-USDC", amount, OrderType.LIMIT, ask_price * Decimal('1.5'), 10001, FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1, FixtureDydx.WS_AFTER_BUY_2, FixtureDydx.WS_AFTER_BUY_3) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) self.assertEqual(buy_order_id, buy_order_completed_event.order_id) def test_limit_sell(self): self.assertGreater(self.market.get_balance("WETH"), 40) # Try to sell 40 ETH to the exchange, and watch for creation event. trading_pair = "WETH-USDC" amount: Decimal = Decimal("40.0") bid_price: Decimal = self.market.get_price(trading_pair, True) sell_order_id: str = self.place_order( False, "WETH-USDC", amount, OrderType.LIMIT, bid_price * Decimal('0.5'), 10001, FixtureDydx.SELL_LIMIT_ORDER, FixtureDydx.WS_AFTER_SELL_1, FixtureDydx.WS_AFTER_SELL_2, FixtureDydx.WS_AFTER_SELL_3) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) self.assertEqual(sell_order_id, sell_order_completed_event.order_id) def test_limit_maker_rejections(self): self.assertGreater(self.market.get_balance("WETH"), 40) trading_pair = "WETH-USDC" amount: Decimal = Decimal("40.0") bid_price: Decimal = self.market.get_price(trading_pair, True) sell_order_id: str = self.place_order( False, "WETH-USDC", amount, OrderType.LIMIT_MAKER, bid_price * Decimal('0.5'), 10001, FixtureDydx.SELL_LIMIT_MAKER_ORDER, FixtureDydx.WS_AFTER_SELL_1, FixtureDydx.LIMIT_MAKER_SELL_ERROR) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(sell_order_id, order_cancelled_event.order_id) def test_limit_makers_unfilled(self): self.assertGreater(self.market.get_balance("USDC"), 16000) trading_pair = "WETH-USDC" amount: Decimal = Decimal("40.0") bid_price: Decimal = self.market.get_price(trading_pair, True) buy_order_id: str = self.place_order(True, "WETH-USDC", amount, OrderType.LIMIT_MAKER, bid_price * Decimal('0.5'), 10001, FixtureDydx.BUY_LIMIT_MAKER_ORDER, FixtureDydx.WS_AFTER_BUY_1) self.run_parallel(asyncio.sleep(6.0)) self.market.cancel(trading_pair, buy_order_id) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) def test_market_buy(self): # Market orders not supported on Dydx pass def test_market_sell(self): # Market orders not supported on Dydx pass def test_cancel_order(self): self.assertGreater(self.market.get_balance("USDC"), 16000) trading_pair = "WETH-USDC" bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("40.0") # Intentionally setting price far away from best ask client_order_id = self.place_order(True, "WETH-USDC", amount, OrderType.LIMIT_MAKER, bid_price * Decimal('0.5'), 10001, FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1) self.run_parallel(asyncio.sleep(1.0)) self.market.cancel(trading_pair, client_order_id) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.run_parallel(asyncio.sleep(6.0)) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(client_order_id, order_cancelled_event.order_id) def test_cancel_all(self): self.assertGreater(self.market.get_balance("USDC"), 16000) trading_pair = "WETH-USDC" bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("40.0") # Intentionally setting price far away from best ask client_order_id = self.place_order(True, "WETH-USDC", amount, OrderType.LIMIT, bid_price * Decimal('0.5'), 10001, FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1) self.run_parallel(asyncio.sleep(1.0)) self.run_parallel(self.market.cancel_all(5.0)) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.run_parallel(asyncio.sleep(6.0)) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(client_order_id, order_cancelled_event.order_id) def test_limit_orders(self): self.assertGreater(self.market.get_balance("USDC"), 16000) trading_pair = "WETH-USDC" amount: Decimal = Decimal("40.0") bid_price: Decimal = self.market.get_price(trading_pair, True) buy_order_id: str = self.place_order(True, "WETH-USDC", amount, OrderType.LIMIT, bid_price * Decimal('0.5'), 10001, FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1) [buy_order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(amount, self.market.limit_orders[0].quantity) self.market.cancel(trading_pair, buy_order_id) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) def test_order_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None trading_pair: str = "WETH-USDC" recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) self.assertGreater(self.market.get_balance("USDC"), 16000) amount: Decimal = Decimal("40.0") current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = Decimal("0.5") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.place_order(True, trading_pair, amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: DydxExchange = DydxExchange( dydx_eth_private_key=PRIVATE_KEY, dydx_node_address=NODE_ADDRESS, poll_interval=10.0, trading_pairs=[trading_pair], trading_required=True) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.run_parallel(asyncio.sleep(5.0)) self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_json_threadsafe( self._ws_user_url, FixtureDydx.WS_AFTER_CANCEL_BUY, delay=0.1) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: ask_price: Decimal = self.market.get_price("WETH-USDC", True) self.assertGreater(self.market.get_balance("USDC"), 16000) amount: Decimal = Decimal('40') order_id = self.place_order( True, "WETH-USDC", amount, OrderType.LIMIT, ask_price * Decimal('1.5'), 1000100010001000, FixtureDydx.BUY_LIMIT_ORDER, FixtureDydx.WS_AFTER_BUY_1, FixtureDydx.WS_AFTER_BUY_2, FixtureDydx.WS_AFTER_BUY_3) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of LINK to the exchange, # and watch for completion event. ask_price: Decimal = self.market.get_price("WETH-USDC", False) amount = buy_order_completed_event.base_asset_amount order_id = self.place_order( False, "WETH-USDC", amount, OrderType.LIMIT, ask_price, 1000200010001000, FixtureDydx.SELL_LIMIT_ORDER, FixtureDydx.WS_AFTER_SELL_1, FixtureDydx.WS_AFTER_SELL_2, FixtureDydx.WS_AFTER_SELL_3) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel("WETH-USDC", order_id) recorder.stop() os.unlink(self.db_path)
class KrakenExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: KrakenExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: KrakenExchange = KrakenExchange(conf.kraken_api_key, conf.kraken_secret_key, trading_pairs=[PAIR]) cls.count = 0 print("Initializing Kraken market... this will take about a minute. ") cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) cls.count += 1 await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../kraken_test.sqlite")) try: unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.run_async(self.run_parallel_async(*tasks)) def run_async(self, task): return self.ev_loop.run_until_complete(task) def sleep(self, t=1.0): self.run_parallel(asyncio.sleep(t)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee( BASE, QUOTE, OrderType.LIMIT_MAKER, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee( BASE, QUOTE, OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["kraken_taker_fee"].value = None taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0026"), taker_fee.percent) fee_overrides_config_map["kraken_taker_fee"].value = Decimal('0.2') taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["kraken_maker_fee"].value = None maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0016"), maker_fee.percent) fee_overrides_config_map["kraken_maker_fee"].value = Decimal('0.5') maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price): order_id = None if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) return order_id def cancel_order(self, trading_pair, order_id): self.market.cancel(trading_pair, order_id) def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance(QUOTE), 6) trading_pair = PAIR self.sleep(3) price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) and t.amount is not None ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual(BASE, order_completed_event.base_asset) self.assertEqual(QUOTE, order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): self.assertGreater(self.market.get_balance(BASE), 0.02) trading_pair = PAIR self.sleep(3) price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT, price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) and t.amount is not None ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual(BASE, order_completed_event.base_asset) self.assertEqual(QUOTE, order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def underpriced_limit_buy(self): self.assertGreater(self.market.get_balance(QUOTE), 4) trading_pair = PAIR current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price * Decimal('0.005') quantized_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantized_bid_price) return order_id def underpriced_limit_buy_multiple(self, num): order_ids = [] for _ in range(num): order_ids.append(self.underpriced_limit_buy()) self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) return order_ids def test_cancel_order(self): order_id = self.underpriced_limit_buy() self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.cancel_order(PAIR, order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): order_ids = self.underpriced_limit_buy_multiple(2) cancelled_orders = self.run_async(self.market.cancel_all(10.)) self.assertEqual([order.order_id for order in cancelled_orders], order_ids) self.assertTrue([order.success for order in cancelled_orders]) def test_order_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.02 ETH at fraction of USDC market price, and watch for order creation event. order_id = self.underpriced_limit_buy() [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: KrakenExchange = KrakenExchange( conf.kraken_api_key, conf.kraken_secret_key, trading_pairs=[PAIR]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(PAIR, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(PAIR, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.02 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(PAIR, True) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( PAIR, amount) order_id = self.place_order(True, PAIR, quantized_amount, OrderType.LIMIT, price) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(PAIR, False) amount = buy_order_completed_event.base_asset_amount quantized_amount: Decimal = self.market.quantize_order_amount( PAIR, amount) order_id = self.place_order(False, PAIR, quantized_amount, OrderType.LIMIT, price) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel(PAIR, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() unlink(self.db_path) def test_pair_convesion(self): for pair in self.market.trading_rules: exchange_pair = convert_to_exchange_trading_pair(pair) self.assertTrue(exchange_pair in self.market.order_books)
class LiquidExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: LiquidExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(API_HOST, ["/products", "/currencies"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_HOST, "/fiat_accounts", FixtureLiquid.FIAT_ACCOUNTS) cls.web_app.update_response("get", API_HOST, "/crypto_accounts", FixtureLiquid.CRYPTO_ACCOUNTS) cls.web_app.update_response("get", API_HOST, "/orders", FixtureLiquid.ORDERS_GET) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.liquid.liquid_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: LiquidExchange = LiquidExchange( API_KEY, API_SECRET, poll_interval=5, trading_pairs=['CEL-ETH'], ) # cls.ev_loop.run_until_complete(cls.market._update_balances()) print("Initializing Liquid market... this will take about a minute.") cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../liquid_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USD", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal(4000)) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USD", OrderType.LIMIT, TradeType.BUY, Decimal(1)) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USD", OrderType.LIMIT_MAKER, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["liquid_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["liquid_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["liquid_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["liquid_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, order_resp, get_resp): order_id, exchange_id = None, None if API_MOCK_ENABLED: side = 'buy' if is_buy else 'sell' self._t_nonce_mock.return_value = nonce order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}" resp = order_resp.copy() resp["client_order_id"] = order_id exchange_id = resp["id"] self.web_app.update_response("post", API_HOST, "/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = get_resp.copy() resp["models"][-1]["client_order_id"] = order_id self.web_app.update_response("get", API_HOST, "/orders", resp) return order_id, exchange_id async def cancel_all_open_orders(self): listed_orders = await self.market.list_orders() live_orders = [ o for o in listed_orders.get("models", []) if o["status"] == "live" ] for order in live_orders: path_url = Constants.CANCEL_ORDER_URI.format( exchange_order_id=str(order["id"])) res = await self.market._api_request("put", path_url=path_url) print(res) def test_maintain_user_balances(self): # self.ev_loop.run_until_complete(self.cancel_all_open_orders()) # return trading_pair = "CEL-ETH" base = trading_pair.split("-")[0] quote = trading_pair.split("-")[1] base_bal = self.market.get_available_balance(base) starting_quote_bal = self.market.get_available_balance(quote) print(f"{base} available: {base_bal}") print(f"starting quote available: {starting_quote_bal}") bid_price = self.market.get_price(trading_pair, False) buy_price = bid_price * Decimal("0.9") buy_price = self.market.quantize_order_price(trading_pair, buy_price) amount = Decimal("1") post_data = FixtureLiquid.BUY_MARKET_ORDER.copy() get_data = FixtureLiquid.ORDERS_UNFILLED.copy() if API_MOCK_ENABLED: resp = FixtureLiquid.CRYPTO_ACCOUNTS.copy() resp[0]["reserved_balance"] = float((buy_price * amount)) self.web_app.update_response("get", API_HOST, "/crypto_accounts", resp) order_id_1, exchange_id_1 = self.place_order(True, "CEL-ETH", amount, OrderType.LIMIT, buy_price, 10001, post_data, get_data) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) print(f"order_created_event: {order_created_event}") self.assertEqual(order_id_1, order_created_event.order_id) # ToDo: the test from here on pass fine in real API test mode, for the API mocked we first need to fix # https://github.com/CoinAlpha/hummingbot/issues/2222 if API_MOCK_ENABLED: return base_bal = self.market.get_available_balance(base) quote_bal = self.market.get_available_balance(quote) expected_quote_bal = starting_quote_bal - (buy_price * amount) self.assertAlmostEqual(quote_bal, expected_quote_bal, 5) print(f"{base} available: {base_bal}") print(f"{quote} available: {quote_bal}") self.run_parallel(asyncio.sleep(5)) post_data = FixtureLiquid.BUY_MARKET_ORDER.copy() get_data = FixtureLiquid.ORDERS_UNFILLED.copy() get_data["models"].append(get_data["models"][0].copy()) get_data["models"][0]["client_order_id"] = order_id_1 get_data["models"][1]["id"] = get_data["models"][0]["id"] + 1 if API_MOCK_ENABLED: resp = FixtureLiquid.CRYPTO_ACCOUNTS.copy() resp[0]["reserved_balance"] = float((2 * buy_price * amount)) self.web_app.update_response("get", API_HOST, "/crypto_accounts", resp) order_id_2, exchange_id_2 = self.place_order(True, "CEL-ETH", amount, OrderType.LIMIT, buy_price, 10002, post_data, get_data) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) print(f"order_created_event: {order_created_event}") self.assertEqual(order_id_2, order_created_event.order_id) base_bal = self.market.get_available_balance(base) quote_bal = self.market.get_available_balance(quote) expected_quote_bal = starting_quote_bal - 2 * (buy_price * amount) self.assertAlmostEqual(quote_bal, expected_quote_bal, 5) print(f"{base} available: {base_bal}") print(f"{quote} available: {quote_bal}") if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL self.web_app.update_response( "put", API_HOST, f"/orders/{str(exchange_id_2)}/cancel", order_cancel_resp) resp = FixtureLiquid.CRYPTO_ACCOUNTS.copy() resp[0]["reserved_balance"] = float((buy_price * amount)) self.web_app.update_response("get", API_HOST, "/crypto_accounts", resp) self.market.cancel("CEL-ETH", order_id_2) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) quote_bal = self.market.get_available_balance(quote) expected_quote_bal = starting_quote_bal - 1 * (buy_price * amount) print(f"expected_quote_bal: {expected_quote_bal}") print(f"quote_bal: {quote_bal}") self.assertAlmostEqual(quote_bal, expected_quote_bal, 5) if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL self.web_app.update_response( "put", API_HOST, f"/orders/{str(exchange_id_1)}/cancel", order_cancel_resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) def test_limit_taker_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.01")) current_price: Decimal = self.market.get_price("CEL-ETH", True) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "CEL-ETH", amount) order_id, _ = self.place_order(True, "CEL-ETH", amount, OrderType.LIMIT, current_price, 10001, FixtureLiquid.BUY_MARKET_ORDER, FixtureLiquid.ORDERS_GET_AFTER_BUY) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("CEL", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of CEL to the exchange, and watch for completion event. current_price: Decimal = self.market.get_price("CEL-ETH", False) amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id, _ = self.place_order( False, "CEL-ETH", amount, OrderType.LIMIT, current_price, 10002, FixtureLiquid.SELL_MARKET_ORDER, FixtureLiquid.ORDERS_GET_AFTER_MARKET_SELL) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("CEL", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "CEL-ETH" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, 1) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, 1) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): trading_pair = "CEL-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) buy_order_id, buy_exchange_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureLiquid.BUY_LIMIT_ORDER_BEFORE_CANCEL, FixtureLiquid.ORDERS_GET_AFTER_BUY) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, order_created_event.order_id) sell_order_id, sell_exchange_id = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureLiquid.SELL_LIMIT_ORDER_BEFORE_CANCEL, FixtureLiquid.ORDERS_GET_AFTER_MARKET_SELL) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL self.web_app.update_response( "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel", order_cancel_resp) order_cancel_resp = FixtureLiquid.BUY_LIMIT_ORDER_AFTER_CANCEL self.web_app.update_response( "put", API_HOST, f"/orders/{str(sell_exchange_id)}/cancel", order_cancel_resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_cancel_all(self): trading_pair = "CEL-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) _, buy_exchange_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureLiquid.BUY_LIMIT_ORDER_BEFORE_CANCEL, FixtureLiquid.ORDERS_GET_AFTER_BUY) _, sell_exchange_id = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureLiquid.SELL_LIMIT_ORDER_BEFORE_CANCEL, FixtureLiquid.ORDERS_GET_AFTER_MARKET_SELL) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.SELL_LIMIT_ORDER_AFTER_CANCEL self.web_app.update_response( "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel", order_cancel_resp) order_cancel_resp = FixtureLiquid.BUY_LIMIT_ORDER_AFTER_CANCEL self.web_app.update_response( "put", API_HOST, f"/orders/{str(sell_exchange_id)}/cancel", order_cancel_resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.005 ETH worth of CEL, and watch for order creation event. current_bid_price: Decimal = self.market.get_price("CEL-ETH", True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( "CEL-ETH", bid_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "CEL-ETH", amount) order_id, buy_exchange_id = self.place_order( True, "CEL-ETH", quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureLiquid.ORDER_SAVE_RESTORE, FixtureLiquid.ORDERS_GET_AFTER_BUY) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: LiquidExchange = LiquidExchange( API_KEY, API_SECRET, trading_pairs=['ETH-USD', 'CEL-ETH']) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.ORDER_CANCEL_SAVE_RESTORE.copy( ) self.web_app.update_response( "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel", order_cancel_resp) self.market.cancel("CEL-ETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel("CEL-ETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 1 CEL from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price("CEL-ETH", True) amount: Decimal = 1 order_id, _ = self.place_order( True, "CEL-ETH", amount, OrderType.LIMIT, current_price, 10001, FixtureLiquid.FILLED_BUY_LIMIT_ORDER, FixtureLiquid.ORDERS_GET_AFTER_LIMIT_BUY) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of CEL to the exchange, and watch for completion event. current_price: Decimal = self.market.get_price("CEL-ETH", False) amount = buy_order_completed_event.base_asset_amount order_id, _ = self.place_order( False, "CEL-ETH", amount, OrderType.LIMIT, current_price, 10002, FixtureLiquid.FILLED_SELL_LIMIT_ORDER, FixtureLiquid.ORDERS_GET_AFTER_LIMIT_SELL) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel("CEL-ETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price))
class BambooRelayMarketCoordinatedUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderExpired, MarketEvent.OrderFilled, MarketEvent.WithdrawAsset ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: BambooRelayMarket market_logger: EventLogger wallet_logger: EventLogger @classmethod def setUpClass(cls): if conf.test_bamboo_relay_chain_id == 3: chain = EthereumChain.ROPSTEN elif conf.test_bamboo_relay_chain_id == 4: chain = EthereumChain.RINKEBY elif conf.test_bamboo_relay_chain_id == 42: chain = EthereumChain.KOVAN else: chain = EthereumChain.MAIN_NET cls.chain = chain cls.base_token_symbol = conf.test_bamboo_relay_base_token_symbol cls.quote_token_symbol = conf.test_bamboo_relay_quote_token_symbol cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_private_key_bamboo, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[conf.test_bamboo_relay_base_token_address, conf.test_bamboo_relay_quote_token_address], chain=chain) cls.market: BambooRelayMarket = BambooRelayMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API, symbols=[conf.test_bamboo_relay_base_token_symbol + "-" + conf.test_bamboo_relay_quote_token_symbol], use_coordinator=True, pre_emptive_soft_cancels=True ) print("Initializing Bamboo Relay market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../bamboo_relay_uncordinated_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee("ZRX", self.quote_token_symbol, OrderType.LIMIT, TradeType.BUY, 20, 0.01) self.assertEqual(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee("ZRX", self.quote_token_symbol, OrderType.MARKET, TradeType.BUY, 20) self.assertEqual(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 1) self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), 0) self.assertGreaterEqual((balances[self.quote_token_symbol]), 0) def test_single_limit_order_cancel(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol current_price: float = self.market.get_price(symbol, True) amount: float = 0.01 expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(self.base_token_symbol + "-" + self.quote_token_symbol, buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.assertEqual(float(quantized_amount), float(buy_order_opened_event.amount)) self.run_parallel(self.market.cancel_order(buy_order_id)) [buy_order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) # Reset the logs self.market_logger.clear() def test_limit_buy_and_sell_and_cancel_all(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol current_price: float = self.market.get_price(symbol, True) amount: float = 0.01 expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(float(quantized_amount), float(buy_order_opened_event.amount)) self.assertEqual(self.base_token_symbol + "-" + self.quote_token_symbol, buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) # Reset the logs self.market_logger.clear() current_price: float = self.market.get_price(symbol, False) sell_order_id = self.market.sell(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price + 0.2 * current_price, expiration_ts=expires) [sell_order_opened_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(float(quantized_amount), float(sell_order_opened_event.amount)) self.assertEqual(self.base_token_symbol + "-" + self.quote_token_symbol, sell_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) [cancellation_results, order_cancelled_event] = self.run_parallel(self.market.cancel_all(60 * 5), self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cancellation_results[0], CancellationResult(buy_order_id, True)) self.assertEqual(cancellation_results[1], CancellationResult(sell_order_id, True)) # Wait for the order book source to also register the cancellation self.assertTrue((buy_order_opened_event.order_id == order_cancelled_event.order_id or sell_order_opened_event.order_id == order_cancelled_event.order_id)) # Reset the logs self.market_logger.clear() def test_order_pre_emptive_cancel(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol current_price: float = self.market.get_price(symbol, True) amount: float = 0.03 expires = int(time.time() + 60) # expires in 1 min quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(self.base_token_symbol + "-" + self.quote_token_symbol, buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) [buy_order_expired_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent, 75)) self.assertEqual(buy_order_opened_event.order_id, buy_order_expired_event.order_id) # Reset the logs self.market_logger.clear() def test_market_buy(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.buy(self.base_token_symbol + "-" + self.quote_token_symbol, amount, OrderType.MARKET) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] self.assertTrue([evt.order_type == OrderType.MARKET for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_symbol, order_completed_event.base_asset) self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset) self.market_logger.clear() def test_batch_market_buy(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol amount: float = 0.02 current_price: float = self.market.get_price(symbol, False) expires = int(time.time() + 60 * 5) sell_order_id = self.market.sell(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price - 0.2 * current_price, expiration_ts=expires) [sell_order_opened_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) amount: float = 0.04 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.buy(self.base_token_symbol + "-" + self.quote_token_symbol, amount, OrderType.MARKET) [order_completed_event, sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent), self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] self.assertTrue([evt.order_type == OrderType.MARKET for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_symbol, order_completed_event.base_asset) self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset) self.market_logger.clear() def test_market_sell(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol amount: float = 0.01 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] self.assertTrue([evt.order_type == OrderType.MARKET for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_symbol, order_completed_event.base_asset) self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset) self.market_logger.clear() def test_batch_market_sell(self): symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol amount: float = 0.02 current_price: float = self.market.get_price(symbol, True) expires = int(time.time() + 60 * 5) buy_order_id = self.market.buy(symbol=symbol, amount=amount, order_type=OrderType.LIMIT, price=current_price + 0.2 * current_price, expiration_ts=expires) [buy_order_opened_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) amount: float = 0.05 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.sell(self.base_token_symbol + "-" + self.quote_token_symbol, amount, OrderType.MARKET) [order_completed_event, buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent), self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] self.assertTrue([evt.order_type == OrderType.MARKET for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual(self.base_token_symbol, order_completed_event.base_asset) self.assertEqual(self.quote_token_symbol, order_completed_event.quote_asset) self.market_logger.clear() def test_wrap_eth(self): amount_to_wrap = 0.01 tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel(self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_wrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_unwrap_eth(self): amount_to_unwrap = 0.01 tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel(self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_unwrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_z_orders_saving_and_restoration(self): self.market.reset_state() config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) # Try to put limit buy order for 0.05 Quote Token worth of Base Token, and watch for order creation event. current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price * 0.8 quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) amount: float = 0.05 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) expires = int(time.time() + 60 * 5) order_id = self.market.buy(symbol, float(quantized_amount), OrderType.LIMIT, float(quantize_bid_price), expiration_ts=expires) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) self.assertEqual(order_id, list(self.market.tracking_states["limit_orders"].keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertIsInstance(saved_market_states.saved_state["limit_orders"], dict) self.assertGreater(len(saved_market_states.saved_state["limit_orders"]), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: BambooRelayMarket = BambooRelayMarket( wallet=self.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API, symbols=[conf.test_bamboo_relay_base_token_symbol + "-" + conf.test_bamboo_relay_quote_token_symbol], use_coordinator=True, pre_emptive_soft_cancels=True ) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(1, len(saved_market_states.saved_state["limit_orders"])) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = self.base_token_symbol + "-" + self.quote_token_symbol sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event. current_price: float = self.market.get_price(symbol, True) amount: float = 0.05 / current_price order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = float(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class KucoinExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: KucoinExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, ["/api/v1/timestamp", "/api/v1/symbols", "/api/v1/bullet-public", "/api/v2/market/orderbook/level2"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/api/v1/accounts", FixtureKucoin.BALANCES) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.kucoin.kucoin_exchange.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls._exch_order_id = 20001 cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready and cls.market_2.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../kucoin_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market_2_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) self.market_2.add_listener(event_tag, self.market_2_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_2.remove_listener(event_tag, self.market_2_logger) self.market_logger = None self.market_2_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def order_response(self, fixture_data, nonce): self._t_nonce_mock.return_value = nonce order_resp = fixture_data.copy() return order_resp def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, post_resp, get_resp): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if API_MOCK_ENABLED: exch_order_id = f"KUCOIN_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 resp = self.order_response(post_resp, nonce) resp["data"]["orderId"] = exch_order_id self.web_app.update_response("post", API_BASE_URL, "/api/v1/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = get_resp.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) return order_id, exch_order_id def test_fee_overrides_config(self): fee_overrides_config_map["kucoin_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["kucoin_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["kucoin_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["kucoin_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDT" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDT" bid_price = self.market.get_price(trading_pair, True) * Decimal("0.8") quantized_bid_price = self.market.quantize_order_price(trading_pair, bid_price) quantized_bid_amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id, _ = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER, quantized_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.ORDER_GET_BUY_UNMATCHED) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) ask_price = self.market.get_price(trading_pair, True) * Decimal("1.2") quatized_ask_price = self.market.quantize_order_price(trading_pair, ask_price) quatized_ask_amount = self.market.quantize_order_amount(trading_pair, Decimal(0.01)) order_id, _ = self.place_order(False, trading_pair, quatized_ask_amount, OrderType.LIMIT_MAKER, quatized_ask_price, 10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.ORDER_GET_SELL_UNMATCHED) [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(0.01) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.BUY_MARKET_ORDER) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("ETH", buy_order_completed_event.base_asset) self.assertEqual("USDT", buy_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(buy_order_completed_event.base_asset_amount), places=4) self.assertAlmostEqual(quote_amount_traded, float(buy_order_completed_event.quote_asset_amount), places=4) self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_limit_taker_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal(0.011) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.SELL_MARKET_ORDER) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), sell_order_completed_event.base_asset_amount) self.assertEqual("ETH", sell_order_completed_event.base_asset) self.assertEqual("USDT", sell_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(sell_order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(sell_order_completed_event.quote_asset_amount)) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_cancel(self): trading_pair = "ETH-USDT" current_price: float = self.market.get_price(trading_pair, False) amount: Decimal = Decimal(0.01) price: Decimal = Decimal(current_price) * Decimal(1.1) quantized_price: Decimal = self.market.quantize_order_price(trading_pair, price) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantized_price, 10001, FixtureKucoin.ORDER_PLACE_2, FixtureKucoin.OPEN_SELL_LIMIT_ORDER) [order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) if API_MOCK_ENABLED: resp = FixtureKucoin.CANCEL_ORDER.copy() resp["data"]["cancelledOrderIds"] = [exch_order_id] self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = FixtureKucoin.GET_CANCELLED_ORDER.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) self.market_logger.clear() def test_cancel_all(self): trading_pair = "ETH-USDT" bid_price: Decimal = Decimal(self.market_2.get_price(trading_pair, True)) ask_price: Decimal = Decimal(self.market_2.get_price(trading_pair, False)) amount: Decimal = Decimal(0.01) quantized_amount: Decimal = self.market_2.quantize_order_amount(trading_pair, amount) # Intentionally setting high price to prevent getting filled quantize_bid_price: Decimal = self.market_2.quantize_order_price(trading_pair, bid_price * Decimal(0.8)) quantize_ask_price: Decimal = self.market_2.quantize_order_price(trading_pair, ask_price * Decimal(1.2)) _, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_BUY_LIMIT_ORDER) _, exch_order_id2 = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_SELL_LIMIT_ORDER) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = FixtureKucoin.ORDERS_BATCH_CANCELLED.copy() resp["data"]["cancelledOrderIds"] = [exch_order_id, exch_order_id2] self.web_app.update_response("delete", API_BASE_URL, "/api/v1/orders", resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) self.market_2_logger.clear() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: float = self.market.get_price(trading_pair, True) bid_price: Decimal = Decimal(current_bid_price * Decimal(0.8)) quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) amount: Decimal = Decimal(0.04) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.OPEN_BUY_LIMIT_ORDER) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: KucoinExchange = KucoinExchange( kucoin_api_key=API_KEY, kucoin_passphrase=API_PASSPHRASE, kucoin_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) if API_MOCK_ENABLED: resp = FixtureKucoin.CANCEL_ORDER.copy() resp["data"]["cancelledOrderIds"] = exch_order_id self.web_app.update_response("delete", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = FixtureKucoin.GET_CANCELLED_ORDER.copy() resp["data"]["id"] = exch_order_id resp["data"]["clientOid"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/api/v1/orders/{exch_order_id}", resp) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) self.market_logger.clear() def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.01 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(0.01) order_id, _ = self.place_order(True, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureKucoin.ORDER_PLACE, FixtureKucoin.BUY_MARKET_ORDER) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal(buy_order_completed_event.base_asset_amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10002, FixtureKucoin.ORDER_PLACE, FixtureKucoin.SELL_MARKET_ORDER) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) self.market_logger.clear() def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertFalse(math.isnan(order_book.last_trade_price))
class OkexExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: OkexExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def strip_host_from_okex_url(cls, url): HOST = "https://www.okex.com" return url.split(HOST)[-1] @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if MOCK_API_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, []) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local # mock_account_id = FixtureOKEx.GET_ACCOUNTS["data"][0]["id"] # warning: second parameter starts with / cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_INSTRUMENTS_URL), FixtureOKEx.OKEX_INSTRUMENTS_URL) cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_PRICE_URL).format( trading_pair='ETH-USDT'), FixtureOKEx.INSTRUMENT_TICKER) cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_DEPTH_URL).format( trading_pair='ETH-USDT'), FixtureOKEx.OKEX_ORDER_BOOK) cls.web_app.update_response( "get", API_BASE_URL, cls.strip_host_from_okex_url(OKEX_TICKERS_URL), FixtureOKEx.OKEX_TICKERS) cls.web_app.update_response("get", API_BASE_URL, '/' + OKEX_BALANCE_URL, FixtureOKEx.OKEX_BALANCE_URL) cls.web_app.update_response("get", API_BASE_URL, '/' + OKEX_SERVER_TIME, FixtureOKEx.TIMESTAMP) # cls.web_app.update_response("POST", API_BASE_URL, '/' + OKEX_PLACE_ORDER, FixtureOKEx.ORDER_PLACE) # cls.web_app.update_response("get", OKEX_BASE_URL, f"/v1/account/accounts/{mock_account_id}/balance", # FixtureOKEx.GET_BALANCES) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.okex.okex_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: OkexExchange = OkexExchange(API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=["ETH-USDT"]) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: OkexExchange = OkexExchange(API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=["ETH-USDT"]) cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() if MOCK_API_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready and cls.market_2.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../okex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market_2_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) self.market_2.add_listener(event_tag, self.market_2_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_2.remove_listener(event_tag, self.market_2_logger) self.market_logger = None self.market_2_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, 1, 10) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["okex_taker_fee"].value = None taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0015"), taker_fee.percent) fee_overrides_config_map["okex_taker_fee"].value = Decimal('0.1') taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["okex_maker_fee"].value = None maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["okex_maker_fee"].value = Decimal('0.5') maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, get_resp, market_connector=None): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if MOCK_API_ENABLED: exch_order_id = f"OKEX_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 self._t_nonce_mock.return_value = nonce resp = FixtureOKEx.ORDER_PLACE.copy() resp["data"][0]["ordId"] = exch_order_id # resp = exch_order_id side = 'buy' if is_buy else 'sell' order_id = f"{side}-{trading_pair}-{nonce}" self.web_app.update_response("post", API_BASE_URL, "/" + OKEX_PLACE_ORDER, resp) market = self.market if market_connector is None else market_connector if is_buy: order_id = market.buy(trading_pair, amount, order_type, price) else: order_id = market.sell(trading_pair, amount, order_type, price) if MOCK_API_ENABLED: resp = get_resp.copy() # resp is the response passed by parameter resp["data"][0]["ordId"] = exch_order_id resp["data"][0]["clOrdId"] = order_id self.web_app.update_response( "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format( ordId=exch_order_id, trading_pair="ETH-USDT"), resp) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exchange_order_id, get_resp): if MOCK_API_ENABLED: resp = FixtureOKEx.ORDER_CANCEL.copy() resp["data"][0]["ordId"] = exchange_order_id resp["data"][0]["clOrdId"] = order_id self.web_app.update_response("post", API_BASE_URL, '/' + OKEX_ORDER_CANCEL, resp, params={"ordId": exchange_order_id}) self.market.cancel(trading_pair, order_id) if MOCK_API_ENABLED: resp = get_resp.copy() resp["data"][0]["ordId"] = exchange_order_id resp["data"][0]["clOrdId"] = order_id self.web_app.update_response( "get", API_BASE_URL, '/' + OKEX_ORDER_DETAILS_URL.format( ordId=exchange_order_id, trading_pair="ETH-USDT"), resp) def test_limit_maker_rejections(self): if MOCK_API_ENABLED: return trading_pair = "ETH-USDT" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.06")) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.06")) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if MOCK_API_ENABLED: return trading_pair = "ETH-USDT" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.1")) order_id1, exch_order_id1 = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id1, order_created_event.order_id) order_id2, exch_order_id2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureOKEx.ORDER_GET_LIMIT_SELL_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id2, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) if MOCK_API_ENABLED: resp = FixtureOKEx.ORDERS_BATCH_CANCELLED.copy() resp["data"]["success"] = [exch_order_id1, exch_order_id2] self.web_app.update_response("post", API_BASE_URL, "/v1/order/orders/batchcancel", resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) # Reset the logs self.market_logger.clear() def test_limit_taker_buy(self): trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureOKEx.ORDER_GET_MARKET_BUY) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("ETH", buy_order_completed_event.base_asset) self.assertEqual("USDT", buy_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, buy_order_completed_event.base_asset_amount, places=4) self.assertAlmostEqual(quote_amount_traded, buy_order_completed_event.quote_asset_amount, places=4) self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_taker_sell(self): trading_pair = "ETH-USDT" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureOKEx.ORDER_GET_MARKET_SELL) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, sell_order_completed_event.base_asset_amount) self.assertEqual("ETH", sell_order_completed_event.base_asset) self.assertEqual("USDT", sell_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, sell_order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, sell_order_completed_event.quote_asset_amount) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.05") bid_price: Decimal = current_bid_price - Decimal( "0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.cancel_order(trading_pair, order_id, exch_order_id, FixtureOKEx.ORDER_GET_CANCELED) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): trading_pair = "ETH-USDT" bid_price: Decimal = self.market_2.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market_2.get_price(trading_pair, False) * 2 amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market_2.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market_2.quantize_order_price( trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market_2.quantize_order_price( trading_pair, ask_price * Decimal("1.1")) _, exch_order_id1 = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 1001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED, self.market_2) _, exch_order_id2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 1002, FixtureOKEx.ORDER_GET_LIMIT_BUY_FILLED, self.market_2) self.run_parallel(asyncio.sleep(1)) if MOCK_API_ENABLED: resp = FixtureOKEx.ORDERS_BATCH_CANCELLED.copy() resp["data"][0]["ordId"] = exch_order_id1 self.web_app.update_response("post", API_BASE_URL, '/' + OKEX_BATCH_ORDER_CANCEL, resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, '0') def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureOKEx.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event # self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: OkexExchange = OkexExchange( API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=["ETH-USDT", "BTC-USDT"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id, FixtureOKEx.ORDER_GET_CANCELED) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.06") order_id, _ = self.place_order(True, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureOKEx.ORDER_GET_MARKET_BUY) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount = buy_order_completed_event.base_asset_amount order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, price, 10002, FixtureOKEx.ORDER_GET_MARKET_SELL) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertFalse(math.isnan(order_book.last_trade_price))
class BittrexMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: BittrexMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BittrexMarket = BittrexMarket( bittrex_api_key=conf.bittrex_api_key, bittrex_secret_key=conf.bittrex_secret_key, trading_pairs=["LTC-ETH", "XRP-ETH"]) print("Initializing Bittrex market... this will take about a minute. ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../bittrex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("LTC", "ETH", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("LTC", "ETH", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) trading_pair = "LTC-ETH" amount: Decimal = Decimal('0.02') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal( '0.005') * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LTC", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): trading_pair = "LTC-ETH" amount: Decimal = Decimal('0.02') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) ask_price: Decimal = current_ask_price - Decimal( '0.005') * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LTC", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) trading_pair = "LTC-ETH" amount: Decimal = Decimal('0.02') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LTC", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_sell(self): trading_pair = "LTC-ETH" self.assertGreater(self.market.get_balance("LTC"), 0.01) amount: Decimal = Decimal('0.02') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.sell(trading_pair, amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LTC", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "XRP-ETH" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal('0.1') / current_bid_price bid_price: Decimal = current_bid_price - Decimal( '0.1') * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) client_order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.market.cancel(trading_pair, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): trading_pair = "XRP-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.5') ask_price: Decimal = self.market.get_price(trading_pair, False) * Decimal('2') bid_amount: Decimal = Decimal('0.01') / bid_price ask_amount: Decimal = Decimal( '3.64495247') # Min. trade size in XRP-ETH as of 30 Sep 2019 quantized_bid_amount: Decimal = self.market.quantize_order_amount( trading_pair, bid_amount) quantized_ask_amount: Decimal = self.market.quantize_order_amount( trading_pair, ask_amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(bid_price * Decimal('0.7'))) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(ask_price * Decimal('1.5'))) self.market.buy(trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(trading_pair, quantized_ask_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) # @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) trading_pair = "LTC-ETH" amount: Decimal = Decimal('0.02') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = Decimal('0.7') * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() def test_deposit_info(self): [deposit_info] = self.run_parallel(self.market.get_deposit_info("ETH")) deposit_info: DepositInfo = deposit_info self.assertIsInstance(deposit_info, DepositInfo) self.assertGreater(len(deposit_info.address), 0) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), 1) # Withdraw ZRX from Bittrex to test wallet. self.market.withdraw(self.wallet.address, "ZRX", 1) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(self.wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(1, withdraw_asset_event.amount) self.assertEqual(withdraw_asset_event.fee_amount, 0) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "LTC-ETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal('0.8') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal('0.04') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BittrexMarket = BittrexMarket( bittrex_api_key=conf.bittrex_api_key, bittrex_secret_key=conf.bittrex_secret_key, trading_pairs=["LTC-ETH", "XRP-ETH"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "LTC-ETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. amount: Decimal = Decimal('0.04') order_id = self.market.buy(trading_pair, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. amount = Decimal(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(trading_pair, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class HuobiMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: HuobiMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = HummingWebApp.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, ["/v1/common/timestamp", "/v1/common/symbols", "/market/tickers", "/market/depth"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local mock_account_id = FixtureHuobi.GET_ACCOUNTS["data"][0]["id"] cls.web_app.update_response("get", API_BASE_URL, "/v1/account/accounts", FixtureHuobi.GET_ACCOUNTS) cls.web_app.update_response("get", API_BASE_URL, f"/v1/account/accounts/{mock_account_id}/balance", FixtureHuobi.GET_BALANCES) cls._t_nonce_patcher = unittest.mock.patch("hummingbot.market.huobi.huobi_market.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: HuobiMarket = HuobiMarket( API_KEY, API_SECRET, trading_pairs=["ethusdt"] ) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: HuobiMarket = HuobiMarket( API_KEY, API_SECRET, trading_pairs=["ethusdt"] ) cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready and cls.market_2.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../huobi_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market_2_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) self.market_2.add_listener(event_tag, self.market_2_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_2.remove_listener(event_tag, self.market_2_logger) self.market_logger = None self.market_2_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.SELL, 1, 10) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["huobi_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["huobi_taker_fee"].value = Decimal('0.1') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["huobi_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent) fee_overrides_config_map["huobi_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, get_resp, market_connector=None): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if API_MOCK_ENABLED: exch_order_id = f"HUOBI_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 self._t_nonce_mock.return_value = nonce resp = FixtureHuobi.ORDER_PLACE.copy() resp["data"] = exch_order_id side = 'buy' if is_buy else 'sell' order_id = f"{side}-{trading_pair}-{nonce}" self.web_app.update_response("post", API_BASE_URL, "/v1/order/orders/place", resp, params={"client-order-id": order_id}) market = self.market if market_connector is None else market_connector if is_buy: order_id = market.buy(trading_pair, amount, order_type, price) else: order_id = market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = get_resp.copy() resp["data"]["id"] = exch_order_id resp["data"]["client-order-id"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/v1/order/orders/{exch_order_id}", resp) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exchange_order_id, get_resp): global EXCHANGE_ORDER_ID if API_MOCK_ENABLED: resp = FixtureHuobi.ORDER_PLACE.copy() resp["data"] = exchange_order_id self.web_app.update_response("post", API_BASE_URL, f"/v1/order/orders/{exchange_order_id}/submitcancel", resp) self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = get_resp.copy() resp["data"]["id"] = exchange_order_id resp["data"]["client-order-id"] = order_id self.web_app.update_response("get", API_BASE_URL, f"/v1/order/orders/{exchange_order_id}", resp) def test_limit_buy(self): trading_pair = "ethusdt" amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal("0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureHuobi.ORDER_GET_LIMIT_BUY_FILLED) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("eth", order_completed_event.base_asset) self.assertEqual("usdt", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): trading_pair = "ethusdt" amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) ask_price: Decimal = current_ask_price - Decimal("0.05") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, ask_price) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.LIMIT, quantize_ask_price, 10001, FixtureHuobi.ORDER_GET_LIMIT_SELL_FILLED) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("eth", order_completed_event.base_asset) self.assertEqual("usdt", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_market_buy(self): trading_pair = "ethusdt" amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.MARKET, 0, 10001, FixtureHuobi.ORDER_GET_MARKET_BUY) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("eth", buy_order_completed_event.base_asset) self.assertEqual("usdt", buy_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, buy_order_completed_event.base_asset_amount, places=4) self.assertAlmostEqual(quote_amount_traded, buy_order_completed_event.quote_asset_amount, places=4) self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_market_sell(self): trading_pair = "ethusdt" amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.MARKET, 0, 10001, FixtureHuobi.ORDER_GET_MARKET_SELL) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, sell_order_completed_event.base_asset_amount) self.assertEqual("eth", sell_order_completed_event.base_asset) self.assertEqual("usdt", sell_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, sell_order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, sell_order_completed_event.quote_asset_amount) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ethusdt" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.05") bid_price: Decimal = current_bid_price - Decimal("0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureHuobi.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.cancel_order(trading_pair, order_id, exch_order_id, FixtureHuobi.ORDER_GET_CANCELED) [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): trading_pair = "ethusdt" bid_price: Decimal = self.market_2.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market_2.get_price(trading_pair, False) * 2 amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market_2.quantize_order_amount(trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market_2.quantize_order_price(trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market_2.quantize_order_price(trading_pair, ask_price * Decimal("1.1")) _, exch_order_id1 = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 1001, FixtureHuobi.ORDER_GET_LIMIT_BUY_UNFILLED, self.market_2) _, exch_order_id2 = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price, 1002, FixtureHuobi.ORDER_GET_LIMIT_SELL_UNFILLED, self.market_2) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = FixtureHuobi.ORDERS_BATCH_CANCELLED.copy() resp["data"]["success"] = [exch_order_id1, exch_order_id2] self.web_app.update_response("post", API_BASE_URL, "/v1/order/orders/batchcancel", resp) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureHuobi.ORDER_GET_LIMIT_BUY_UNFILLED) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: HuobiMarket = HuobiMarket( huobi_api_key=API_KEY, huobi_secret_key=API_SECRET, trading_pairs=["ethusdt", "btcusdt"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id, FixtureHuobi.ORDER_GET_CANCELED) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. amount: Decimal = Decimal("0.06") order_id, _ = self.place_order(True, trading_pair, amount, OrderType.MARKET, 0, 10001, FixtureHuobi.ORDER_GET_MARKET_BUY) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. amount = buy_order_completed_event.base_asset_amount order_id, _ = self.place_order(False, trading_pair, amount, OrderType.MARKET, 0, 10002, FixtureHuobi.ORDER_GET_MARKET_SELL) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class EterbaseMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: EterbaseMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: EterbaseMarket = EterbaseMarket(conf.eterbase_api_key, conf.eterbase_secret_key, conf.eterbase_account, trading_pairs=["ETH-EUR"]) print("Initializing Eterbase market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../eterbase_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "EUR", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 1) self.assertGreaterEqual(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "EUR", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreaterEqual(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_maker_rejections(self): trading_pair = "ETH-EUR" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.01")) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.01")) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): trading_pair = "ETH-EUR" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled. quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) order_id_2 = self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id_2, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.2")) trading_pair = "ETH-EUR" amount: Decimal = Decimal("0.02") quantize_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.buy(trading_pair, quantize_amount, OrderType.LIMIT, quantize_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) # not possible to exactly defined, as reuest is in COSTS (amount*price) self.assertAlmostEqual(quantize_amount * quantize_price, order_completed_event.quote_asset_amount, 1) sig_dig = abs( (quantize_amount * quantize_price).as_tuple().exponent) - 1 str_quant_cost = ("{0:." + str(sig_dig) + "g}").format( quantize_amount * quantize_price) str_order_cost = ("{0:." + str(sig_dig) + "g}").format( order_completed_event.quote_asset_amount) quant_cost = None order_cost = None if re.search(r'e+', str(quant_cost)): quant_cost = Decimal("{:.0f}".format(Decimal(str_quant_cost))) order_cost = Decimal("{:.0f}".format(Decimal(str_order_cost))) else: quant_cost = Decimal(str_quant_cost) order_cost = Decimal(str_order_cost) self.assertAlmostEqual(quant_cost, order_cost) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("EUR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_limit_taker_sell(self): trading_pair = "ETH-EUR" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT, price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.price * t.amount for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("EUR", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount, 1) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-EUR" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = 10 / current_bid_price self.assertGreater(self.market.get_balance("ETH"), amount) bid_price: Decimal = current_bid_price - Decimal( "0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) client_order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) self.market.cancel(trading_pair, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): trading_pair = "ETH-EUR" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETH-EUR" amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-EUR" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.04") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) print("close out the current market and start another market") # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: EterbaseMarket = EterbaseMarket( eterbase_api_key=conf.eterbase_api_key, eterbase_secret_key=conf.eterbase_secret_key, eterbase_account=conf.eterbase_account, trading_pairs=["ETHUSDT", "ETH-EUR"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-EUR" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. amount: Decimal = Decimal("0.01") current_ask_price: Decimal = self.market.get_price( trading_pair, False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT, quantize_price) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logger self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount = buy_order_completed_event.base_asset_amount order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT, price) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_pair_convesion(self): for pair in self.market.trading_rules: exchange_pair = convert_to_exchange_trading_pair(pair) self.assertTrue(exchange_pair in self.market.order_books)
class DDEXMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: DDEXMarket market_logger: EventLogger wallet_logger: EventLogger @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_test_private_key_ddex, backend_urls=conf.test_ddex_web3_provider_list, erc20_token_addresses=[ conf.test_ddex_erc20_token_address_1, conf.test_ddex_erc20_token_address_2 ], chain=EthereumChain.MAIN_NET) cls.market: DDEXMarket = DDEXMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_ddex_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, symbols=["HOT-WETH"]) print("Initializing DDEX market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): weth_trade_fee: TradeFee = self.market.get_fee("ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, 10000, 1) self.assertGreater(weth_trade_fee.percent, 0) self.assertEqual(len(weth_trade_fee.flat_fees), 1) self.assertEqual(weth_trade_fee.flat_fees[0][0], "WETH") dai_trade_fee: TradeFee = self.market.get_fee("WETH", "DAI", OrderType.MARKET, TradeType.BUY, 10000) self.assertGreater(dai_trade_fee.percent, 0) self.assertEqual(len(dai_trade_fee.flat_fees), 1) self.assertEqual(dai_trade_fee.flat_fees[0][0], "DAI") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), 0) self.assertGreaterEqual((balances["WETH"]), 0) def test_list_orders(self): [orders] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(orders), 0) def test_list_locked_balances(self): [locked_balances ] = self.run_parallel(self.market.list_locked_balances()) self.assertGreaterEqual(len(locked_balances), 0) @unittest.skipUnless( any("test_bad_orders_are_not_tracked" in arg for arg in sys.argv), "bad_orders_are_not_tracked test requires manual action.") def test_bad_orders_are_not_tracked(self): # Should fail due to insufficient balance order_id = self.market.buy("WETH-DAI", 10000, OrderType.LIMIT, 1) self.assertEqual(self.market.in_flight_orders.get(order_id), None) def test_cancel_order(self): symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) amount = 2000 # Intentionally setting invalid price to prevent getting filled client_order_id = self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.market.cancel(symbol, client_order_id) self.run_parallel(asyncio.sleep(5)) self.assertEqual(self.market.in_flight_orders.get(client_order_id), None) def test_place_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("WETH"), 0.01) # Try to buy 2000 HOT from the exchange, and watch for completion event. symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) amount: float = 2000 buy_order_id: str = self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.run_parallel(asyncio.sleep(3)) exchange_order_id: str = self.market.in_flight_orders.get( buy_order_id).exchange_order_id buy_order = self.run_parallel(self.market.get_order(exchange_order_id)) self.assertEqual(buy_order[0].get('id'), exchange_order_id) self.market.cancel(symbol, buy_order_id) # Try to sell back the same amount of HOT to the exchange, and watch for completion event. ask_price: float = self.market.get_price(symbol, False) sell_order_id: str = self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) self.run_parallel(asyncio.sleep(3)) exchange_order_id: str = self.market.in_flight_orders.get( sell_order_id).exchange_order_id sell_order = self.run_parallel( self.market.get_order(exchange_order_id)) self.assertEqual(sell_order[0].get('id'), exchange_order_id) self.market.cancel(symbol, sell_order_id) @unittest.skipUnless( any("test_limit_buy_and_sell_get_matched" in arg for arg in sys.argv), "test_limit_buy_and_sell_get_matched test requires manual action.") def test_limit_buy_and_sell_get_matched(self): self.assertGreater(self.market.get_balance("WETH"), 0.01) # Try to buy 0.01 WETH worth of HOT from the exchange, and watch for completion event. current_price: float = self.market.get_price("HOT-WETH", True) amount: float = 2000 quantized_amount: Decimal = self.market.quantize_order_amount( "HOT-WETH", amount) order_id = self.market.buy("HOT-WETH", amount, OrderType.LIMIT, current_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of HOT to the exchange, and watch for completion event. current_price: float = self.market.get_price("HOT-WETH", False) amount = float(order_completed_event.base_asset_amount) quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("HOT-WETH", amount, OrderType.LIMIT, current_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_market_buy_and_sell(self): self.assertGreater(self.market.get_balance("WETH"), 0.01) amount: float = 2000.0 # Min order size is 1000 HOT quantized_amount: Decimal = self.market.quantize_order_amount( "HOT-WETH", amount) order_id = self.market.buy("HOT-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) # This is because some of the tokens are deducted in the trading fees. self.assertTrue( float(quantized_amount) > order_completed_event.base_asset_amount > float(quantized_amount) * 0.9) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of HOT to the exchange, and watch for completion event. amount = float(order_completed_event.base_asset_amount) quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("HOT-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) @unittest.skipUnless(any("test_wrap_eth" in arg for arg in sys.argv), "Wrap Eth test requires manual action.") def test_wrap_eth(self): amount_to_wrap = 0.01 tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_wrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) @unittest.skipUnless(any("test_unwrap_eth" in arg for arg in sys.argv), "Unwrap Eth test requires manual action.") def test_unwrap_eth(self): amount_to_unwrap = 0.01 tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_unwrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_cancel_all_happy_case(self): symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) ask_price: float = self.market.get_price(symbol, False) amount = 2000 self.assertGreater(self.market.get_balance("WETH"), 0.02) self.assertGreater(self.market.get_balance("HOT"), amount) # Intentionally setting invalid price to prevent getting filled self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) [cancellation_results] = self.run_parallel(self.market.cancel_all(10)) print(cancellation_results) self.assertGreater(len(cancellation_results), 0) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_cancel_all_failure_case(self): symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) ask_price: float = self.market.get_price(symbol, False) # order submission should fail due to insufficient balance amount = 200000 self.assertLess(self.market.get_balance("WETH"), 100) self.assertLess(self.market.get_balance("HOT"), amount) self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) [cancellation_results] = self.run_parallel(self.market.cancel_all(10)) print(cancellation_results) self.assertGreater(len(cancellation_results), 0) for cr in cancellation_results: self.assertEqual(cr.success, False)
class BinanceMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, ] market: BinanceMarket market_logger: EventLogger @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BinanceMarket = BinanceMarket( MAINNET_RPC_URL, conf.binance_api_key, conf.binance_api_secret, order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType .EXCHANGE_API, symbols=["ZRXETH", "LOOMETH", "IOSTETH"]) print("Initializing Binance market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.LIMIT, TradeType.BUY, 1, 4000) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT, TradeType.SELL, 1, 4000) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) # Try to buy 0.02 ETH worth of ZRX from the exchange, and watch for completion event. current_price: float = self.market.get_price("ZRXETH", True) amount: float = 0.02 / current_price quantized_amount: Decimal = self.market.quantize_order_amount( "ZRXETH", amount) order_id = self.market.buy("ZRXETH", amount) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = float(order_completed_event.base_asset_amount) quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("ZRXETH", amount) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for completion event. current_bid_price: float = self.market.get_price("ZRXETH", True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( "ZRXETH", bid_price) amount: float = 0.02 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( "ZRXETH", amount) order_id = self.market.buy("ZRXETH", quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to put limit sell order for 0.02 ETH worth of ZRX, and watch for completion event. current_ask_price: float = self.market.get_price("ZRXETH", False) ask_price: float = current_ask_price - 0.05 * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( "ZRXETH", ask_price) amount = float(order_completed_event.base_asset_amount) quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("ZRXETH", amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) @unittest.skipUnless(any("test_deposit_eth" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_eth(self): with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd: zrx_abi: str = fd.read() local_wallet: MockWallet = MockWallet( conf.web3_test_private_key_a, MAINNET_RPC_URL, {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi}, chain_id=1) # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(local_wallet.get_balance("ETH"), 0.02) # Deposit ETH to Binance, and wait. tracking_id: str = self.market.deposit(local_wallet, "ETH", 0.01) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800)) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ETH", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(local_wallet.address, received_asset_event.from_address) self.assertAlmostEqual(0.01, received_asset_event.amount_received) @unittest.skipUnless(any("test_deposit_zrx" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_zrx(self): with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd: zrx_abi: str = fd.read() local_wallet: MockWallet = MockWallet( conf.web3_test_private_key_a, MAINNET_RPC_URL, {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi}, chain_id=1) # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(local_wallet.get_balance("ZRX"), 1) # Deposit ZRX to Binance, and wait. tracking_id: str = self.market.deposit(local_wallet, "ZRX", 1) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800)) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ZRX", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(local_wallet.address, received_asset_event.from_address) self.assertEqual(1, received_asset_event.amount_received) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd: zrx_abi: str = fd.read() local_wallet: MockWallet = MockWallet( conf.web3_test_private_key_a, MAINNET_RPC_URL, {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi}, chain_id=1) # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), 10) # Withdraw ZRX from Binance to test wallet. self.market.withdraw(local_wallet.address, "ZRX", 10) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event print(withdraw_asset_event) self.assertEqual(local_wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(10, withdraw_asset_event.amount) self.assertGreater(withdraw_asset_event.fee_amount, 0) def test_cancel_all(self): symbol = "LOOMETH" bid_price: float = self.market.get_price(symbol, True) ask_price: float = self.market.get_price(symbol, False) amount: float = 0.02 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price * 0.7) quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price * 1.5) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_order_price_precision(self): # As of the day this test was written, the min order size (base) is 1 IOST, the min order size (quote) is # 0.01 ETH, and order step size is 1 IOST. symbol = "IOSTETH" bid_price: float = self.market.get_price(symbol, True) ask_price: float = self.market.get_price(symbol, False) mid_price: float = (bid_price + ask_price) / 2 amount: float = 0.02 / mid_price binance_client = self.market.binance_client # Make sure there's enough balance to make the limit orders. self.assertGreater(self.market.get_balance("ETH"), 0.1) self.assertGreater(self.market.get_balance("IOST"), amount * 2) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price: float = mid_price * 0.3333192292111341 ask_price: float = mid_price * 3.4392431474884933 # This is needed to get around the min quote amount limit. bid_amount: float = 0.02 / bid_price # Test bid order bid_order_id: str = self.market.buy(symbol, bid_amount, OrderType.LIMIT, bid_price) # Wait for the order created event and examine the order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent, timeout_seconds=10)) order_data: Dict[str, any] = binance_client.get_order( symbol=symbol, origClientOrderId=bid_order_id) quantized_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) bid_size_quantum: Decimal = self.market.get_order_size_quantum( symbol, bid_amount) self.assertEqual(quantized_bid_price, Decimal(order_data["price"])) self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0) # Test ask order ask_order_id: str = self.market.sell(symbol, amount, OrderType.LIMIT, ask_price) # Wait for the order created event and examine and order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent, timeout_seconds=10)) order_data = binance_client.get_order(symbol=symbol, origClientOrderId=ask_order_id) quantized_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price) quantized_ask_size: Decimal = self.market.quantize_order_amount( symbol, amount) self.assertEqual(quantized_ask_price, Decimal(order_data["price"])) self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"])) # Cancel all the orders [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_server_time_offset(self): BinanceTime.get_instance().SERVER_TIME_OFFSET_CHECK_INTERVAL = 3.0 self.run_parallel(asyncio.sleep(60)) with patch("hummingbot.market.binance.binance_market.time" ) as market_time: def delayed_time(): return time.time() - 30.0 market_time.time = delayed_time self.run_parallel(asyncio.sleep(5.0)) time_offset = BinanceTime.get_instance().time_offset_ms print("offest", time_offset) # check if it is less than 5% off self.assertTrue(time_offset > 0) self.assertTrue(abs(time_offset - 30.0 * 1e3) < 1.5 * 1e3)
class LiquidMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: LiquidMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.ev_loop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = HummingWebApp.get_instance() cls.web_app.add_host_to_mock(API_HOST, ["/products", "/currencies"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_HOST, "/fiat_accounts", FixtureLiquid.FIAT_ACCOUNTS) cls.web_app.update_response("get", API_HOST, "/crypto_accounts", FixtureLiquid.CRYPTO_ACCOUNTS) cls.web_app.update_response("get", API_HOST, "/orders", FixtureLiquid.ORDERS_GET) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.market.liquid.liquid_market.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: LiquidMarket = LiquidMarket( API_KEY, API_SECRET, order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType .EXCHANGE_API, trading_pairs=['CEL-ETH']) # cls.ev_loop.run_until_complete(cls.market._update_balances()) print("Initializing Liquid market... this will take about a minute.") cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../liquid_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USD", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal(4000)) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USD", OrderType.MARKET, TradeType.BUY, Decimal(1)) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USD", OrderType.LIMIT, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["liquid_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["liquid_taker_fee"].value = Decimal('0.002') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["liquid_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["liquid_maker_fee"].value = Decimal('0.005') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, order_resp, get_resp): order_id, exchange_id = None, None if API_MOCK_ENABLED: side = 'buy' if is_buy else 'sell' self._t_nonce_mock.return_value = nonce order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}" resp = order_resp.copy() resp["client_order_id"] = order_id exchange_id = resp["id"] self.web_app.update_response("post", API_HOST, "/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = get_resp.copy() resp["models"][0]["client_order_id"] = order_id self.web_app.update_response("get", API_HOST, "/orders", resp) return order_id, exchange_id def test_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) current_price: Decimal = self.market.get_price("CEL-ETH", True) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "CEL-ETH", amount) order_id, _ = self.place_order(True, "CEL-ETH", amount, OrderType.MARKET, current_price, 10001, FixtureLiquid.ORDER_BUY, FixtureLiquid.ORDERS_GET_AFTER_BUY) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("CEL", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of CEL to the exchange, and watch for completion event. amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id, _ = self.place_order(False, "CEL-ETH", amount, OrderType.MARKET, current_price, 10002, FixtureLiquid.ORDER_SELL, FixtureLiquid.ORDERS_GET_AFTER_SELL) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("CEL", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.001")) # Try to put limit buy order for 0.05 ETH worth of CEL, and watch for completion event. current_bid_price: Decimal = self.market.get_price("CEL-ETH", True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( "CEL-ETH", bid_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "CEL-ETH", amount) order_id, _ = self.place_order( True, "CEL-ETH", quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureLiquid.ORDER_BUY_LIMIT, FixtureLiquid.ORDERS_GET_AFTER_LIMIT_BUY) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("CEL", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to put limit sell order for 0.05 ETH worth of CEL, and watch for completion event. current_ask_price: Decimal = self.market.get_price("CEL-ETH", False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( "CEL-ETH", ask_price) quantized_amount = order_completed_event.base_asset_amount order_id, _ = self.place_order( False, "CEL-ETH", quantized_amount, OrderType.LIMIT, quantize_ask_price, 10002, FixtureLiquid.ORDER_SELL_LIMIT, FixtureLiquid.ORDERS_GET_AFTER_SELL_LIMIT) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("CEL", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_deposit_info(self): [deposit_info] = self.run_parallel(self.market.get_deposit_info("ETH")) deposit_info: DepositInfo = deposit_info self.assertIsInstance(deposit_info, DepositInfo) self.assertGreater(len(deposit_info.address), 0) self.assertGreater(len(deposit_info.extras), 0) self.assertTrue("currency_type" in deposit_info.extras.get('extras')) self.assertEqual("ETH", deposit_info.extras.get('extras').get('currency')) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # CEL_ABI contract file can be found in # https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code with open(realpath(join(__file__, "../../../data/CELABI.json"))) as fd: zrx_abi: str = fd.read() local_wallet: MockWallet = MockWallet( conf.web3_test_private_key_a, conf.test_web3_provider_list[0], {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi}, chain_id=1) # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("CEL"), Decimal('10')) # Withdraw CEL from Liquid to test wallet. self.market.withdraw(local_wallet.address, "CEL", Decimal('10')) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(local_wallet.address, withdraw_asset_event.to_address) self.assertEqual("CEL", withdraw_asset_event.asset_name) self.assertEqual(Decimal('10'), withdraw_asset_event.amount) self.assertGreater(withdraw_asset_event.fee_amount, Decimal(0)) def test_cancel_all(self): trading_pair = "CEL-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) _, buy_exchange_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureLiquid.ORDER_BUY_CANCEL_ALL, FixtureLiquid.ORDERS_GET_AFTER_BUY) _, sell_exchange_id = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price, 10002, FixtureLiquid.ORDER_SELL_CANCEL_ALL, FixtureLiquid.ORDERS_GET_AFTER_SELL) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.ORDER_CANCEL_ALL_1 self.web_app.update_response( "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel", order_cancel_resp) order_cancel_resp = FixtureLiquid.ORDER_CANCEL_ALL_2 self.web_app.update_response( "put", API_HOST, f"/orders/{str(sell_exchange_id)}/cancel", order_cancel_resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.005 ETH worth of CEL, and watch for order creation event. current_bid_price: Decimal = self.market.get_price("CEL-ETH", True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( "CEL-ETH", bid_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "CEL-ETH", amount) order_id, buy_exchange_id = self.place_order( True, "CEL-ETH", quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureLiquid.ORDER_SAVE_RESTORE, FixtureLiquid.ORDERS_GET_AFTER_BUY) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: LiquidMarket = LiquidMarket( API_KEY, API_SECRET, order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, user_stream_tracker_data_source_type= UserStreamTrackerDataSourceType.EXCHANGE_API, trading_pairs=['ETH-USD', 'CEL-ETH']) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. if API_MOCK_ENABLED: order_cancel_resp = FixtureLiquid.ORDER_CANCEL_SAVE_RESTORE.copy( ) self.web_app.update_response( "put", API_HOST, f"/orders/{str(buy_exchange_id)}/cancel", order_cancel_resp) self.market.cancel("CEL-ETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel("CEL-ETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 1 CEL from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price("CEL-ETH", True) amount: Decimal = 1 order_id, _ = self.place_order( True, "CEL-ETH", amount, OrderType.MARKET, current_price, 10001, FixtureLiquid.ORDER_BUY_LIMIT, FixtureLiquid.ORDERS_GET_AFTER_LIMIT_BUY) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of CEL to the exchange, and watch for completion event. amount = buy_order_completed_event.base_asset_amount order_id, _ = self.place_order( False, "CEL-ETH", amount, OrderType.MARKET, current_price, 10002, FixtureLiquid.ORDER_SELL_LIMIT, FixtureLiquid.ORDERS_GET_AFTER_SELL_LIMIT) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel("CEL-ETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BinanceExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: BinanceExchange market_logger: EventLogger stack: contextlib.ExitStack base_api_url = "api.binance.com" @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.ev_loop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(cls.base_api_url, ["/api/v1/ping", "/api/v1/time", "/api/v1/ticker/24hr"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls._req_patcher = unittest.mock.patch.object(requests.Session, "request", autospec=True) cls._req_url_mock = cls._req_patcher.start() cls._req_url_mock.side_effect = MockWebServer.reroute_request cls.web_app.update_response("get", cls.base_api_url, "/api/v3/account", FixtureBinance.BALANCES) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/exchangeInfo", FixtureBinance.MARKETS) cls.web_app.update_response("get", cls.base_api_url, "/wapi/v3/tradeFee.html", FixtureBinance.TRADE_FEES) cls.web_app.update_response("post", cls.base_api_url, "/api/v1/userDataStream", FixtureBinance.LISTEN_KEY) cls.web_app.update_response("put", cls.base_api_url, "/api/v1/userDataStream", FixtureBinance.LISTEN_KEY) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/depth", FixtureBinance.LINKETH_SNAP, params={'symbol': 'LINKETH'}) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/depth", FixtureBinance.ZRXETH_SNAP, params={'symbol': 'ZRXETH'}) cls.web_app.update_response("get", cls.base_api_url, "/api/v3/myTrades", {}, params={'symbol': 'ZRXETH'}) cls.web_app.update_response("get", cls.base_api_url, "/api/v3/myTrades", {}, params={'symbol': 'LINKETH'}) ws_base_url = "wss://stream.binance.com:9443/ws" cls._ws_user_url = f"{ws_base_url}/{FixtureBinance.LISTEN_KEY['listenKey']}" MockWebSocketServerFactory.start_new_server(cls._ws_user_url) MockWebSocketServerFactory.start_new_server(f"{ws_base_url}/linketh@depth/zrxeth@depth") cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.binance.binance_exchange.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.current_nonce = 1000000000000000 cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BinanceExchange = BinanceExchange(API_KEY, API_SECRET, ["LINK-ETH", "ZRX-ETH"], True) print("Initializing Binance market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._req_patcher.stop() cls._ws_patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../binance_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market._current_trade_fills = set() self.market._exchange_order_ids = dict() self.ev_loop.run_until_complete(self.wait_til_ready()) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) @classmethod def get_current_nonce(cls): cls.current_nonce += 1 return cls.current_nonce def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal(4000)) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT, TradeType.BUY, Decimal(1)) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT_MAKER, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["binance_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["binance_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["binance_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["binance_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def test_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) bid_price: Decimal = self.market.get_price("LINK-ETH", True) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount("LINK-ETH", amount) order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, self.get_current_nonce(), FixtureBinance.BUY_MARKET_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) self.market.add_exchange_order_ids_from_market_recorder({str(FixtureBinance.BUY_MARKET_ORDER['orderId']): "buy-LINKETH-1580093594011279"}) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. ask_price: Decimal = self.market.get_price("LINK-ETH", False) amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT, ask_price, 10002, FixtureBinance.SELL_MARKET_ORDER, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) self.market.add_exchange_order_ids_from_market_recorder({str(FixtureBinance.SELL_MARKET_ORDER['orderId']): "sell-LINKETH-1580194659898896"}) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) def test_limit_maker_rejections(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price("LINK-ETH", True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) order_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, self.get_current_nonce(), FixtureBinance.LIMIT_MAKER_ERROR) [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price("LINK-ETH", True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) order_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, self.get_current_nonce(), FixtureBinance.LIMIT_MAKER_ERROR) [order_failure_event] = self.run_parallel(self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): price = self.market.get_price("LINK-ETH", True) * Decimal("0.8") price = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) buy_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, self.get_current_nonce(), FixtureBinance.OPEN_BUY_ORDER) [buy_order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) buy_order_created_event: BuyOrderCreatedEvent = buy_order_created_event self.assertEqual(buy_id, buy_order_created_event.order_id) price = self.market.get_price("LINK-ETH", True) * Decimal("1.2") price = self.market.quantize_order_price("LINK-ETH", price) amount = self.market.quantize_order_amount("LINK-ETH", 1) sell_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT_MAKER, price, self.get_current_nonce(), FixtureBinance.OPEN_SELL_ORDER) [sell_order_created_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) sell_order_created_event: BuyOrderCreatedEvent = sell_order_created_event self.assertEqual(sell_id, sell_order_created_event.order_id) if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=buy_id, side="BUY") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': buy_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=sell_id, side="SELL") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': sell_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def fixture(self, fixture_data, **overwrites): data = fixture_data.copy() for key, value in overwrites.items(): if key not in data: raise Exception(f"{key} not found in fixture_data") data[key] = value return data def order_response(self, fixture_data, nonce, side, trading_pair): self._t_nonce_mock.return_value = nonce order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}" order_resp = fixture_data.copy() order_resp["clientOrderId"] = order_id return order_resp def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, fixture_resp, fixture_ws_1 = None, fixture_ws_2 = None): order_id = None if API_MOCK_ENABLED: resp = self.order_response(fixture_resp, nonce, 'buy' if is_buy else 'sell', trading_pair) self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED and fixture_ws_1 is not None and fixture_ws_2 is not None: exchange_order_id = str(resp['orderId']) data = self.fixture(fixture_ws_1, c=order_id, i=exchange_order_id) MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.1) data = self.fixture(fixture_ws_2, c=order_id, i=exchange_order_id) MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.11) return order_id def test_cancel_all(self): trading_pair = "LINK-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, ask_price * Decimal("1.5")) buy_id = self.place_order(True, "LINK-ETH", quantized_amount, OrderType.LIMIT, quantize_bid_price, self.get_current_nonce(), FixtureBinance.OPEN_BUY_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) sell_id = self.place_order(False, "LINK-ETH", quantized_amount, OrderType.LIMIT, quantize_ask_price, self.get_current_nonce(), FixtureBinance.OPEN_SELL_ORDER, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=buy_id, side="BUY") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': buy_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=sell_id, side="SELL") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': sell_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_order_price_precision(self): # As of the day this test was written, the min order size (base) is 1 LINK, the min order size (quote) is # 0.01 ETH, and order step size is 1 LINK. trading_pair = "LINK-ETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) mid_price: Decimal = (bid_price + ask_price) / 2 amount: Decimal = Decimal("1.23123216") binance_client = self.market.binance_client # Make sure there's enough balance to make the limit orders. self.assertGreater(self.market.get_balance("ETH"), Decimal("0.05")) self.assertGreater(self.market.get_balance("LINK"), amount * 2) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price: Decimal = mid_price * Decimal("0.9333192292111341") ask_price: Decimal = mid_price * Decimal("1.0492431474884933") # This is needed to get around the min quote amount limit. bid_amount: Decimal = Decimal("1.23123216") if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_BUY_PRECISION, self.get_current_nonce(), "buy", "LINK-ETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) # Test bid order bid_order_id: str = self.market.buy( trading_pair, Decimal(bid_amount), OrderType.LIMIT, Decimal(bid_price) ) if API_MOCK_ENABLED: resp = FixtureBinance.ORDER_BUY_PRECISION_GET resp["clientOrderId"] = bid_order_id self.web_app.update_response("get", self.base_api_url, "/api/v3/order", resp) # Wait for the order created event and examine the order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent, timeout_seconds=10) ) order_data: Dict[str, any] = binance_client.get_order( symbol=trading_pair, origClientOrderId=bid_order_id ) quantized_bid_price: Decimal = self.market.quantize_order_price(trading_pair, Decimal(bid_price)) bid_size_quantum: Decimal = self.market.get_order_size_quantum(trading_pair, Decimal(bid_amount)) self.assertEqual(quantized_bid_price, Decimal(order_data["price"])) self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0) # Test ask order if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_SELL_PRECISION, self.get_current_nonce(), "sell", "LINK-ETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) ask_order_id: str = self.market.sell( trading_pair, Decimal(amount), OrderType.LIMIT, Decimal(ask_price) ) if API_MOCK_ENABLED: resp = FixtureBinance.ORDER_SELL_PRECISION_GET resp["clientOrderId"] = ask_order_id self.web_app.update_response("get", self.base_api_url, "/api/v3/order", resp) # Wait for the order created event and examine and order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent, timeout_seconds=10) ) order_data = binance_client.get_order( symbol=trading_pair, origClientOrderId=ask_order_id ) quantized_ask_price: Decimal = self.market.quantize_order_price(trading_pair, Decimal(ask_price)) quantized_ask_size: Decimal = self.market.quantize_order_amount(trading_pair, Decimal(amount)) self.assertEqual(quantized_ask_price, Decimal(order_data["price"])) self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"])) # Cancel all the orders if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=bid_order_id, side="BUY") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': bid_order_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=ask_order_id, side="SELL") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': ask_order_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_server_time_offset(self): time_obj: BinanceTime = binance_client_module.time old_check_interval: float = time_obj._server_time_offset_check_interval time_obj._server_time_offset_check_interval = 1.0 time_obj.stop() time_obj.start() try: local_time_offset = (time.time() - time.perf_counter()) * 1e3 with patch("hummingbot.connector.exchange.binance.binance_time.time") as market_time: def delayed_time(): return time.perf_counter() - 30.0 market_time.perf_counter = delayed_time self.run_parallel(asyncio.sleep(3.0)) raw_time_offset = BinanceTime.get_instance().time_offset_ms time_offset_diff = raw_time_offset - local_time_offset # check if it is less than 5% off self.assertTrue(time_offset_diff > 10000) self.assertTrue(abs(time_offset_diff - 30.0 * 1e3) < 1.5 * 1e3) finally: time_obj._server_time_offset_check_interval = old_check_interval time_obj.stop() time_obj.start() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.market.get_price("LINK-ETH", True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price("LINK-ETH", bid_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount("LINK-ETH", amount) if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.OPEN_BUY_ORDER, self.get_current_nonce(), "buy", "LINK-ETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) order_id = self.market.buy("LINK-ETH", quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.__class__.market: BinanceExchange = BinanceExchange(API_KEY, API_SECRET, ["LINK-ETH", "ZRX-ETH"], True) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.ev_loop.run_until_complete(self.wait_til_ready()) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=order_id, side="BUY") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': order_id}) self.market.cancel("LINK-ETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel("LINK-ETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) buy_id: Optional[str] = None sell_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 1 LINK from the exchange, and watch for completion event. bid_price: Decimal = self.market.get_price("LINK-ETH", True) amount: Decimal = 1 buy_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, self.get_current_nonce(), FixtureBinance.BUY_LIMIT_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of LINK to the exchange, and watch for completion event. ask_price: Decimal = self.market.get_price("LINK-ETH", False) amount = buy_order_completed_event.base_asset_amount sell_id = self.place_order(False, "LINK-ETH", amount, OrderType.LIMIT, ask_price, self.get_current_nonce(), FixtureBinance.SELL_LIMIT_ORDER, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) buy_id = sell_id = None finally: if buy_id is not None: self.market.cancel("LINK-ETH", buy_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) if sell_id is not None: self.market.cancel("LINK-ETH", sell_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_prevent_duplicated_orders(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) buy_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Perform the same order twice which should produce the same exchange_order_id # Try to buy 1 LINK from the exchange, and watch for completion event. bid_price: Decimal = self.market.get_price("LINK-ETH", True) amount: Decimal = 1 buy_id = self.place_order(True, "LINK-ETH", amount, OrderType.LIMIT, bid_price, self.get_current_nonce(), FixtureBinance.BUY_LIMIT_ORDER, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) self.market_logger.clear() # Simulate that order is still in in_flight_orders order_json = {"client_order_id": buy_id, "exchange_order_id": str(FixtureBinance.WS_AFTER_BUY_2['t']), "trading_pair": "LINK-ETH", "order_type": "MARKET", "trade_type": "BUY", "price": bid_price, "amount": amount, "last_state": "NEW", "executed_amount_base": "0", "executed_amount_quote": "0", "fee_asset": "LINK", "fee_paid": "0.0"} self.market.restore_tracking_states({buy_id: order_json}) self.market.in_flight_orders.get(buy_id).trade_id_set.add(str(FixtureBinance.WS_AFTER_BUY_2['t'])) # Simulate incoming responses as if buy_id is executed again data = self.fixture(FixtureBinance.WS_AFTER_BUY_2, c=buy_id) MockWebSocketServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.11) # Will wait, but no order filled event should be triggered because order is ignored self.run_parallel(asyncio.sleep(1)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] exchange_trade_id = FixtureBinance.WS_AFTER_BUY_2['t'] self.assertEqual(len([bf for bf in buy_fills if int(bf.exchange_trade_id) == exchange_trade_id]), 1) buy_id = None finally: if buy_id is not None: self.market.cancel("LINK-ETH", buy_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_history_reconciliation(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: bid_price: Decimal = self.market.get_price("LINK-ETH", True) # Will temporarily change binance history request to return trades buy_id = "1580204166011219" order_id = "123456" self._t_nonce_mock.return_value = 1234567890123456 binance_trades = [{ 'symbol': "LINKETH", 'id': buy_id, 'orderId': order_id, 'orderListId': -1, 'price': float(bid_price), 'qty': 1, 'quoteQty': float(bid_price), 'commission': 0, 'commissionAsset': "ETH", 'time': 1580093596074, 'isBuyer': True, 'isMaker': True, 'isBestMatch': True, }] self.market.add_exchange_order_ids_from_market_recorder({order_id: "buy-LINKETH-1580093594011279"}) self.web_app.update_response("get", self.base_api_url, "/api/v3/myTrades", binance_trades, params={'symbol': 'LINKETH'}) [market_order_completed] = self.run_parallel(self.market_logger.wait_for(OrderFilledEvent)) trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] self.assertEqual(len([bf for bf in buy_fills if bf.exchange_trade_id == buy_id]), 1) buy_id = None finally: if buy_id is not None: self.market.cancel("LINK-ETH", buy_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) # Undo change to binance history request self.web_app.update_response("get", self.base_api_url, "/api/v3/myTrades", {}, params={'symbol': 'LINKETH'}) recorder.stop() os.unlink(self.db_path) def test_pair_conversion(self): if API_MOCK_ENABLED: return for pair in self.market.trading_rules: exchange_pair = convert_to_exchange_trading_pair(pair) self.assertTrue(exchange_pair in self.market.order_books)
class BinanceMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: BinanceMarket market_logger: EventLogger stack: contextlib.ExitStack base_api_url = "api.binance.com" @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.ev_loop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = HummingWebApp.get_instance() cls.web_app.add_host_to_mock( cls.base_api_url, ["/api/v1/ping", "/api/v1/exchangeInfo", "/api/v1/time"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls._req_patcher = unittest.mock.patch.object(requests.Session, "request", autospec=True) cls._req_url_mock = cls._req_patcher.start() cls._req_url_mock.side_effect = HummingWebApp.reroute_request cls.web_app.update_response("get", cls.base_api_url, "/api/v3/account", FixtureBinance.GET_ACCOUNT) cls.web_app.update_response("get", cls.base_api_url, "/wapi/v3/tradeFee.html", FixtureBinance.GET_TRADE_FEES) cls.web_app.update_response("post", cls.base_api_url, "/api/v1/userDataStream", FixtureBinance.GET_LISTEN_KEY) cls.web_app.update_response("put", cls.base_api_url, "/api/v1/userDataStream", FixtureBinance.GET_LISTEN_KEY) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/depth", FixtureBinance.LINKETH_SNAP, params={'symbol': 'LINKETH'}) cls.web_app.update_response("get", cls.base_api_url, "/api/v1/depth", FixtureBinance.ZRXETH_SNAP, params={'symbol': 'ZRXETH'}) ws_base_url = "wss://stream.binance.com:9443/ws" cls._ws_user_url = f"{ws_base_url}/{FixtureBinance.GET_LISTEN_KEY['listenKey']}" HummingWsServerFactory.start_new_server(cls._ws_user_url) HummingWsServerFactory.start_new_server( f"{ws_base_url}/linketh@depth/zrxeth@depth") cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.market.binance.binance_market.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BinanceMarket = BinanceMarket( API_KEY, API_SECRET, order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType .EXCHANGE_API, trading_pairs=["LINKETH", "ZRXETH"]) print("Initializing Binance market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._req_patcher.stop() cls._ws_patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../binance_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal(4000)) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.MARKET, TradeType.BUY, Decimal(1)) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["binance_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), taker_fee.percent) fee_overrides_config_map["binance_taker_fee"].value = Decimal('0.002') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["binance_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.001"), maker_fee.percent) fee_overrides_config_map["binance_maker_fee"].value = Decimal('0.005') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def test_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "LINKETH", amount) order_id = self.place_order(True, "LINKETH", amount, OrderType.MARKET, 0, 10001, FixtureBinance.ORDER_BUY, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id = self.place_order(False, "LINKETH", amount, OrderType.MARKET, 0, 10002, FixtureBinance.ORDER_SELL, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) # Try to put limit buy order for 1 LINK, and watch for completion event. ask_price: Decimal = self.market.get_price("LINKETH", False) * Decimal("1.01") quantize_bid_price: Decimal = self.market.quantize_order_price( "LINKETH", ask_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "LINKETH", amount) order_id = self.place_order(True, "LINKETH", quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBinance.ORDER_BUY_LIMIT, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to put limit sell order for 0.02 ETH worth of ZRX, and watch for completion event. bid_price: Decimal = self.market.get_price("LINKETH", True) * Decimal('0.99') quantize_ask_price: Decimal = self.market.quantize_order_price( "LINKETH", bid_price) quantized_amount = order_completed_event.base_asset_amount order_id = self.place_order(False, "LINKETH", quantized_amount, OrderType.LIMIT, quantize_ask_price, 10002, FixtureBinance.ORDER_SELL_LIMIT, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("LINK", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_deposit_info(self): if API_MOCK_ENABLED: self.web_app.update_response("get", self.base_api_url, "/wapi/v3/depositAddress.html", FixtureBinance.GET_DEPOSIT_INFO) [deposit_info] = self.run_parallel(self.market.get_deposit_info("BNB")) deposit_info: DepositInfo = deposit_info self.assertIsInstance(deposit_info, DepositInfo) self.assertGreater(len(deposit_info.address), 0) self.assertGreater(len(deposit_info.extras), 0) self.assertTrue("addressTag" in deposit_info.extras) self.assertEqual("BNB", deposit_info.extras["asset"]) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # ZRX_ABI contract file can be found in # https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd: zrx_abi: str = fd.read() local_wallet: MockWallet = MockWallet( conf.web3_test_private_key_a, conf.test_web3_provider_list[0], {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi}, chain_id=1) # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), Decimal('10')) # Withdraw ZRX from Binance to test wallet. self.market.withdraw(local_wallet.address, "ZRX", Decimal('10')) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(local_wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(Decimal('10'), withdraw_asset_event.amount) self.assertGreater(withdraw_asset_event.fee_amount, Decimal(0)) def fixture(self, fixture_data, **overwrites): data = fixture_data.copy() for key, value in overwrites.items(): if key not in data: raise Exception(f"{key} not found in fixture_data") data[key] = value return data def order_response(self, fixture_data, nonce, side, trading_pair): self._t_nonce_mock.return_value = nonce order_id = f"{side.lower()}-{trading_pair}-{str(nonce)}" order_resp = fixture_data.copy() order_resp["clientOrderId"] = order_id return order_resp def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, fixture_resp, fixture_ws_1, fixture_ws_2): order_id = None if API_MOCK_ENABLED: resp = self.order_response(fixture_resp, nonce, 'buy' if is_buy else 'sell', trading_pair) self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: data = self.fixture(fixture_ws_1, c=order_id) HummingWsServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.1) data = self.fixture(fixture_ws_2, c=order_id) HummingWsServerFactory.send_json_threadsafe(self._ws_user_url, data, delay=0.11) return order_id def test_cancel_all(self): trading_pair = "LINKETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) buy_id = self.place_order(True, "LINKETH", quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBinance.ORDER_BUY_NOT_FILLED, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) sell_id = self.place_order(False, "LINKETH", quantized_amount, OrderType.LIMIT, quantize_ask_price, 10002, FixtureBinance.ORDER_SELL_NOT_FILLED, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=buy_id, side="BUY") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': buy_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=sell_id, side="SELL") self.web_app.update_response("delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': sell_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_order_price_precision(self): # As of the day this test was written, the min order size (base) is 1 LINK, the min order size (quote) is # 0.01 ETH, and order step size is 1 LINK. trading_pair = "LINKETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) print(f"bid_price: {bid_price} ask_price: {ask_price}") for ent in self.market.order_book_bid_entries("LINKETH"): print(f"bid: {ent.price} volume: {ent.amount}") for ent in self.market.order_book_ask_entries("LINKETH"): print(f"ask: {ent.price} volume: {ent.amount}") mid_price: Decimal = (bid_price + ask_price) / 2 amount: Decimal = Decimal("0.02") / mid_price binance_client = self.market.binance_client # Make sure there's enough balance to make the limit orders. self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) self.assertGreater(self.market.get_balance("LINK"), amount * 2) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price: Decimal = mid_price * Decimal("0.3333192292111341") ask_price: Decimal = mid_price * Decimal("3.4392431474884933") # This is needed to get around the min quote amount limit. bid_amount: Decimal = Decimal("0.02") / bid_price if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_BUY_PRECISION, 1000001, "buy", "LINKETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) # Test bid order bid_order_id: str = self.market.buy(trading_pair, Decimal(bid_amount), OrderType.LIMIT, Decimal(bid_price)) if API_MOCK_ENABLED: resp = FixtureBinance.ORDER_BUY_PRECISION_GET resp["clientOrderId"] = bid_order_id self.web_app.update_response("get", self.base_api_url, "/api/v3/order", resp) # Wait for the order created event and examine the order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent, timeout_seconds=10)) order_data: Dict[str, any] = binance_client.get_order( symbol=trading_pair, origClientOrderId=bid_order_id) quantized_bid_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(bid_price)) bid_size_quantum: Decimal = self.market.get_order_size_quantum( trading_pair, Decimal(bid_amount)) self.assertEqual(quantized_bid_price, Decimal(order_data["price"])) self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0) # Test ask order if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_SELL_PRECISION, 1000002, "sell", "LINKETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) ask_order_id: str = self.market.sell(trading_pair, Decimal(amount), OrderType.LIMIT, Decimal(ask_price)) if API_MOCK_ENABLED: resp = FixtureBinance.ORDER_SELL_PRECISION_GET resp["clientOrderId"] = ask_order_id self.web_app.update_response("get", self.base_api_url, "/api/v3/order", resp) # Wait for the order created event and examine and order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent, timeout_seconds=10)) order_data = binance_client.get_order(symbol=trading_pair, origClientOrderId=ask_order_id) quantized_ask_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(ask_price)) quantized_ask_size: Decimal = self.market.quantize_order_amount( trading_pair, Decimal(amount)) self.assertEqual(quantized_ask_price, Decimal(order_data["price"])) self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"])) # Cancel all the orders if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=bid_order_id, side="BUY") self.web_app.update_response( "delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': bid_order_id}) resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=ask_order_id, side="SELL") self.web_app.update_response( "delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': ask_order_id}) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_server_time_offset(self): time_obj: BinanceTime = binance_client_module.time old_check_interval: float = time_obj._server_time_offset_check_interval time_obj._server_time_offset_check_interval = 1.0 time_obj.stop() time_obj.start() try: local_time_offset = (time.time() - time.perf_counter()) * 1e3 with patch("hummingbot.market.binance.binance_time.time" ) as market_time: def delayed_time(): return time.perf_counter() - 30.0 market_time.perf_counter = delayed_time self.run_parallel(asyncio.sleep(3.0)) raw_time_offset = BinanceTime.get_instance().time_offset_ms time_offset_diff = raw_time_offset - local_time_offset # check if it is less than 5% off self.assertTrue(time_offset_diff > 10000) self.assertTrue(abs(time_offset_diff - 30.0 * 1e3) < 1.5 * 1e3) finally: time_obj._server_time_offset_check_interval = old_check_interval time_obj.stop() time_obj.start() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.market.get_price("LINKETH", True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( "LINKETH", bid_price) amount: Decimal = 1 quantized_amount: Decimal = self.market.quantize_order_amount( "LINKETH", amount) if API_MOCK_ENABLED: resp = self.order_response(FixtureBinance.ORDER_BUY_NOT_FILLED, 1000001, "buy", "LINKETH") self.web_app.update_response("post", self.base_api_url, "/api/v3/order", resp) order_id = self.market.buy("LINKETH", quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BinanceMarket = BinanceMarket( binance_api_key=API_KEY, binance_api_secret=API_SECRET, order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, user_stream_tracker_data_source_type= UserStreamTrackerDataSourceType.EXCHANGE_API, trading_pairs=["LINKETH", "ZRXETH"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. if API_MOCK_ENABLED: resp = self.fixture(FixtureBinance.CANCEL_ORDER, origClientOrderId=order_id, side="BUY") self.web_app.update_response( "delete", self.base_api_url, "/api/v3/order", resp, params={'origClientOrderId': order_id}) self.market.cancel("LINKETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel("LINKETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 1 LINK from the exchange, and watch for completion event. amount: Decimal = 1 order_id = self.place_order(True, "LINKETH", amount, OrderType.MARKET, 0, 10001, FixtureBinance.ORDER_BUY_LIMIT, FixtureBinance.WS_AFTER_BUY_1, FixtureBinance.WS_AFTER_BUY_2) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of LINK to the exchange, and watch for completion event. amount = buy_order_completed_event.base_asset_amount order_id = self.place_order(False, "LINKETH", amount, OrderType.MARKET, 0, 10002, FixtureBinance.ORDER_SELL_LIMIT, FixtureBinance.WS_AFTER_SELL_1, FixtureBinance.WS_AFTER_SELL_2) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel("LINKETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BinanceMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: BinanceMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BinanceMarket = BinanceMarket( conf.binance_api_key, conf.binance_api_secret, order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, user_stream_tracker_data_source_type=UserStreamTrackerDataSourceType .EXCHANGE_API, trading_pairs=["ZRXETH", "IOSTETH"]) print("Initializing Binance market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../binance_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal(4000)) self.assertGreater(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "BTC", "USDT", OrderType.MARKET, TradeType.BUY, Decimal(1)) self.assertGreater(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("BTC", "USDT", OrderType.LIMIT, TradeType.SELL, Decimal(1), Decimal(4000)) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) # Try to buy 0.02 ETH worth of ZRX from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price("ZRXETH", True) amount: Decimal = Decimal("0.02") / current_price quantized_amount: Decimal = self.market.quantize_order_amount( "ZRXETH", amount) order_id = self.market.buy("ZRXETH", amount) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("ZRXETH", amount) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for completion event. current_bid_price: Decimal = self.market.get_price("ZRXETH", True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( "ZRXETH", bid_price) amount: Decimal = Decimal("0.02") / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( "ZRXETH", amount) order_id = self.market.buy("ZRXETH", quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to put limit sell order for 0.02 ETH worth of ZRX, and watch for completion event. current_ask_price: Decimal = self.market.get_price("ZRXETH", False) ask_price: Decimal = current_ask_price - Decimal( "0.05") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( "ZRXETH", ask_price) amount = order_completed_event.base_asset_amount quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("ZRXETH", quantized_amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("ETH", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_deposit_info(self): [deposit_info] = self.run_parallel(self.market.get_deposit_info("BNB")) deposit_info: DepositInfo = deposit_info self.assertIsInstance(deposit_info, DepositInfo) self.assertGreater(len(deposit_info.address), 0) self.assertGreater(len(deposit_info.extras), 0) self.assertTrue("addressTag" in deposit_info.extras) self.assertEqual("BNB", deposit_info.extras["asset"]) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # ZRX_ABI contract file can be found in # https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498#code with open(realpath(join(__file__, "../../../data/ZRXABI.json"))) as fd: zrx_abi: str = fd.read() local_wallet: MockWallet = MockWallet( conf.web3_test_private_key_a, conf.test_web3_provider_list[0], {"0xE41d2489571d322189246DaFA5ebDe1F4699F498": zrx_abi}, chain_id=1) # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), Decimal('10')) # Withdraw ZRX from Binance to test wallet. self.market.withdraw(local_wallet.address, "ZRX", Decimal('10')) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(local_wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(Decimal('10'), withdraw_asset_event.amount) self.assertGreater(withdraw_asset_event.fee_amount, Decimal(0)) def test_cancel_all(self): trading_pair = "ZRXETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.02") / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_order_price_precision(self): # As of the day this test was written, the min order size (base) is 1 IOST, the min order size (quote) is # 0.01 ETH, and order step size is 1 IOST. trading_pair = "IOSTETH" bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) mid_price: Decimal = (bid_price + ask_price) / 2 amount: Decimal = Decimal("0.02") / mid_price binance_client = self.market.binance_client # Make sure there's enough balance to make the limit orders. self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) self.assertGreater(self.market.get_balance("IOST"), amount * 2) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price: Decimal = mid_price * Decimal("0.3333192292111341") ask_price: Decimal = mid_price * Decimal("3.4392431474884933") # This is needed to get around the min quote amount limit. bid_amount: Decimal = Decimal("0.02") / bid_price # Test bid order bid_order_id: str = self.market.buy(trading_pair, Decimal(bid_amount), OrderType.LIMIT, Decimal(bid_price)) # Wait for the order created event and examine the order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent, timeout_seconds=10)) order_data: Dict[str, any] = binance_client.get_order( symbol=trading_pair, origClientOrderId=bid_order_id) quantized_bid_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(bid_price)) bid_size_quantum: Decimal = self.market.get_order_size_quantum( trading_pair, Decimal(bid_amount)) self.assertEqual(quantized_bid_price, Decimal(order_data["price"])) self.assertTrue(Decimal(order_data["origQty"]) % bid_size_quantum == 0) # Test ask order ask_order_id: str = self.market.sell(trading_pair, Decimal(amount), OrderType.LIMIT, Decimal(ask_price)) # Wait for the order created event and examine and order made [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent, timeout_seconds=10)) order_data = binance_client.get_order(symbol=trading_pair, origClientOrderId=ask_order_id) quantized_ask_price: Decimal = self.market.quantize_order_price( trading_pair, Decimal(ask_price)) quantized_ask_size: Decimal = self.market.quantize_order_amount( trading_pair, Decimal(amount)) self.assertEqual(quantized_ask_price, Decimal(order_data["price"])) self.assertEqual(quantized_ask_size, Decimal(order_data["origQty"])) # Cancel all the orders [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_server_time_offset(self): time_obj: BinanceTime = binance_client_module.time old_check_interval: float = time_obj.SERVER_TIME_OFFSET_CHECK_INTERVAL time_obj.SERVER_TIME_OFFSET_CHECK_INTERVAL = 1.0 time_obj.stop() time_obj.start() try: with patch("hummingbot.market.binance.binance_time.time" ) as market_time: def delayed_time(): return time.time() - 30.0 market_time.time = delayed_time self.run_parallel(asyncio.sleep(3.0)) time_offset = BinanceTime.get_instance().time_offset_ms # check if it is less than 5% off self.assertTrue(time_offset > 10000) self.assertTrue(abs(time_offset - 30.0 * 1e3) < 1.5 * 1e3) finally: time_obj.SERVER_TIME_OFFSET_CHECK_INTERVAL = old_check_interval time_obj.stop() time_obj.start() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.market.get_price("ZRXETH", True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( "ZRXETH", bid_price) amount: Decimal = Decimal("0.02") / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( "ZRXETH", amount) order_id = self.market.buy("ZRXETH", quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BinanceMarket = BinanceMarket( binance_api_key=conf.binance_api_key, binance_api_secret=conf.binance_api_secret, order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, user_stream_tracker_data_source_type= UserStreamTrackerDataSourceType.EXCHANGE_API, trading_pairs=["ZRXETH", "IOSTETH"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel("ZRXETH", order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel("ZRXETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.02 ETH worth of ZRX from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price("ZRXETH", True) amount: Decimal = Decimal("0.02") / current_price order_id = self.market.buy("ZRXETH", amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = buy_order_completed_event.base_asset_amount order_id = self.market.sell("ZRXETH", amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.market.cancel("ZRXETH", order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class CoinbaseProMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated ] market: CoinbaseProMarket market_logger: EventLogger @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: CoinbaseProMarket = CoinbaseProMarket( ethereum_rpc_url=conf.test_web3_provider_list[0], coinbase_pro_api_key=conf.coinbase_pro_api_key, coinbase_pro_secret_key=conf.coinbase_pro_secret_key, coinbase_pro_passphrase=conf.coinbase_pro_passphrase, symbols=["ETH-USDC", "ETH-USD"]) cls.wallet: Web3Wallet = Web3Wallet( private_key=conf.web3_private_key_coinbase_pro, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[ conf.mn_weth_token_address, conf.mn_zerox_token_address ], chain=EthereumChain.MAIN_NET) print( "Initializing Coinbase Pro market... this will take about a minute." ) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.wallet) stack = contextlib.ExitStack() cls._clock = stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_ask_price: float = self.market.get_price(symbol, False) ask_price: float = current_ask_price - 0.05 * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price) order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_sell(self): symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): self.assertGreater(self.market.get_balance("ETH"), 10) symbol = "ETH-USDC" current_bid_price: float = self.market.get_price(symbol, True) amount: float = 10 / current_bid_price bid_price: float = current_bid_price - 0.1 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): symbol = "ETH-USDC" bid_price: float = self.market.get_price(symbol, True) * 0.5 ask_price: float = self.market.get_price(symbol, False) * 2 amount: float = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price * 0.7) quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price * 1.5) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() @unittest.skipUnless(any("test_deposit_eth" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_eth(self): # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(self.wallet.get_balance("ETH"), 0.02) # Deposit ETH to Binance, and wait. tracking_id: str = self.market.deposit(self.wallet, "ETH", 0.01) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800)) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ETH", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(self.wallet.address, received_asset_event.from_address) self.assertAlmostEqual(0.01, received_asset_event.amount_received) @unittest.skipUnless(any("test_deposit_zrx" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_zrx(self): # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(self.wallet.get_balance("ZRX"), 1) # Deposit ZRX to Coinbase Pro, and wait. tracking_id: str = self.market.deposit(self.wallet, "ZRX", 1) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800)) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ZRX", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(self.wallet.address, received_asset_event.from_address) self.assertEqual(1, received_asset_event.amount_received) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), 1) # Withdraw ZRX from Coinbase Pro to test wallet. self.market.withdraw(self.wallet.address, "ZRX", 1) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent)) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(self.wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(1, withdraw_asset_event.amount) self.assertEqual(withdraw_asset_event.fee_amount, 0)
class AscendExExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] connector: AscendExExchange event_logger: EventLogger trading_pair = "BTC-USDT" base_token, quote_token = trading_pair.split("-") stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.connector: AscendExExchange = AscendExExchange( ascend_ex_api_key=API_KEY, ascend_ex_secret_key=API_SECRET, trading_pairs=[cls.trading_pair], trading_required=True) print( "Initializing AscendEx exchange... this will take about a minute.") cls.clock.add_iterator(cls.connector) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls, connector=None): if connector is None: connector = cls.connector while True: now = time.time() next_iteration = now // 1.0 + 1 if connector.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../connector_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.event_logger = EventLogger() for event_tag in self.events: self.connector.add_listener(event_tag, self.event_logger) def tearDown(self): for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) self.event_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def _place_order(self, is_buy, amount, order_type, price, ex_order_id) -> str: if is_buy: cl_order_id = self.connector.buy(self.trading_pair, amount, order_type, price) else: cl_order_id = self.connector.sell(self.trading_pair, amount, order_type, price) return cl_order_id def _cancel_order(self, cl_order_id): self.connector.cancel(self.trading_pair, cl_order_id) def test_estimate_fee(self): maker_fee = self.connector.estimate_fee_pct(True) self.assertAlmostEqual(maker_fee, Decimal("0.001")) taker_fee = self.connector.estimate_fee_pct(False) self.assertAlmostEqual(taker_fee, Decimal("0.001")) def test_buy_and_sell(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("1.05") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0002")) quote_bal = self.connector.get_available_balance(self.quote_token) base_bal = self.connector.get_available_balance(self.base_token) order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1) order_completed_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(2)) trade_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(amount, order_completed_event.base_asset_amount) self.assertEqual("BTC", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.event_logger.event_log ])) # check available quote balance gets updated, we need to wait a bit for the balance message to arrive expected_quote_bal = quote_bal - quote_amount_traded # self.ev_loop.run_until_complete(asyncio.sleep(1)) self.ev_loop.run_until_complete(self.connector._update_balances()) self.assertAlmostEqual( expected_quote_bal, self.connector.get_available_balance(self.quote_token), 1) # Reset the logs self.event_logger.clear() # Try to sell back the same amount to the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("0.95") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0002")) order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2) order_completed_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) trade_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(amount, order_completed_event.base_asset_amount) self.assertEqual("BTC", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.event_logger.event_log ])) # check available base balance gets updated, we need to wait a bit for the balance message to arrive expected_base_bal = base_bal # self.ev_loop.run_until_complete(asyncio.sleep(1)) self.ev_loop.run_until_complete(self.connector._update_balances()) self.assertAlmostEqual( expected_base_bal, self.connector.get_available_balance(self.base_token), 5) def test_limit_makers_unfilled(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("0.8") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0002")) quote_bal = self.connector.get_available_balance(self.quote_token) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) # check available quote balance gets updated, we need to wait a bit for the balance message to arrive expected_quote_bal = quote_bal - (price * amount) self.ev_loop.run_until_complete(self.connector._update_balances()) # self.ev_loop.run_until_complete(asyncio.sleep(2)) self.assertAlmostEqual( expected_quote_bal, self.connector.get_available_balance(self.quote_token), 1) self._cancel_order(cl_order_id) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) price = self.connector.get_price(self.trading_pair, True) * Decimal("1.2") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0002")) cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) self._cancel_order(cl_order_id) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) # # @TODO: find a way to create "rejected" # def test_limit_maker_rejections(self): # price = self.connector.get_price(self.trading_pair, True) * Decimal("1.2") # price = self.connector.quantize_order_price(self.trading_pair, price) # amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.000001")) # cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1) # event = self.ev_loop.run_until_complete(self.event_logger.wait_for(OrderCancelledEvent)) # self.assertEqual(cl_order_id, event.order_id) # price = self.connector.get_price(self.trading_pair, False) * Decimal("0.8") # price = self.connector.quantize_order_price(self.trading_pair, price) # amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.000001")) # cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2) # event = self.ev_loop.run_until_complete(self.event_logger.wait_for(OrderCancelledEvent)) # self.assertEqual(cl_order_id, event.order_id) def test_cancel_all(self): bid_price = self.connector.get_price(self.trading_pair, True) ask_price = self.connector.get_price(self.trading_pair, False) bid_price = self.connector.quantize_order_price( self.trading_pair, bid_price * Decimal("0.9")) ask_price = self.connector.quantize_order_price( self.trading_pair, ask_price * Decimal("1.1")) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0002")) buy_id = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1) sell_id = self._place_order(False, amount, OrderType.LIMIT, ask_price, 2) self.ev_loop.run_until_complete(asyncio.sleep(1)) asyncio.ensure_future(self.connector.cancel_all(3)) self.ev_loop.run_until_complete(asyncio.sleep(3)) cancel_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderCancelledEvent) ] self.assertEqual({buy_id, sell_id}, {o.order_id for o in cancel_events}) def test_order_quantized_values(self): bid_price: Decimal = self.connector.get_price(self.trading_pair, True) ask_price: Decimal = self.connector.get_price(self.trading_pair, False) mid_price: Decimal = (bid_price + ask_price) / 2 # Make sure there's enough balance to make the limit orders. self.assertGreater(self.connector.get_balance("BTC"), Decimal("0.001")) self.assertGreater(self.connector.get_balance("USDT"), Decimal("10")) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price = self.connector.quantize_order_price( self.trading_pair, mid_price * Decimal("0.9333192292111341")) ask_price = self.connector.quantize_order_price( self.trading_pair, mid_price * Decimal("1.1492431474884933")) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.000223456")) # Test bid order cl_order_id_1 = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1) # Wait for the order created event and examine the order made self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) # Test ask order cl_order_id_2 = self._place_order(False, amount, OrderType.LIMIT, ask_price, 1) # Wait for the order created event and examine and order made self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCreatedEvent)) self._cancel_order(cl_order_id_1) self._cancel_order(cl_order_id_2) def test_orders_saving_and_restoration(self): config_path = "test_config" strategy_name = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id = None recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: self.connector._in_flight_orders.clear() self.assertEqual(0, len(self.connector.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.connector.get_price( self.trading_pair, True) price: Decimal = current_bid_price * Decimal("0.8") price = self.connector.quantize_order_price( self.trading_pair, price) amount: Decimal = Decimal("0.0002") amount = self.connector.quantize_order_amount( self.trading_pair, amount) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.connector.tracking_states)) self.assertEqual(cl_order_id, list(self.connector.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.connector) self.assertEqual(1, len(recorded_orders)) self.assertEqual(cl_order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.connector) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.connector.stop(self._clock) self.ev_loop.run_until_complete(asyncio.sleep(5)) self.clock.remove_iterator(self.connector) for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) new_connector = AscendExExchange(API_KEY, API_SECRET, [self.trading_pair], True) for event_tag in self.events: new_connector.add_listener(event_tag, self.event_logger) recorder.stop() recorder = MarketsRecorder(sql, [new_connector], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, new_connector) self.clock.add_iterator(new_connector) self.ev_loop.run_until_complete(self.wait_til_ready(new_connector)) self.assertEqual(0, len(new_connector.limit_orders)) self.assertEqual(0, len(new_connector.tracking_states)) new_connector.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(new_connector.limit_orders)) self.assertEqual(1, len(new_connector.tracking_states)) # Cancel the order and verify that the change is saved. self._cancel_order(cl_order_id) self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(new_connector.limit_orders)) self.assertEqual(0, len(new_connector.tracking_states)) saved_market_states = recorder.get_market_states( config_path, new_connector) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.connector.cancel(self.trading_pair, cl_order_id) self.run_parallel( self.event_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.connector.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_filled_orders_recorded(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id = None recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: # Try to buy some token from the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("1.05") price = self.connector.quantize_order_price( self.trading_pair, price) amount = self.connector.quantize_order_amount( self.trading_pair, Decimal("0.0002")) order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1) self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(1)) # Reset the logs self.event_logger.clear() # Try to sell back the same amount to the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("0.95") price = self.connector.quantize_order_price( self.trading_pair, price) amount = self.connector.quantize_order_amount( self.trading_pair, Decimal("0.0002")) order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2) self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.connector.cancel(self.trading_pair, order_id) self.run_parallel( self.event_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class RadarRelayMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderExpired, MarketEvent.OrderFilled, MarketEvent.WithdrawAsset, ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: RadarRelayMarket market_logger: EventLogger wallet_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_private_key_radar, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[ conf.mn_zerox_token_address, conf.mn_weth_token_address ], chain=EthereumChain.MAIN_NET) cls.market: RadarRelayMarket = RadarRelayMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, trading_pairs=["ZRX-WETH"]) print("Initializing Radar Relay market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath( join(__file__, "../radar_relay_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): maker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, Decimal(20), Decimal(0.01)) self.assertEqual(maker_buy_trade_fee.percent, 0) self.assertEqual(len(maker_buy_trade_fee.flat_fees), 0) taker_buy_trade_fee: TradeFee = self.market.get_fee( "ZRX", "WETH", OrderType.MARKET, TradeType.BUY, Decimal(20)) self.assertEqual(taker_buy_trade_fee.percent, 0) self.assertEqual(len(taker_buy_trade_fee.flat_fees), 1) self.assertEqual(taker_buy_trade_fee.flat_fees[0][0], "ETH") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), s_decimal_0) self.assertGreaterEqual((balances["WETH"]), s_decimal_0) def test_single_limit_order_cancel(self): trading_pair: str = "ZRX-WETH" current_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(10) expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price * Decimal("0.8"), expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual("ZRX-WETH", buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.assertEqual(quantized_amount, Decimal(buy_order_opened_event.amount)) self.run_parallel(self.market.cancel_order(buy_order_id)) [buy_order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) # Reset the logs self.market_logger.clear() def test_limit_buy_and_sell_and_cancel_all(self): trading_pair: str = "ZRX-WETH" current_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(10) expires = int(time.time() + 60 * 5) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) buy_order_id = self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price * Decimal("0.8"), expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(quantized_amount, Decimal(buy_order_opened_event.amount)) self.assertEqual("ZRX-WETH", buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) # Reset the logs self.market_logger.clear() sell_order_id = self.market.sell(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price * Decimal("1.2"), expiration_ts=expires) [sell_order_opened_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(quantized_amount, Decimal(sell_order_opened_event.amount)) self.assertEqual("ZRX-WETH", sell_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) [cancellation_results ] = self.run_parallel(self.market.cancel_all(60 * 5)) self.assertEqual(cancellation_results[0], CancellationResult(buy_order_id, True)) self.assertEqual(cancellation_results[1], CancellationResult(sell_order_id, True)) # Reset the logs self.market_logger.clear() def test_order_expire(self): trading_pair: str = "ZRX-WETH" current_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal(10) expires = int(time.time() + 60 * 2) # expires in 2 min self.market.buy(trading_pair=trading_pair, amount=amount, order_type=OrderType.LIMIT, price=current_price * Decimal("0.8"), expiration_ts=expires) [buy_order_opened_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual("ZRX-WETH", buy_order_opened_event.trading_pair) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) [buy_order_expired_event] = self.run_parallel( self.market_logger.wait_for(OrderExpiredEvent, 60 * 3)) self.assertEqual(buy_order_opened_event.order_id, buy_order_expired_event.order_id) # Reset the logs self.market_logger.clear() def test_market_buy(self): amount: Decimal = Decimal(5) quantized_amount: Decimal = self.market.quantize_order_amount( "ZRX-WETH", amount) order_id = self.market.buy("ZRX-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.market_logger.clear() def test_market_sell(self): amount: Decimal = Decimal(5) quantized_amount: Decimal = self.market.quantize_order_amount( "ZRX-WETH", amount) order_id = self.market.sell("ZRX-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), float(order_completed_event.base_asset_amount)) self.assertEqual("ZRX", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.market_logger.clear() def test_wrap_eth(self): amount_to_wrap = Decimal("0.01") tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_wrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_unwrap_eth(self): amount_to_unwrap = Decimal("0.01") tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_unwrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ZRX-WETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) # Try to put limit buy order for 0.05 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal(0.8) quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: float = Decimal("0.05") / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, Decimal(amount)) expires = int(time.time() + 60 * 5) order_id = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, expiration_ts=expires) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) self.assertEqual( order_id, list(self.market.tracking_states["limit_orders"].keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertIsInstance( saved_market_states.saved_state["limit_orders"], dict) self.assertGreater( len(saved_market_states.saved_state["limit_orders"]), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: RadarRelayMarket = RadarRelayMarket( wallet=self.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, trading_pairs=["ZRX-WETH"]) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states["limit_orders"])) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) # Cancel the order and verify that the change is saved. self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states["limit_orders"])) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual( 1, len(saved_market_states.saved_state["limit_orders"])) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ZRX-WETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.05 ETH worth of ZRX from the exchange, and watch for completion event. current_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.05") / current_price order_id = self.market.buy(trading_pair, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ZRX to the exchange, and watch for completion event. amount = Decimal(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(trading_pair, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class LatokenExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, ] market: LatokenExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: LatokenExchange = LatokenExchange( API_KEY, API_SECRET, trading_pairs=[trading_pair], domain=domain) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../latoken_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFeeBase = self.market.get_fee(base_asset, quote_asset, OrderType.LIMIT, TradeType.BUY, Decimal("1"), Decimal("1")) self.assertGreater(limit_fee.percent, Decimal("0")) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFeeBase = self.market.get_fee(base_asset, quote_asset, OrderType.MARKET, TradeType.BUY, Decimal("1")) self.assertGreater(market_fee.percent, Decimal("0")) self.assertEqual(len(market_fee.flat_fees), 0) def test_minimum_order_size(self): amount = Decimal("0.000001") quantized_amount = self.market.quantize_order_amount( trading_pair, amount) self.assertEqual(quantized_amount, Decimal("0")) def test_get_balance(self): balance = self.market.get_balance(quote_asset) self.assertGreater(balance, 10) def test_limit_buy(self): amount: Decimal = Decimal("0.04") current_ask_price: Decimal = self.market.get_price(trading_pair, False) # no fill bid_price: Decimal = Decimal("0.9") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) # Wait for order creation event self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) # Cancel order. Automatically asserts that order is tracked self.market.cancel(trading_pair, order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(order_cancelled_event.order_id, order_id) # # Reset the logs self.market_logger.clear() def test_limit_sell(self): current_ask_price: Decimal = self.market.get_price(trading_pair, False) # for no fill ask_price: Decimal = Decimal("1.1") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) amount: Decimal = Decimal("0.02") order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) # Wait for order creation event self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) # Cancel order. Automatically asserts that order is tracked self.market.cancel(trading_pair, order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(order_cancelled_event.order_id, order_id) # Reset the logs self.market_logger.clear() # # # WARNING AUTOMATICALLY EXECUTES ORDER # def test_execute_limit_buy(self): # amount: Decimal = Decimal("0.04") # quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, # amount) # # # bid_entries = self.market.order_books[trading_pair].bid_entries() # ask_entries = self.market.order_books[trading_pair].ask_entries() # # most_top_bid = next(bid_entries) # most_top_ask = next(ask_entries) # # bid_price: Decimal = Decimal(most_top_bid.price) # # quantize_bid_price = self.market.quantize_order_price(trading_pair, bid_price) * Decimal("1.1") # # ask_price: Decimal = Decimal(most_top_ask.price) # min_price_increment = self.market.trading_rules[trading_pair].min_price_increment # ask_price_to_be_lifted = self.market.quantize_order_price(trading_pair, ask_price - min_price_increment) # # order_id_sell = self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, ask_price_to_be_lifted) # print(order_id_sell) # _ = self.run_parallel( # self.market_logger.wait_for(SellOrderCreatedEvent)) # # order_id_buy = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, ask_price_to_be_lifted) # # # [order_completed_event_sell] = self.run_parallel( # # self.market_logger.wait_for(SellOrderCompletedEvent)) # # order_completed_event_buy, order_completed_event_sell = self.run_parallel( # self.market_logger.wait_for(BuyOrderCompletedEvent), self.market_logger.wait_for(SellOrderCompletedEvent)) # # order_completed_event_buy: BuyOrderCompletedEvent = order_completed_event_buy # trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log # if isinstance(t, OrderFilledEvent)] # base_amount_traded: Decimal = sum(t.amount for t in trade_events) # quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) # # self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) # self.assertEqual(order_id_buy, order_completed_event_buy.order_id) # self.assertAlmostEqual(quantized_amount, # order_completed_event_buy.base_asset_amount) # self.assertEqual(base_asset, order_completed_event_buy.base_asset) # self.assertEqual(quote_asset, order_completed_event_buy.quote_asset) # self.assertAlmostEqual(base_amount_traded, # order_completed_event_buy.base_asset_amount + order_completed_event_sell.base_asset_amount) # self.assertAlmostEqual(quote_amount_traded, # order_completed_event_buy.quote_asset_amount + order_completed_event_sell.quote_asset_amount) # self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id_buy # for event in self.market_logger.event_log])) # # Reset the logs # self.market_logger.clear() # # # WARNING AUTOMATICALLY EXECUTES ORDER # def test_execute_limit_sell(self): # amount: Decimal = Decimal("0.04") # quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, # amount) # # bid_entries = self.market.order_books[trading_pair].bid_entries() # # ask_entries = self.market.order_books[trading_pair].ask_entries() # most_top_bid = next(bid_entries) # # most_top_ask = next(ask_entries) # # bid_price: Decimal = Decimal(most_top_bid.price) # # quantize_bid_price = self.market.quantize_order_price(trading_pair, bid_price) * Decimal("1.1") # # bid_price: Decimal = Decimal(most_top_bid.price) # min_price_increment = self.market.trading_rules[trading_pair].min_price_increment # bid_price_to_be_lifted = self.market.quantize_order_price(trading_pair, bid_price + min_price_increment) # # order_id_sell = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, bid_price_to_be_lifted) # print(order_id_sell) # _ = self.run_parallel( # self.market_logger.wait_for(BuyOrderCreatedEvent)) # # order_id_buy = self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, bid_price_to_be_lifted) # # # [order_completed_event_sell] = self.run_parallel( # # self.market_logger.wait_for(SellOrderCompletedEvent)) # # order_completed_event_buy, order_completed_event_sell = self.run_parallel( # self.market_logger.wait_for(SellOrderCompletedEvent), self.market_logger.wait_for(BuyOrderCompletedEvent)) # # order_completed_event_buy: SellOrderCompletedEvent = order_completed_event_buy # trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log # if isinstance(t, OrderFilledEvent)] # base_amount_traded: Decimal = sum(t.amount for t in trade_events) # quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) # # self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) # self.assertEqual(order_id_buy, order_completed_event_buy.order_id) # self.assertAlmostEqual(quantized_amount, # order_completed_event_buy.base_asset_amount) # self.assertEqual(base_asset, order_completed_event_buy.base_asset) # self.assertEqual(quote_asset, order_completed_event_buy.quote_asset) # self.assertAlmostEqual(base_amount_traded, # order_completed_event_sell.base_asset_amount + order_completed_event_buy.base_asset_amount) # self.assertAlmostEqual(quote_amount_traded, # order_completed_event_sell.quote_asset_amount + order_completed_event_buy.quote_asset_amount) # self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id_buy # for event in self.market_logger.event_log])) # # Reset the logs # self.market_logger.clear() # needs manual execution # def test_execute_limit_sell(self): # amount: Decimal = Decimal(0.02) # quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, # amount) # ask_entries = self.market.order_books[trading_pair].ask_entries() # most_top_ask = next(ask_entries) # ask_price: Decimal = Decimal(most_top_ask.price) # # quantize_ask_price = self.market.quantize_order_price(trading_pair, ask_price) * Decimal("0.9") # # order_id = self.market.sell(trading_pair, # quantized_amount, # OrderType.LIMIT, # quantize_ask_price, # ) # [order_completed_event] = self.run_parallel( # self.market_logger.wait_for(SellOrderCompletedEvent)) # # order_completed_event: SellOrderCompletedEvent = order_completed_event # trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log # if isinstance(t, OrderFilledEvent)] # base_amount_traded: Decimal = sum(t.amount for t in trade_events) # quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) # # self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) # self.assertEqual(order_id, order_completed_event.order_id) # self.assertAlmostEqual(quantized_amount, # order_completed_event.base_asset_amount) # self.assertEqual(base_asset, order_completed_event.base_asset) # self.assertEqual(quote_asset, order_completed_event.quote_asset) # self.assertAlmostEqual(base_amount_traded, # order_completed_event.base_asset_amount) # self.assertAlmostEqual(quote_amount_traded, # order_completed_event.quote_asset_amount) # self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id # for event in self.market_logger.event_log])) # # Reset the logs # self.market_logger.clear() def test_orders_saving_and_restoration(self): self.tearDownClass() self.setUpClass() self.setUp() config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() session = sql.get_new_session() try: self.assertEqual(0, len(self.market.tracking_states)) amount: Decimal = Decimal(".04") current_bid_price: Decimal = self.market.get_price( trading_pair, False) bid_price: Decimal = Decimal("0.9") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market, session=session) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: LatokenExchange = LatokenExchange( API_KEY, API_SECRET, trading_pairs=[trading_pair], domain=domain) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market, session=session) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.run_parallel(asyncio.sleep(5.0)) self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None recorder.save_market_states(config_path, self.market, session) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market, session=session) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) session.close() recorder.stop() self.setUpClass() # Place random orders and cancel them all. Beware : there are some time_out values that need # to be chosen carefully depending on the throttling policy of the exchange, otherwise it will not work def test_place_random_orders_and_cancel_all(self): # number of orders to be sent for testing cancellations (2 x order_count orders are sent : buy and sell) order_count = 100 # timeout in seconds due to throttling of TPS coming from the exchange time_out_open_orders_sec = 10 # timeout in seconds due to throttling of TPS coming from the exchange time_out_cancellations_sec = 10 bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.01") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.1")) order_ids = [] for i in range(order_count): # Define a random diff price to change the price of placed orders diff_price = self.market.trading_rules[ trading_pair].min_price_increment * random.randint(0, 9) quantize_bid_price = self.market.quantize_order_price( trading_pair, quantize_bid_price - diff_price) quantize_ask_price = self.market.quantize_order_price( trading_pair, quantize_ask_price + diff_price) order_id_buy = self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) order_id_sell = self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price) order_ids.append(order_id_buy) order_ids.append(order_id_sell) self.run_parallel(asyncio.sleep(time_out_open_orders_sec)) all_orders_opened = [ self.market.in_flight_orders[order_id] for order_id in order_ids ] self.assertEqual(order_count * 2, len(all_orders_opened)) are_all_orders_opened = [ order.current_state == OrderState.OPEN for order in all_orders_opened ] self.assertTrue(all(are_all_orders_opened)) are_all_orders_with_exchange_id = [ order.exchange_order_id is not None for order in all_orders_opened ] self.assertTrue(all(are_all_orders_with_exchange_id)) [cancellation_results] = self.run_parallel( self.market.cancel_all(time_out_cancellations_sec)) # all_failing_order_ids = [order_id not in self.market.all_orders for order_id in order_ids] # failing_order_ids = [order_id for order_id in order_ids if order_id not in self.market.all_orders] # failing_order_ids = [self.market.all_orders[order_id].exchange_order_id is None for order_id in order_ids] are_all_orders_cancelled = [ self.market.all_orders[order_id].current_state == OrderState.CANCELED for order_id in order_ids ] self.assertTrue(all(are_all_orders_cancelled)) for cr in cancellation_results: self.assertEqual(cr.success, True)
class DDEXMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: DDEXMarket market_logger: EventLogger wallet_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_test_private_key_ddex, backend_urls=conf.test_ddex_web3_provider_list, erc20_token_addresses=[ conf.test_ddex_erc20_token_address_1, conf.test_ddex_erc20_token_address_2 ], chain=EthereumChain.MAIN_NET) cls.market: DDEXMarket = DDEXMarket( wallet=cls.wallet, ethereum_rpc_url=conf.test_ddex_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType. EXCHANGE_API, symbols=["HOT-WETH"]) print("Initializing DDEX market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../ddex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): weth_trade_fee: TradeFee = self.market.get_fee("ZRX", "WETH", OrderType.LIMIT, TradeType.BUY, 10000, 1) self.assertGreater(weth_trade_fee.percent, 0) self.assertEqual(len(weth_trade_fee.flat_fees), 1) self.assertEqual(weth_trade_fee.flat_fees[0][0], "WETH") dai_trade_fee: TradeFee = self.market.get_fee("WETH", "DAI", OrderType.MARKET, TradeType.BUY, 10000) self.assertGreater(dai_trade_fee.percent, 0) self.assertEqual(len(dai_trade_fee.flat_fees), 1) self.assertEqual(dai_trade_fee.flat_fees[0][0], "DAI") def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), 0) self.assertGreaterEqual((balances["WETH"]), 0) def test_list_orders(self): [orders] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(orders), 0) def test_list_locked_balances(self): [locked_balances ] = self.run_parallel(self.market.list_locked_balances()) self.assertGreaterEqual(len(locked_balances), 0) @unittest.skipUnless( any("test_bad_orders_are_not_tracked" in arg for arg in sys.argv), "bad_orders_are_not_tracked test requires manual action.") def test_bad_orders_are_not_tracked(self): # Should fail due to insufficient balance order_id = self.market.buy("WETH-DAI", 10000, OrderType.LIMIT, 1) self.assertEqual(self.market.in_flight_orders.get(order_id), None) def test_cancel_order(self): symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) amount = 2000 # Intentionally setting invalid price to prevent getting filled client_order_id = self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.run_parallel(asyncio.sleep(6.0)) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(client_order_id, order_cancelled_event.order_id) def test_place_limit_buy_and_sell(self): self.assertGreater(self.market.get_balance("WETH"), 0.1) self.assertGreater(self.market.get_balance("HOT"), 2000) # Try to buy 2000 HOT from the exchange, and watch for completion event. symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) amount: float = 2000 buy_order_id: str = self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) [buy_order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) exchange_order_id: str = self.market.in_flight_orders.get( buy_order_id).exchange_order_id buy_order = self.run_parallel(self.market.get_order(exchange_order_id)) self.assertEqual(buy_order[0].get('id'), exchange_order_id) self.assertEqual(buy_order_id, buy_order_created_event.order_id) self.market.cancel(symbol, buy_order_id) [_] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) # Try to sell back the same amount of HOT to the exchange, and watch for completion event. ask_price: float = self.market.get_price(symbol, False) sell_order_id: str = self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) [sell_order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) exchange_order_id: str = self.market.in_flight_orders.get( sell_order_id).exchange_order_id sell_order = self.run_parallel( self.market.get_order(exchange_order_id)) self.assertEqual(sell_order[0].get('id'), exchange_order_id) self.assertEqual(sell_order_id, sell_order_created_event.order_id) self.market.cancel(symbol, sell_order_id) [_] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) @unittest.skipUnless( any("test_limit_buy_and_sell_get_matched" in arg for arg in sys.argv), "test_limit_buy_and_sell_get_matched test requires manual action.") def test_limit_buy_and_sell_get_matched(self): self.assertGreater(self.market.get_balance("WETH"), 0.01) # Try to buy 0.01 WETH worth of HOT from the exchange, and watch for completion event. current_price: float = self.market.get_price("HOT-WETH", True) amount: float = 2000 quantized_amount: Decimal = self.market.quantize_order_amount( "HOT-WETH", amount) order_id = self.market.buy("HOT-WETH", amount, OrderType.LIMIT, current_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of HOT to the exchange, and watch for completion event. current_price: float = self.market.get_price("HOT-WETH", False) amount = float(order_completed_event.base_asset_amount) quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("HOT-WETH", amount, OrderType.LIMIT, current_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in order_filled_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) def test_market_buy_and_sell(self): self.assertGreater(self.market.get_balance("WETH"), 0.01) amount: float = 2000.0 # Min order size is 1000 HOT quantized_amount: Decimal = self.market.quantize_order_amount( "HOT-WETH", amount) order_id = self.market.buy("HOT-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) # This is because some of the tokens are deducted in the trading fees. self.assertTrue( float(quantized_amount) > order_completed_event.base_asset_amount > float(quantized_amount) * 0.9) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of HOT to the exchange, and watch for completion event. amount = float(order_completed_event.base_asset_amount) quantized_amount = order_completed_event.base_asset_amount order_id = self.market.sell("HOT-WETH", amount, OrderType.MARKET) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event order_filled_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] self.assertTrue([ evt.order_type == OrderType.MARKET for evt in order_filled_events ]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("HOT", order_completed_event.base_asset) self.assertEqual("WETH", order_completed_event.quote_asset) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) @unittest.skipUnless(any("test_wrap_eth" in arg for arg in sys.argv), "Wrap Eth test requires manual action.") def test_wrap_eth(self): amount_to_wrap = 0.01 tx_hash = self.wallet.wrap_eth(amount_to_wrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletWrappedEthEvent)) tx_completed_event: WalletWrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_wrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) @unittest.skipUnless(any("test_unwrap_eth" in arg for arg in sys.argv), "Unwrap Eth test requires manual action.") def test_unwrap_eth(self): amount_to_unwrap = 0.01 tx_hash = self.wallet.unwrap_eth(amount_to_unwrap) [tx_completed_event] = self.run_parallel( self.wallet_logger.wait_for(WalletUnwrappedEthEvent)) tx_completed_event: WalletUnwrappedEthEvent = tx_completed_event self.assertEqual(tx_hash, tx_completed_event.tx_hash) self.assertEqual(amount_to_unwrap, tx_completed_event.amount) self.assertEqual(self.wallet.address, tx_completed_event.address) def test_cancel_all_happy_case(self): symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) ask_price: float = self.market.get_price(symbol, False) amount = 2000 self.assertGreater(self.market.get_balance("WETH"), 0.02) self.assertGreater(self.market.get_balance("HOT"), amount) # Intentionally setting invalid price to prevent getting filled self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) [cancellation_results] = self.run_parallel(self.market.cancel_all(10)) print(cancellation_results) self.assertGreater(len(cancellation_results), 0) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_cancel_all_failure_case(self): symbol = "HOT-WETH" bid_price: float = self.market.get_price(symbol, True) ask_price: float = self.market.get_price(symbol, False) # order submission should fail due to insufficient balance amount = 200000 self.assertLess(self.market.get_balance("WETH"), 100) self.assertLess(self.market.get_balance("HOT"), amount) self.market.buy(symbol, amount, OrderType.LIMIT, bid_price * 0.7) self.market.sell(symbol, amount, OrderType.LIMIT, ask_price * 1.5) [cancellation_results] = self.run_parallel(self.market.cancel_all(10)) print(cancellation_results) self.assertGreater(len(cancellation_results), 0) for cr in cancellation_results: self.assertEqual(cr.success, False) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "HOT-WETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.05 ETH worth of HOT, and watch for order creation event. current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price * 0.8 quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) amount: float = 0.05 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: DDEXMarket = DDEXMarket( wallet=self.wallet, ethereum_rpc_url=conf.test_ddex_web3_provider_list[0], order_book_tracker_data_source_type= OrderBookTrackerDataSourceType.EXCHANGE_API, symbols=[symbol]) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(1, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "HOT-WETH" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.05 ETH worth of HOT from the exchange, and watch for completion event. current_price: float = self.market.get_price(symbol, True) amount: float = 0.05 / current_price order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of HOT to the exchange, and watch for completion event. amount = float(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class HuobiMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: HuobiMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: HuobiMarket = HuobiMarket(conf.huobi_api_key, conf.huobi_secret_key, symbols=["ethusdt"]) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: HuobiMarket = HuobiMarket(conf.huobi_api_key, conf.huobi_secret_key, symbols=["ethusdt"]) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.market_2) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready and cls.market_2.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../huobi_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.market_2_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) self.market_2.add_listener(event_tag, self.market_2_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_2.remove_listener(event_tag, self.market_2_logger) self.market_logger = None self.market_2_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.SELL, 1, 10) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("eth"), 0.1) symbol = "ethusdt" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("eth", order_completed_event.base_asset) self.assertEqual("usdt", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): symbol = "ethusdt" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_ask_price: float = self.market.get_price(symbol, False) ask_price: float = current_ask_price - 0.05 * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price) order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("eth", order_completed_event.base_asset) self.assertEqual("usdt", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_buy(self): symbol = "ethusdt" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("eth", buy_order_completed_event.base_asset) self.assertEqual("usdt", buy_order_completed_event.quote_asset) self.assertAlmostEqual( base_amount_traded, float(buy_order_completed_event.base_asset_amount), places=4) self.assertAlmostEqual( quote_amount_traded, float(buy_order_completed_event.quote_asset_amount), places=4) self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_sell(self): symbol = "ethusdt" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), sell_order_completed_event.base_asset_amount) self.assertEqual("eth", sell_order_completed_event.base_asset) self.assertEqual("usdt", sell_order_completed_event.quote_asset) self.assertAlmostEqual( base_amount_traded, float(sell_order_completed_event.base_asset_amount)) self.assertAlmostEqual( quote_amount_traded, float(sell_order_completed_event.quote_asset_amount)) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): symbol = "ethusdt" current_bid_price: float = self.market.get_price(symbol, True) amount: float = 0.02 bid_price: float = current_bid_price - 0.1 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): symbol = "ethusdt" bid_price: float = self.market_2.get_price(symbol, True) * 0.5 ask_price: float = self.market_2.get_price(symbol, False) * 2 amount: float = 0.05 quantized_amount: Decimal = self.market_2.quantize_order_amount( symbol, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market_2.quantize_order_price( symbol, bid_price * 0.7) quantize_ask_price: Decimal = self.market_2.quantize_order_price( symbol, ask_price * 1.5) self.market_2.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market_2.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market_2.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price * 0.8 quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) amount: float = 0.04 quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: HuobiMarket = HuobiMarket( huobi_api_key=conf.huobi_api_key, huobi_secret_key=conf.huobi_secret_key, symbols=["ethusdt", "btcusdt"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. amount: float = 0.04 order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. amount = float(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
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 HuobiMarketUnitTest(AioHTTPTestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: HuobiMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) @classmethod def tearDownClass(cls) -> None: cls.stack.close() # get_application overrides the aiohttp.web and allows mocking api endpoints async def get_application(self): app = web.Application() self.mock_api = HuobiMockAPI() app.router.add_get("/mockSnapshot", self.mock_api.get_mock_snapshot) app.router.add_get("/market/tickers", self.mock_api.get_market_tickers) app.router.add_get("/account/accounts", self.mock_api.get_account_accounts) app.router.add_get("/common/timestamp", self.mock_api.get_common_timestamp) app.router.add_get("/common/symbols", self.mock_api.get_common_symbols) app.router.add_get("/account/accounts/{user_id}/balance", self.mock_api.get_user_balance) app.router.add_post("/order/orders/place", self.mock_api.post_order_place) app.router.add_get("/order/orders/{order_id}", self.mock_api.get_order_update) app.router.add_post("/order/orders/{order_id}/submitcancel", self.mock_api.post_submit_cancel) app.router.add_post("/order/orders/batchcancel", self.mock_api.post_batch_cancel) return app @staticmethod async def wait_til_ready(market, clock): while True: now = time.time() next_iteration = now // 1.0 + 1 if market.ready: break else: await clock.run_til(next_iteration) await asyncio.sleep(1.0) # setUp function from unittests is called before get_application so this needs # to be called manually before every test def customSetUp(self): self.market: HuobiMarket = HuobiMarket(MOCK_HUOBI_API_KEY, MOCK_HUOBI_SECRET_KEY, symbols=["ethusdt"]) # replace regular aiohttp client with test client self.market.shared_client: TestClient = self.client # replace default data source with mock data source mock_data_source: MockAPIOrderBookDataSource = MockAPIOrderBookDataSource( self.client, HuobiOrderBook, ["ethusdt"]) self.market.order_book_tracker.data_source = mock_data_source self.clock.add_iterator(self.market) self.run_parallel(self.wait_til_ready(self.market, self._clock)) self.db_path: str = realpath(join(__file__, "../huobi_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(0.5) return future.result() def run_parallel(self, *tasks): self.ev_loop = asyncio.get_event_loop() return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): self.customSetUp() limit_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.BUY, 1, 10) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) sell_trade_fee: TradeFee = self.market.get_fee("eth", "usdt", OrderType.LIMIT, TradeType.SELL, 1, 10) self.assertGreater(sell_trade_fee.percent, 0) self.assertEqual(len(sell_trade_fee.flat_fees), 0) def test_limit_buy(self): self.customSetUp() self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_BUY_ORDER_ID symbol = "ethusdt" amount: Decimal = Decimal(0.02) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_bid_price: Decimal = self.market.get_price(symbol, True) bid_price: Decimal = current_bid_price + Decimal( 0.05) * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = Decimal( sum(t.amount for t in trade_events)) quote_amount_traded: Decimal = Decimal( sum(t.amount * t.price for t in trade_events)) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("eth", order_completed_event.base_asset) self.assertEqual("usdt", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): self.customSetUp() self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_SELL_ORDER_ID symbol = "ethusdt" amount: Decimal = Decimal(0.02) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) current_ask_price: Decimal = self.market.get_price(symbol, False) ask_price: Decimal = current_ask_price - Decimal( 0.05) * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price) order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("eth", order_completed_event.base_asset) self.assertEqual("usdt", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_buy(self): self.customSetUp() self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_BUY_ORDER_ID symbol = "ethusdt" amount: Decimal = Decimal(0.02) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) buy_order_completed_event: BuyOrderCompletedEvent = buy_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = Decimal( sum(t.amount for t in trade_events)) quote_amount_traded: Decimal = Decimal( sum(t.amount * t.price for t in trade_events)) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, buy_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, buy_order_completed_event.base_asset_amount, places=4) self.assertEqual("eth", buy_order_completed_event.base_asset) self.assertEqual("usdt", buy_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, buy_order_completed_event.base_asset_amount, places=4) self.assertAlmostEqual(quote_amount_traded, buy_order_completed_event.quote_asset_amount, places=4) self.assertGreater(buy_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_sell(self): self.customSetUp() self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_SELL_ORDER_ID symbol = "ethusdt" amount: Decimal = Decimal(0.02) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) sell_order_completed_event: SellOrderCompletedEvent = sell_order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = Decimal(sum(t.amount for t in trade_events)) quote_amount_traded = Decimal( sum(t.amount * t.price for t in trade_events)) self.assertTrue( [evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, sell_order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, sell_order_completed_event.base_asset_amount) self.assertEqual("eth", sell_order_completed_event.base_asset) self.assertEqual("usdt", sell_order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, sell_order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, sell_order_completed_event.quote_asset_amount) self.assertGreater(sell_order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): self.customSetUp() self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_CANCEL_ORDER_ID symbol = "ethusdt" current_bid_price: Decimal = self.market.get_price(symbol, True) amount: Decimal = Decimal(0.02) bid_price: Decimal = current_bid_price * Decimal(0.9) quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): self.customSetUp() self.mock_api.cancel_all_order_ids = [ self.mock_api.MOCK_HUOBI_LIMIT_BUY_ORDER_ID, self.mock_api.MOCK_HUOBI_LIMIT_SELL_ORDER_ID, ] symbol = "ethusdt" bid_price: Decimal = self.market.get_price(symbol, True) * Decimal(0.5) ask_price: Decimal = self.market.get_price(symbol, False) * Decimal(2) amount: Decimal = Decimal(0.05) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) # Intentionally setting high price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price * Decimal(0.7)) quantize_ask_price: Decimal = self.market.quantize_order_price( symbol, ask_price * Decimal(1.5)) self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_BUY_ORDER_ID self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_SELL_ORDER_ID self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): self.customSetUp() config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price(symbol, True) bid_price: Decimal = current_bid_price * Decimal(0.8) quantize_bid_price: Decimal = self.market.quantize_order_price( symbol, bid_price) amount: Decimal = Decimal(0.04) quantized_amount: Decimal = self.market.quantize_order_amount( symbol, amount) self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_OPEN_ORDER_ID order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: HuobiMarket = HuobiMarket( huobi_api_key=MOCK_HUOBI_API_KEY, huobi_secret_key=MOCK_HUOBI_SECRET_KEY, symbols=["ethusdt", "btcusdt"]) self.market.shared_client: TestClient = self.client mock_data_source: MockAPIOrderBookDataSource = MockAPIOrderBookDataSource( self.client, HuobiOrderBook, ["ethusdt"]) self.market.order_book_tracker.data_source = mock_data_source for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_OPEN_ORDER_ID self.mock_api.order_response_dict[ self.mock_api. MOCK_HUOBI_LIMIT_OPEN_ORDER_ID]["data"]["state"] = "canceled" self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): self.customSetUp() config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. amount: Decimal = Decimal(0.04) self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_BUY_ORDER_ID order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. self.mock_api.order_id = self.mock_api.MOCK_HUOBI_MARKET_SELL_ORDER_ID amount: Decimal = Decimal( buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BeaxyExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure, ] market: BeaxyExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock(PRIVET_API_BASE_URL, []) cls.web_app.add_host_to_mock(PUBLIC_API_BASE_URL, []) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", PUBLIC_API_BASE_URL, "/api/v2/symbols", FixtureBeaxy.SYMBOLS) cls.web_app.update_response("get", PUBLIC_API_BASE_URL, "/api/v2/symbols/DASHBTC/book", FixtureBeaxy.TRADE_BOOK) cls.web_app.update_response("get", PUBLIC_API_BASE_URL, "/api/v2/symbols/DASHBTC/rate", FixtureBeaxy.EXCHANGE_RATE) cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/health", FixtureBeaxy.HEALTH) cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/wallets", FixtureBeaxy.BALANCES) cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/tradingsettings", FixtureBeaxy.TRADE_SETTINGS) cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/orders/open", FixtureBeaxy.ORDERS_OPEN_EMPTY) cls.web_app.update_response("get", PRIVET_API_BASE_URL, "/api/v2/orders/closed", FixtureBeaxy.ORDERS_CLOSED_EMPTY) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.beaxy.beaxy_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() MockWebSocketServerFactory.url_host_only = True MockWebSocketServerFactory.start_new_server( BeaxyConstants.TradingApi.WS_BASE_URL) MockWebSocketServerFactory.start_new_server( BeaxyConstants.PublicApi.WS_BASE_URL) cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect cls._auth_confirm_patcher = unittest.mock.patch( "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._BeaxyAuth__login_confirm" ) cls._auth_confirm_mock = cls._auth_confirm_patcher.start() cls._auth_session_patcher = unittest.mock.patch( "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._BeaxyAuth__get_session_data" ) cls._auth_session_mock = cls._auth_session_patcher.start() cls._auth_session_mock.return_value = { "sign_key": 123, "session_id": '123' } cls._auth_headers_patcher = unittest.mock.patch( "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth.get_token" ) cls._auth_headers_mock = cls._auth_headers_patcher.start() cls._auth_headers_mock.return_value = '123' cls._auth_poll_patcher = unittest.mock.patch( "hummingbot.connector.exchange.beaxy.beaxy_auth.BeaxyAuth._auth_token_polling_loop" ) cls._auth_poll_mock = cls._auth_poll_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BeaxyExchange = BeaxyExchange(API_KEY, API_SECRET, trading_pairs=["DASH-BTC"]) if API_MOCK_ENABLED: async def mock_status_polling_task(): pass # disable status polling as it will make orders update inconsistent from mock view cls.market._status_polling_task = asyncio.ensure_future( mock_status_polling_task()) cls.ev_loop.run_until_complete(cls.market._update_balances()) print("Initializing Beaxy market... this will take about a minute.") cls.clock.add_iterator(cls.market) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def setUp(self): self.db_path: str = realpath(join(__file__, "../beaxy_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def test_balances(self): balances = self.market.get_all_balances() self.assertGreater(len(balances), 0) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["beaxy_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0025"), taker_fee.percent) fee_overrides_config_map["beaxy_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["beaxy_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent) fee_overrides_config_map["beaxy_maker_fee"].value = Decimal('0.75') maker_fee: TradeFee = self.market.get_fee("BTC", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, ws_resps=[]): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: for delay, ws_resp in ws_resps: MockWebSocketServerFactory.send_str_threadsafe( BeaxyConstants.TradingApi.WS_BASE_URL, ws_resp, delay=delay) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exch_order_id): self.market.cancel(trading_pair, order_id) def test_limit_buy(self): if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_LIMIT_BUY_ORDER) amount: Decimal = Decimal("0.01") self.assertGreater(self.market.get_balance("BTC"), 0.00005) trading_pair = "DASH-BTC" price: Decimal = self.market.get_price(trading_pair, True) * Decimal(1.1) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT, price, [(3, FixtureBeaxy.TEST_LIMIT_BUY_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_LIMIT_BUY_WS_ORDER_COMPLETED)]) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("DASH", order_completed_event.base_asset) self.assertEqual("BTC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_LIMIT_SELL_ORDER) trading_pair = "DASH-BTC" self.assertGreater(self.market.get_balance("DASH"), 0.01) price: Decimal = self.market.get_price(trading_pair, False) * Decimal(0.9) amount: Decimal = Decimal("0.01") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT, price, [(3, FixtureBeaxy.TEST_LIMIT_SELL_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_LIMIT_SELL_WS_ORDER_COMPLETED)]) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("DASH", order_completed_event.base_asset) self.assertEqual("BTC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "DASH-BTC" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, 1) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, 1) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_UNFILLED_ORDER1) self.assertGreater(self.market.get_balance("BTC"), 0.00005) trading_pair = "DASH-BTC" current_bid_price: Decimal = self.market.get_price( trading_pair, True) * Decimal('0.8') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.01') quantized_bid_amount: Decimal = self.market.quantize_order_amount( trading_pair, bid_amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, current_ask_price) ask_amount: Decimal = Decimal('0.01') quantized_ask_amount: Decimal = self.market.quantize_order_amount( trading_pair, ask_amount) order_id1, exch_order_id_1 = self.place_order( True, trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price, [(3, FixtureBeaxy.TEST_UNFILLED_ORDER1_WS_ORDER_CREATED)]) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id1, order_created_event.order_id) if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_UNFILLED_ORDER2) order_id2, exch_order_id_2 = self.place_order( False, trading_pair, quantized_ask_amount, OrderType.LIMIT, quantize_ask_price, [(3, FixtureBeaxy.TEST_UNFILLED_ORDER2_WS_ORDER_CREATED)]) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id2, order_created_event.order_id) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_str_threadsafe( BeaxyConstants.TradingApi.WS_BASE_URL, FixtureBeaxy.TEST_UNFILLED_ORDER1_WS_ORDER_CANCELED, delay=3) MockWebSocketServerFactory.send_str_threadsafe( BeaxyConstants.TradingApi.WS_BASE_URL, FixtureBeaxy.TEST_UNFILLED_ORDER2_WS_ORDER_CANCELED, delay=3) self.web_app.update_response("delete", PRIVET_API_BASE_URL, "/api/v1/orders", "") self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_market_buy(self): if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_MARKET_BUY_ORDER) amount: Decimal = Decimal("0.01") self.assertGreater(self.market.get_balance("BTC"), 0.00005) trading_pair = "DASH-BTC" price: Decimal = self.market.get_price(trading_pair, True) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( True, trading_pair, quantized_amount, OrderType.MARKET, price, [(3, FixtureBeaxy.TEST_MARKET_BUY_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_MARKET_BUY_WS_ORDER_COMPLETED)]) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("DASH", order_completed_event.base_asset) self.assertEqual("BTC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_market_sell(self): if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_MARKET_SELL_ORDER) trading_pair = "DASH-BTC" self.assertGreater(self.market.get_balance("DASH"), 0.01) price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.01") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( False, trading_pair, quantized_amount, OrderType.MARKET, price, [(3, FixtureBeaxy.TEST_MARKET_SELL_WS_ORDER_CREATED), (5, FixtureBeaxy.TEST_MARKET_SELL_WS_ORDER_COMPLETED)]) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("DASH", order_completed_event.base_asset) self.assertEqual("BTC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_CANCEL_BUY_ORDER) self.web_app.update_response( "delete", PRIVET_API_BASE_URL, "/api/v2/orders/open/435118B0-A7F7-40D2-A409-820E8FC342A2", '') amount: Decimal = Decimal("0.01") self.assertGreater(self.market.get_balance("BTC"), 0.00005) trading_pair = "DASH-BTC" # make worst price so order wont be executed price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.5') quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT, price, [(3, FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER_COMPLETED)]) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_str_threadsafe( BeaxyConstants.TradingApi.WS_BASE_URL, FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER_CANCELED, delay=3) self.cancel_order(trading_pair, order_id, exch_order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): if API_MOCK_ENABLED: self.web_app.update_response( "delete", PRIVET_API_BASE_URL, "/api/v2/orders/open/435118B0-A7F7-40D2-A409-820E8FC342A2", '') self.assertGreater(self.market.get_balance("BTC"), 0.00005) self.assertGreater(self.market.get_balance("DASH"), 0.01) trading_pair = "DASH-BTC" # make worst price so order wont be executed current_bid_price: Decimal = self.market.get_price( trading_pair, True) * Decimal('0.5') quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.01') quantized_bid_amount: Decimal = self.market.quantize_order_amount( trading_pair, bid_amount) # make worst price so order wont be executed current_ask_price: Decimal = self.market.get_price( trading_pair, False) * Decimal('2') quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, current_ask_price) ask_amount: Decimal = Decimal('0.01') quantized_ask_amount: Decimal = self.market.quantize_order_amount( trading_pair, ask_amount) if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_CANCEL_ALL_ORDER1) _, exch_order_id_1 = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT_MAKER, quantize_bid_price) if API_MOCK_ENABLED: self.web_app.update_response("post", PRIVET_API_BASE_URL, "/api/v2/orders", FixtureBeaxy.TEST_CANCEL_ALL_ORDER2) _, exch_order_id_2 = self.place_order(False, trading_pair, quantized_ask_amount, OrderType.LIMIT_MAKER, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) if API_MOCK_ENABLED: MockWebSocketServerFactory.send_str_threadsafe( BeaxyConstants.TradingApi.WS_BASE_URL, FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER1_CANCELED, delay=3) MockWebSocketServerFactory.send_str_threadsafe( BeaxyConstants.TradingApi.WS_BASE_URL, FixtureBeaxy.TEST_CANCEL_BUY_WS_ORDER2_CANCELED, delay=3) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_cancel_empty(self): trading_pair = "DASH-BTC" self.cancel_order(trading_pair, '123', '123')
class IDEXMarketUnitTest(unittest.TestCase): market_events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] wallet_events: List[WalletEvent] = [ WalletEvent.WrappedEth, WalletEvent.UnwrappedEth ] wallet: Web3Wallet market: IDEXMarket market_logger: EventLogger wallet_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.wallet = Web3Wallet(private_key=conf.web3_test_private_key_idex, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[conf.test_idex_erc20_token_address_1, conf.test_idex_erc20_token_address_2], chain=EthereumChain.MAIN_NET) cls.market: IDEXMarket = IDEXMarket( idex_api_key=conf.idex_api_key, wallet=cls.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API, symbols=[ETH_QNT] ) print("Initializing IDEX market... ") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.wallet) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../idex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() self.wallet_logger = EventLogger() for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) for event_tag in self.wallet_events: self.wallet.add_listener(event_tag, self.wallet_logger) def tearDown(self): for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None for event_tag in self.wallet_events: self.wallet.remove_listener(event_tag, self.wallet_logger) self.wallet_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) await self.market.start_network() while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_wallet_balances(self): balances = self.market.get_all_balances() self.assertGreaterEqual((balances["ETH"]), s_decimal_0) def test_quantize_order_amount(self): amount = self.market.quantize_order_amount("ETH_QNT", Decimal(0.01)) self.assertEqual(amount, 0) amount = self.market.quantize_order_amount("ETH_QNT", Decimal(100000)) self.assertEqual(amount, 100000) def test_place_limit_buy_and_cancel(self): symbol = ETH_QNT buy_amount: Decimal = Decimal("16000000") buy_price = Decimal("0.00000001") buy_order_id: str = self.market.buy(symbol, buy_amount, OrderType.LIMIT, buy_price) [buy_order_opened_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(buy_amount, buy_order_opened_event.amount) self.assertEqual(ETH_QNT, buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) self.run_parallel(self.market.cancel_order(buy_order_id)) [buy_order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(buy_order_opened_event.order_id, buy_order_cancelled_event.order_id) def test_place_limit_sell_and_cancel(self): symbol = ETH_QNT sell_amount: Decimal = Decimal(5) sell_price = Decimal(100000000) sell_order_id: str = self.market.sell(symbol, sell_amount, OrderType.LIMIT, sell_price) [sell_order_opened_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(sell_amount, float(sell_order_opened_event.amount)) self.assertEqual(ETH_QNT, sell_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) self.run_parallel(self.market.cancel_order(sell_order_id)) [sell_order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(sell_order_opened_event.order_id, sell_order_cancelled_event.order_id) def test_cancel_all_happy_case(self): symbol = ETH_QNT buy_amount: Decimal = Decimal(17000000) buy_price = Decimal("0.00000001") buy_order_id: str = self.market.buy(symbol, buy_amount, OrderType.LIMIT, buy_price) [buy_order_opened_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(buy_order_id, buy_order_opened_event.order_id) self.assertEqual(buy_amount, float(buy_order_opened_event.amount)) self.assertEqual(ETH_QNT, buy_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, buy_order_opened_event.type) symbol = ETH_QNT sell_amount: Decimal = Decimal(5) sell_price = Decimal(110000000) sell_order_id: str = self.market.sell(symbol, sell_amount, OrderType.LIMIT, sell_price) [sell_order_opened_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(sell_order_id, sell_order_opened_event.order_id) self.assertEqual(sell_amount, float(sell_order_opened_event.amount)) self.assertEqual(ETH_QNT, sell_order_opened_event.symbol) self.assertEqual(OrderType.LIMIT, sell_order_opened_event.type) [cancellation_results] = self.run_parallel(self.market.cancel_all(30)) self.assertGreater(len(cancellation_results), 0) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_market_buy(self): symbol = ETH_QNT current_price: Decimal = Decimal(self.market.get_price(symbol, True)) buy_amount: Decimal = Decimal(0.16) / current_price buy_order_id: str = self.market.buy(symbol, buy_amount, OrderType.MARKET) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event self.assertEqual(buy_order_id, order_completed_event.order_id) def test_market_sell(self): symbol = ETH_QNT current_price: Decimal = Decimal(self.market.get_price(symbol, False)) sell_amount: Decimal = Decimal(0.155) / current_price sell_order_id: str = self.market.sell(symbol, sell_amount, OrderType.MARKET) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event self.assertEqual(sell_order_id, order_completed_event.order_id) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = ETH_QNT sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.05 ETH worth of QNT, and watch for order creation event. bid_price = Decimal("0.00000002") quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) amount: Decimal = Decimal("18000000") quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) expires = int(time.time() + 60 * 5) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price, expiration_ts=expires) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.market_events: self.market.remove_listener(event_tag, self.market_logger) self.market: IDEXMarket = IDEXMarket( idex_api_key=conf.idex_api_key, wallet=self.wallet, ethereum_rpc_url=conf.test_web3_provider_list[0], order_book_tracker_data_source_type=OrderBookTrackerDataSourceType.EXCHANGE_API, symbols=[ETH_QNT] ) for event_tag in self.market_events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(1, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = ETH_QNT sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.16 ETH worth of QNT from the exchange, and watch for completion event. current_price: Decimal = Decimal(self.market.get_price(symbol, True)) amount: Decimal = Decimal(0.16) / current_price order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of QNT to the exchange, and watch for completion event. amount = Decimal(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BittrexMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: BittrexMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() if API_MOCK_ENABLED: cls.web_app = HummingWebApp.get_instance() cls.web_app.add_host_to_mock(API_BASE_URL, []) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/v3/ping", FixtureBittrex.PING) cls.web_app.update_response("get", API_BASE_URL, "/v3/markets", FixtureBittrex.MARKETS) cls.web_app.update_response("get", API_BASE_URL, "/v3/markets/tickers", FixtureBittrex.MARKETS_TICKERS) cls.web_app.update_response("get", API_BASE_URL, "/v3/balances", FixtureBittrex.BALANCES) cls.web_app.update_response("get", API_BASE_URL, "/v3/orders/open", FixtureBittrex.ORDERS_OPEN) cls._t_nonce_patcher = unittest.mock.patch("hummingbot.market.bittrex.bittrex_market.get_tracking_nonce") cls._t_nonce_mock = cls._t_nonce_patcher.start() cls._us_patcher = unittest.mock.patch("hummingbot.market.bittrex.bittrex_api_user_stream_data_source." "BittrexAPIUserStreamDataSource._transform_raw_message", autospec=True) cls._us_mock = cls._us_patcher.start() cls._us_mock.side_effect = _transform_raw_message_patch cls._ob_patcher = unittest.mock.patch("hummingbot.market.bittrex.bittrex_api_order_book_data_source." "BittrexAPIOrderBookDataSource._transform_raw_message", autospec=True) cls._ob_mock = cls._ob_patcher.start() cls._ob_mock.side_effect = _transform_raw_message_patch HummingWsServerFactory.url_host_only = True ws_server = HummingWsServerFactory.start_new_server(WS_BASE_URL) cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect ws_server.add_stock_response("queryExchangeState", FixtureBittrex.WS_ORDER_BOOK_SNAPSHOT.copy()) cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BittrexMarket = BittrexMarket( bittrex_api_key=API_KEY, bittrex_secret_key=API_SECRET, trading_pairs=["ETH-USDT"] ) print("Initializing Bittrex market... this will take about a minute. ") cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() cls._ob_patcher.stop() cls._us_patcher.stop() cls._ws_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../bittrex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDT", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["bittrex_taker_fee"].value = None taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0025"), taker_fee.percent) fee_overrides_config_map["bittrex_taker_fee"].value = Decimal('0.2') taker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.MARKET, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["bittrex_maker_fee"].value = None maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0025"), maker_fee.percent) fee_overrides_config_map["bittrex_maker_fee"].value = Decimal('0.5') maker_fee: TradeFee = self.market.get_fee("LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, post_resp, ws_resp): global EXCHANGE_ORDER_ID order_id, exch_order_id = None, None if API_MOCK_ENABLED: exch_order_id = f"BITTREX_{EXCHANGE_ORDER_ID}" EXCHANGE_ORDER_ID += 1 self._t_nonce_mock.return_value = nonce resp = post_resp.copy() resp["id"] = exch_order_id side = 'buy' if is_buy else 'sell' resp["direction"] = side.upper() resp["type"] = order_type.name.upper() if order_type == OrderType.MARKET: del resp["limit"] self.web_app.update_response("post", API_BASE_URL, "/v3/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = ws_resp.copy() resp["content"]["o"]["OU"] = exch_order_id HummingWsServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=1.0) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exch_order_id): if API_MOCK_ENABLED: resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id}", resp) self.market.cancel(trading_pair, order_id) def test_limit_buy(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" self.run_parallel(asyncio.sleep(3)) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price * Decimal('1.005') quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): trading_pair = "ETH-USDT" current_ask_price: Decimal = self.market.get_price(trading_pair, False) ask_price: Decimal = current_ask_price - Decimal('0.005') * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, ask_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_market_buy(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.MARKET, 0, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_market_sell(self): trading_pair = "ETH-USDT" self.assertGreater(self.market.get_balance("ETH"), 0.06) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(False, trading_pair, quantized_amount, OrderType.MARKET, 0, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.MARKET for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, current_bid_price) amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) self.cancel_order(trading_pair, order_id, exch_order_id) [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.06') quantized_bid_amount: Decimal = self.market.quantize_order_amount(trading_pair, bid_amount) current_ask_price: Decimal = self.market.get_price(trading_pair, False) quantize_ask_price: Decimal = self.market.quantize_order_price(trading_pair, current_ask_price) ask_amount: Decimal = Decimal('0.06') quantized_ask_amount: Decimal = self.market.quantize_order_amount(trading_pair, ask_amount) _, exch_order_id_1 = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) _, exch_order_id_2 = self.place_order(False, trading_pair, quantized_ask_amount, OrderType.LIMIT, quantize_ask_price, 10002, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id_1 self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id_1}", resp) resp = FixtureBittrex.ORDER_CANCEL.copy() resp["id"] = exch_order_id_2 self.web_app.update_response("delete", API_BASE_URL, f"/v3/orders/{exch_order_id_2}", resp) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("USDT"), 20) trading_pair = "ETH-USDT" current_bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.06') quantized_bid_amount: Decimal = self.market.quantize_order_amount(trading_pair, bid_amount) self.market.buy(trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) current_bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.80') quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, current_bid_price) bid_amount: Decimal = Decimal('0.06') quantized_bid_amount: Decimal = self.market.quantize_order_amount(trading_pair, bid_amount) order_id, exch_order_id = self.place_order(True, trading_pair, quantized_bid_amount, OrderType.LIMIT, quantize_bid_price, 10001, FixtureBittrex.ORDER_PLACE_OPEN, FixtureBittrex.WS_ORDER_OPEN) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BittrexMarket = BittrexMarket( bittrex_api_key=API_KEY, bittrex_secret_key=API_SECRET, trading_pairs=["XRP-BTC"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDT" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: amount: Decimal = Decimal("0.06") quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) order_id, _ = self.place_order(True, trading_pair, quantized_amount, OrderType.MARKET, 0, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() amount = Decimal(buy_order_completed_event.base_asset_amount) order_id, _ = self.place_order(False, trading_pair, amount, OrderType.MARKET, 0, 10001, FixtureBittrex.ORDER_PLACE_FILLED, FixtureBittrex.WS_ORDER_FILLED) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class CoinbaseProExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] market: CoinbaseProExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.ev_loop = asyncio.get_event_loop() trading_pair = "ETH-USDC" if API_MOCK_ENABLED: cls.web_app = MockWebServer.get_instance() cls.web_app.add_host_to_mock( API_BASE_URL, ["/time", "/products", f"/products/{trading_pair}/book"]) cls.web_app.start() cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) cls._patcher = mock.patch("aiohttp.client.URL") cls._url_mock = cls._patcher.start() cls._url_mock.side_effect = cls.web_app.reroute_local cls.web_app.update_response("get", API_BASE_URL, "/accounts", FixtureCoinbasePro.BALANCES) cls.web_app.update_response("get", API_BASE_URL, "/fees", FixtureCoinbasePro.TRADE_FEES) cls.web_app.update_response("get", API_BASE_URL, "/orders", FixtureCoinbasePro.ORDERS_STATUS) MockWebSocketServerFactory.start_new_server(WS_BASE_URL) cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) cls._ws_mock = cls._ws_patcher.start() cls._ws_mock.side_effect = MockWebSocketServerFactory.reroute_ws_connect cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.coinbase_pro.coinbase_pro_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: CoinbaseProExchange = CoinbaseProExchange( API_KEY, API_SECRET, API_PASSPHRASE, trading_pairs=[trading_pair]) print( "Initializing Coinbase Pro market... this will take about a minute." ) cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() if API_MOCK_ENABLED: cls.web_app.stop() cls._patcher.stop() cls._t_nonce_patcher.stop() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath( join(__file__, "../coinbase_pro_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDC", OrderType.LIMIT_MAKER, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee( "ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_fee_overrides_config(self): fee_overrides_config_map["coinbase_pro_taker_fee"].value = None taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), taker_fee.percent) fee_overrides_config_map["coinbase_pro_taker_fee"].value = Decimal( '0.2') taker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.002"), taker_fee.percent) fee_overrides_config_map["coinbase_pro_maker_fee"].value = None maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.005"), maker_fee.percent) fee_overrides_config_map["coinbase_pro_maker_fee"].value = Decimal( '0.75') maker_fee: AddedToCostTradeFee = self.market.get_fee( "LINK", "ETH", OrderType.LIMIT_MAKER, TradeType.BUY, Decimal(1), Decimal('0.1')) self.assertAlmostEqual(Decimal("0.0075"), maker_fee.percent) def place_order(self, is_buy, trading_pair, amount, order_type, price, nonce, fixture_resp, fixture_ws): order_id, exch_order_id = None, None if API_MOCK_ENABLED: self._t_nonce_mock.return_value = nonce side = 'buy' if is_buy else 'sell' resp = fixture_resp.copy() exch_order_id = resp["id"] resp["side"] = side self.web_app.update_response("post", API_BASE_URL, "/orders", resp) if is_buy: order_id = self.market.buy(trading_pair, amount, order_type, price) else: order_id = self.market.sell(trading_pair, amount, order_type, price) if API_MOCK_ENABLED: resp = fixture_ws.copy() resp["order_id"] = exch_order_id resp["side"] = side MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) return order_id, exch_order_id def cancel_order(self, trading_pair, order_id, exchange_order_id, fixture_ws): if API_MOCK_ENABLED: self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exchange_order_id}", exchange_order_id) self.market.cancel(trading_pair, order_id) if API_MOCK_ENABLED: resp = fixture_ws.copy() resp["order_id"] = exchange_order_id MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) def test_limit_maker_rejections(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDC" # Try to put a buy limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('1.02') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.02")) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) self.market_logger.clear() # Try to put a sell limit maker order that is going to match, this should triggers order failure event. price: Decimal = self.market.get_price(trading_pair, True) * Decimal('0.98') price: Decimal = self.market.quantize_order_price(trading_pair, price) amount = self.market.quantize_order_amount(trading_pair, Decimal("0.02")) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT_MAKER, price) [order_failure_event] = self.run_parallel( self.market_logger.wait_for(MarketOrderFailureEvent)) self.assertEqual(order_id, order_failure_event.order_id) def test_limit_makers_unfilled(self): if API_MOCK_ENABLED: return trading_pair = "ETH-USDC" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) order_id_2, exch_order_id_2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureCoinbasePro.OPEN_SELL_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id_2, order_created_event.order_id) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id}", exch_order_id) self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id_2}", exch_order_id_2) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) if API_MOCK_ENABLED: resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.copy() resp["order_id"] = exch_order_id MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.copy() resp["order_id"] = exch_order_id_2 MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.11) for cr in cancellation_results: self.assertEqual(cr.success, True) # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_limit_taker_buy(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETH-USDC" price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureCoinbasePro.BUY_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT_MAKER for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_limit_taker_sell(self): trading_pair = "ETH-USDC" price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, _ = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT, price, 10001, FixtureCoinbasePro.BUY_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT_MAKER for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): trading_pair = "ETH-USDC" current_bid_price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.2") self.assertGreater(self.market.get_balance("ETH"), amount) bid_price: Decimal = current_bid_price - Decimal( "0.1") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELLED) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, order_id) def test_cancel_all(self): trading_pair = "ETH-USDC" bid_price: Decimal = self.market.get_price(trading_pair, True) * Decimal("0.5") ask_price: Decimal = self.market.get_price(trading_pair, False) * 2 amount: Decimal = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.7")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.5")) _, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) _, exch_order_id_2 = self.place_order( False, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_ask_price, 10002, FixtureCoinbasePro.OPEN_SELL_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) self.run_parallel(asyncio.sleep(1)) if API_MOCK_ENABLED: self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id}", exch_order_id) self.web_app.update_response("delete", API_BASE_URL, f"/orders/{exch_order_id_2}", exch_order_id_2) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) if API_MOCK_ENABLED: resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.copy() resp["order_id"] = exch_order_id MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.1) resp = FixtureCoinbasePro.WS_ORDER_CANCELLED.copy() resp["order_id"] = exch_order_id_2 MockWebSocketServerFactory.send_json_threadsafe(WS_BASE_URL, resp, delay=0.11) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), Decimal("0.1")) trading_pair = "ETH-USDC" amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price + Decimal( "0.05") * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price( trading_pair, True) bid_price: Decimal = current_bid_price * Decimal("0.8") quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) amount: Decimal = Decimal("0.02") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) order_id, exch_order_id = self.place_order( True, trading_pair, quantized_amount, OrderType.LIMIT_MAKER, quantize_bid_price, 10001, FixtureCoinbasePro.OPEN_BUY_LIMIT_ORDER, FixtureCoinbasePro.WS_ORDER_OPEN) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: CoinbaseProExchange = CoinbaseProExchange( coinbase_pro_api_key=API_KEY, coinbase_pro_secret_key=API_SECRET, coinbase_pro_passphrase=API_PASSPHRASE, trading_pairs=["ETH-USDC"]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELLED) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELLED) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.market.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, True) amount: Decimal = Decimal("0.02") order_id, exch_order_id = self.place_order( True, trading_pair, amount, OrderType.LIMIT, price, 10001, FixtureCoinbasePro.BUY_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [buy_order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. price: Decimal = self.market.get_price(trading_pair, False) amount = buy_order_completed_event.base_asset_amount order_id, exch_order_id = self.place_order( False, trading_pair, amount, OrderType.LIMIT, price, 10002, FixtureCoinbasePro.SELL_MARKET_ORDER, FixtureCoinbasePro.WS_AFTER_MARKET_BUY_2) [sell_order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.cancel_order(trading_pair, order_id, exch_order_id, FixtureCoinbasePro.WS_ORDER_CANCELLED) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)
class BitfinexExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, ] market: BitfinexExchange market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: BitfinexExchange = BitfinexExchange( API_KEY, API_SECRET, trading_pairs=[trading_pair]) cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../bitfinex_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: AddedToCostTradeFee = self.market.get_fee( base_asset, quote_asset, OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: AddedToCostTradeFee = self.market.get_fee( base_asset, quote_asset, OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_minimum_order_size(self): amount = Decimal("0.001") quantized_amount = self.market.quantize_order_amount( trading_pair, amount) self.assertEqual(quantized_amount, 0) def test_get_balance(self): balance = self.market.get_balance(quote_asset) self.assertGreater(balance, 10) def test_limit_buy(self): amount: Decimal = Decimal("0.04") current_ask_price: Decimal = self.market.get_price(trading_pair, False) # no fill bid_price: Decimal = Decimal("0.9") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) # Wait for order creation event self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) # Cancel order. Automatically asserts that order is tracked self.market.cancel(trading_pair, order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(order_cancelled_event.order_id, order_id) # # Reset the logs self.market_logger.clear() def test_limit_sell(self): amount: Decimal = Decimal("0.02") current_ask_price: Decimal = self.market.get_price(trading_pair, False) # for no fill ask_price: Decimal = Decimal("1.1") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price) order_id = self.market.sell(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) # Wait for order creation event self.run_parallel(self.market_logger.wait_for(SellOrderCreatedEvent)) # Cancel order. Automatically asserts that order is tracked self.market.cancel(trading_pair, order_id) [order_cancelled_event] = self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) self.assertEqual(order_cancelled_event.order_id, order_id) # Reset the logs self.market_logger.clear() def test_execute_limit_buy(self): amount: Decimal = Decimal("0.04") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) bid_entries = self.market.order_books[trading_pair].bid_entries() most_top_bid = next(bid_entries) bid_price: Decimal = Decimal(most_top_bid.price) quantize_bid_price: Decimal = \ self.market.quantize_order_price(trading_pair, bid_price) quantize_bid_price = quantize_bid_price * Decimal("1.1") order_id = self.market.buy( trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price, ) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual(base_asset, order_completed_event.base_asset) self.assertEqual(quote_asset, order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_execute_limit_sell(self): amount: Decimal = Decimal(0.02) quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) ask_entries = self.market.order_books[trading_pair].ask_entries() most_top_ask = next(ask_entries) ask_price: Decimal = Decimal(most_top_ask.price) quantize_ask_price: Decimal = \ self.market.quantize_order_price(trading_pair, ask_price) quantize_ask_price = quantize_ask_price * Decimal("0.9") order_id = self.market.sell( trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price, ) [order_completed_event] = self.run_parallel( self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [ t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded: Decimal = sum(t.amount for t in trade_events) quote_amount_traded: Decimal = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(quantized_amount, order_completed_event.base_asset_amount) self.assertEqual(base_asset, order_completed_event.base_asset) self.assertEqual(quote_asset, order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log ])) # Reset the logs self.market_logger.clear() def test_orders_saving_and_restoration(self): self.tearDownClass() self.setUpClass() self.setUp() config_path: str = "test_config" strategy_name: str = "test_strategy" sql: SQLConnectionManager = SQLConnectionManager( SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) amount: Decimal = Decimal("0.04") current_ask_price: Decimal = self.market.get_price( trading_pair, False) bid_price: Decimal = Decimal("0.9") * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price) order_id = self.market.buy(trading_pair, amount, OrderType.LIMIT, quantize_ask_price) [order_created_event] = self.run_parallel( self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: BitfinexExchange = BitfinexExchange( API_KEY, API_SECRET, trading_pairs=[trading_pair]) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.run_parallel(asyncio.sleep(5.0)) self.market.cancel(trading_pair, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states( config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(trading_pair, order_id) self.run_parallel( self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() self.setUpClass() def test_cancel_all(self): bid_price: Decimal = self.market.get_price(trading_pair, True) ask_price: Decimal = self.market.get_price(trading_pair, False) amount: Decimal = Decimal("0.04") quantized_amount: Decimal = self.market.quantize_order_amount( trading_pair, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price( trading_pair, bid_price * Decimal("0.9")) quantize_ask_price: Decimal = self.market.quantize_order_price( trading_pair, ask_price * Decimal("1.1")) self.market.buy(trading_pair, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(trading_pair, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(5)) [cancellation_results] = self.run_parallel(self.market.cancel_all(45)) for cr in cancellation_results: self.assertEqual(cr.success, True)
class DigifinexExchangeUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.OrderFilled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled, MarketEvent.OrderFailure ] connector: DigifinexExchange event_logger: EventLogger trading_pair = "BTC-USDT" base_token, quote_token = trading_pair.split("-") stack: contextlib.ExitStack sql: SQLConnectionManager @classmethod def setUpClass(cls): global MAINNET_RPC_URL cls.ev_loop = asyncio.get_event_loop() if API_MOCK_ENABLED: raise NotImplementedError() # cls.web_app = HummingWebApp.get_instance() # cls.web_app.add_host_to_mock(BASE_API_URL, []) # cls.web_app.start() # cls.ev_loop.run_until_complete(cls.web_app.wait_til_started()) # cls._patcher = mock.patch("aiohttp.client.URL") # cls._url_mock = cls._patcher.start() # cls._url_mock.side_effect = cls.web_app.reroute_local # cls.web_app.update_response("get", BASE_API_URL, "/v2/public/get-ticker", fixture.TICKERS) # cls.web_app.update_response("get", BASE_API_URL, "/v2/public/get-instruments", fixture.INSTRUMENTS) # cls.web_app.update_response("get", BASE_API_URL, "/v2/public/get-book", fixture.GET_BOOK) # cls.web_app.update_response("post", BASE_API_URL, "/v2/private/get-account-summary", fixture.BALANCES) # cls.web_app.update_response("post", BASE_API_URL, "/v2/private/cancel-order", fixture.CANCEL) # HummingWsServerFactory.start_new_server(WSS_PRIVATE_URL) # HummingWsServerFactory.start_new_server(WSS_PUBLIC_URL) # cls._ws_patcher = unittest.mock.patch("websockets.connect", autospec=True) # cls._ws_mock = cls._ws_patcher.start() # cls._ws_mock.side_effect = HummingWsServerFactory.reroute_ws_connect cls.clock: Clock = Clock(ClockMode.REALTIME) cls.connector: DigifinexExchange = DigifinexExchange( digifinex_api_key=API_KEY, digifinex_secret_key=API_SECRET, trading_pairs=[cls.trading_pair], trading_required=True) print( "Initializing Digifinex market... this will take about a minute.") cls.clock.add_iterator(cls.connector) cls.stack: contextlib.ExitStack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) # if API_MOCK_ENABLED: # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_INITIATED, delay=0.5) # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_SUBSCRIBE, delay=0.51) # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_HEARTBEAT, delay=0.52) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() # if API_MOCK_ENABLED: # cls.web_app.stop() # cls._patcher.stop() # cls._ws_patcher.stop() @classmethod async def wait_til_ready(cls, connector=None): if connector is None: connector = cls.connector while True: now = time.time() next_iteration = now // 1.0 + 1 if connector.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../connector_test.sqlite")) try: # on windows cannot unlink the sqlite db file before closing the db if os.name != 'nt': os.unlink(self.db_path) except FileNotFoundError: pass self.event_logger = EventLogger() for event_tag in self.events: self.connector.add_listener(event_tag, self.event_logger) def tearDown(self): for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) self.event_logger = None # self.sql._engine.dispose() async def run_parallel_async(self, *tasks): future: asyncio.Future = safe_ensure_future(safe_gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self._clock.run_til(next_iteration) await asyncio.sleep(1.0) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_estimate_fee(self): maker_fee = self.connector.estimate_fee_pct(True) self.assertAlmostEqual(maker_fee, Decimal("0.001")) taker_fee = self.connector.estimate_fee_pct(False) self.assertAlmostEqual(taker_fee, Decimal("0.001")) def _place_order(self, is_buy, amount, order_type, price, ex_order_id, get_order_fixture=None, ws_trade_fixture=None, ws_order_fixture=None) -> str: # if API_MOCK_ENABLED: # data = fixture.PLACE_ORDER.copy() # data["result"]["order_id"] = str(ex_order_id) # self.web_app.update_response("post", BASE_API_URL, "/v2/private/create-order", data) if is_buy: cl_order_id = self.connector.buy(self.trading_pair, amount, order_type, price) else: cl_order_id = self.connector.sell(self.trading_pair, amount, order_type, price) # if API_MOCK_ENABLED: # if get_order_fixture is not None: # data = get_order_fixture.copy() # data["result"]["order_info"]["client_oid"] = cl_order_id # data["result"]["order_info"]["order_id"] = ex_order_id # self.web_app.update_response("post", BASE_API_URL, "/v2/private/get-order-detail", data) # if ws_trade_fixture is not None: # data = ws_trade_fixture.copy() # data["result"]["data"][0]["order_id"] = str(ex_order_id) # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.1) # if ws_order_fixture is not None: # data = ws_order_fixture.copy() # data["result"]["data"][0]["order_id"] = str(ex_order_id) # data["result"]["data"][0]["client_oid"] = cl_order_id # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.12) return cl_order_id def _cancel_order(self, cl_order_id): self.connector.cancel(self.trading_pair, cl_order_id) # if API_MOCK_ENABLED: # data = fixture.WS_ORDER_CANCELED.copy() # data["result"]["data"][0]["client_oid"] = cl_order_id # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.1) def test_buy_and_sell(self): self.ev_loop.run_until_complete(self.connector.cancel_all(0)) price = self.connector.get_price(self.trading_pair, True) * Decimal("1.05") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001")) quote_bal = self.connector.get_available_balance(self.quote_token) base_bal = self.connector.get_available_balance(self.base_token) order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1, None, fixture.WS_TRADE) order_completed_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(2)) trade_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(amount, order_completed_event.base_asset_amount) self.assertEqual("BTC", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) self.assertTrue( any([ isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.event_logger.event_log ])) # check available quote balance gets updated, we need to wait a bit for the balance message to arrive expected_quote_bal = quote_bal - quote_amount_traded # self._mock_ws_bal_update(self.quote_token, expected_quote_bal) self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertAlmostEqual(expected_quote_bal, self.connector.get_available_balance( self.quote_token), delta=0.1) # Reset the logs self.event_logger.clear() # Try to sell back the same amount to the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("0.95") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001")) order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2, None, fixture.WS_TRADE) order_completed_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) trade_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderFilledEvent) ] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue( [evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertEqual(amount, order_completed_event.base_asset_amount) self.assertEqual("BTC", order_completed_event.base_asset) self.assertEqual("USDT", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, order_completed_event.base_asset_amount) self.assertAlmostEqual(quote_amount_traded, order_completed_event.quote_asset_amount) # todo: get fee # self.assertGreater(order_completed_event.fee_amount, Decimal(0)) self.assertTrue( any([ isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.event_logger.event_log ])) # check available base balance gets updated, we need to wait a bit for the balance message to arrive expected_base_bal = base_bal # self._mock_ws_bal_update(self.base_token, expected_base_bal) self.ev_loop.run_until_complete(asyncio.sleep(1)) self.assertAlmostEqual( expected_base_bal, self.connector.get_available_balance(self.base_token), 5) def test_limit_makers_unfilled(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("0.8") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.00005")) quote_bal = self.connector.get_available_balance(self.quote_token) # order_id = self.connector.buy(self.trading_pair, amount, OrderType.LIMIT_MAKER, price) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1, fixture.UNFILLED_ORDER) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) # check available quote balance gets updated, we need to wait a bit for the balance message to arrive expected_quote_bal = quote_bal - (price * amount) # self._mock_ws_bal_update(self.quote_token, expected_quote_bal) self.ev_loop.run_until_complete(asyncio.sleep(2)) self.assertAlmostEqual( expected_quote_bal, self.connector.get_available_balance(self.quote_token), 1) self._cancel_order(cl_order_id) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) price = self.connector.get_price(self.trading_pair, True) * Decimal("1.2") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001")) cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2, fixture.UNFILLED_ORDER) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) self._cancel_order(cl_order_id) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) # def _mock_ws_bal_update(self, token, available): # if API_MOCK_ENABLED: # available = float(available) # data = fixture.WS_BALANCE.copy() # data["result"]["data"][0]["currency"] = token # data["result"]["data"][0]["available"] = available # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, fixture.WS_BALANCE, delay=0.1) def test_limit_maker_rejections(self): price = self.connector.get_price(self.trading_pair, True) * Decimal("1.2") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001")) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1, None, None, fixture.WS_ORDER_CANCELED) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) price = self.connector.get_price(self.trading_pair, False) * Decimal("0.8") price = self.connector.quantize_order_price(self.trading_pair, price) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001")) cl_order_id = self._place_order(False, amount, OrderType.LIMIT_MAKER, price, 2, None, None, fixture.WS_ORDER_CANCELED) event = self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) self.assertEqual(cl_order_id, event.order_id) def test_cancel_all(self): bid_price = self.connector.get_price(self.trading_pair, True) ask_price = self.connector.get_price(self.trading_pair, False) bid_price = self.connector.quantize_order_price( self.trading_pair, bid_price * Decimal("0.7")) ask_price = self.connector.quantize_order_price( self.trading_pair, ask_price * Decimal("1.5")) amount = self.connector.quantize_order_amount(self.trading_pair, Decimal("0.0001")) buy_id = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1) sell_id = self._place_order(False, amount, OrderType.LIMIT, ask_price, 2) self.ev_loop.run_until_complete(asyncio.sleep(1)) asyncio.ensure_future(self.connector.cancel_all(3)) # if API_MOCK_ENABLED: # data = fixture.WS_ORDER_CANCELED.copy() # data["result"]["data"][0]["client_oid"] = buy_id # data["result"]["data"][0]["order_id"] = 1 # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.1) # self.ev_loop.run_until_complete(asyncio.sleep(1)) # data = fixture.WS_ORDER_CANCELED.copy() # data["result"]["data"][0]["client_oid"] = sell_id # data["result"]["data"][0]["order_id"] = 2 # HummingWsServerFactory.send_json_threadsafe(WSS_PRIVATE_URL, data, delay=0.11) self.ev_loop.run_until_complete(asyncio.sleep(3)) cancel_events = [ t for t in self.event_logger.event_log if isinstance(t, OrderCancelledEvent) ] self.assertEqual({buy_id, sell_id}, {o.order_id for o in cancel_events}) def test_order_price_precision(self): bid_price: Decimal = self.connector.get_price(self.trading_pair, True) ask_price: Decimal = self.connector.get_price(self.trading_pair, False) mid_price: Decimal = (bid_price + ask_price) / 2 amount: Decimal = Decimal("0.000123456") # Make sure there's enough balance to make the limit orders. self.assertGreater(self.connector.get_balance("BTC"), Decimal("0.001")) self.assertGreater(self.connector.get_balance("USDT"), Decimal("10")) # Intentionally set some prices with too many decimal places s.t. they # need to be quantized. Also, place them far away from the mid-price s.t. they won't # get filled during the test. bid_price = mid_price * Decimal("0.9333192292111341") ask_price = mid_price * Decimal("1.0492431474884933") cl_order_id_1 = self._place_order(True, amount, OrderType.LIMIT, bid_price, 1, fixture.UNFILLED_ORDER) # Wait for the order created event and examine the order made self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) order = self.connector.in_flight_orders[cl_order_id_1] quantized_bid_price = self.connector.quantize_order_price( self.trading_pair, bid_price) quantized_bid_size = self.connector.quantize_order_amount( self.trading_pair, amount) self.assertEqual(quantized_bid_price, order.price) self.assertEqual(quantized_bid_size, order.amount) # Test ask order cl_order_id_2 = self._place_order(False, amount, OrderType.LIMIT, ask_price, 1, fixture.UNFILLED_ORDER) # Wait for the order created event and examine and order made self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCreatedEvent)) order = self.connector.in_flight_orders[cl_order_id_2] quantized_ask_price = self.connector.quantize_order_price( self.trading_pair, Decimal(ask_price)) quantized_ask_size = self.connector.quantize_order_amount( self.trading_pair, Decimal(amount)) self.assertEqual(quantized_ask_price, order.price) self.assertEqual(quantized_ask_size, order.amount) self._cancel_order(cl_order_id_1) self._cancel_order(cl_order_id_2) def test_orders_saving_and_restoration(self): config_path = "test_config" strategy_name = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id = None recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: self.connector._in_flight_orders.clear() self.assertEqual(0, len(self.connector.tracking_states)) # Try to put limit buy order for 0.02 ETH worth of ZRX, and watch for order creation event. current_bid_price: Decimal = self.connector.get_price( self.trading_pair, True) price: Decimal = current_bid_price * Decimal("0.8") price = self.connector.quantize_order_price( self.trading_pair, price) amount: Decimal = Decimal("0.0001") amount = self.connector.quantize_order_amount( self.trading_pair, amount) cl_order_id = self._place_order(True, amount, OrderType.LIMIT_MAKER, price, 1, fixture.UNFILLED_ORDER) order_created_event = self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCreatedEvent)) self.assertEqual(cl_order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.connector.tracking_states)) self.assertEqual(cl_order_id, list(self.connector.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[ Order] = recorder.get_orders_for_config_and_market( config_path, self.connector) self.assertEqual(1, len(recorded_orders)) self.assertEqual(cl_order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states( config_path, self.connector) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.connector.stop(self._clock) self.ev_loop.run_until_complete(asyncio.sleep(5)) self.clock.remove_iterator(self.connector) for event_tag in self.events: self.connector.remove_listener(event_tag, self.event_logger) new_connector = DigifinexExchange(API_KEY, API_SECRET, [self.trading_pair], True) for event_tag in self.events: new_connector.add_listener(event_tag, self.event_logger) recorder.stop() recorder = MarketsRecorder(sql, [new_connector], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states( config_path, new_connector) self.clock.add_iterator(new_connector) if not API_MOCK_ENABLED: self.ev_loop.run_until_complete( self.wait_til_ready(new_connector)) self.assertEqual(0, len(new_connector.limit_orders)) self.assertEqual(0, len(new_connector.tracking_states)) new_connector.restore_tracking_states( saved_market_states.saved_state) self.assertEqual(1, len(new_connector.limit_orders)) self.assertEqual(1, len(new_connector.tracking_states)) # Cancel the order and verify that the change is saved. self._cancel_order(cl_order_id) self.ev_loop.run_until_complete( self.event_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(new_connector.limit_orders)) self.assertEqual(0, len(new_connector.tracking_states)) saved_market_states = recorder.get_market_states( config_path, new_connector) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.connector.cancel(self.trading_pair, cl_order_id) self.run_parallel( self.event_logger.wait_for(OrderCancelledEvent)) recorder.stop() # sql._engine.dispose() # on windows cannot unlink the sqlite db file before closing the db if os.name != 'nt': os.unlink(self.db_path) def test_update_last_prices(self): # This is basic test to see if order_book last_trade_price is initiated and updated. for order_book in self.connector.order_books.values(): for _ in range(5): self.ev_loop.run_until_complete(asyncio.sleep(1)) print(order_book.last_trade_price) self.assertFalse(math.isnan(order_book.last_trade_price)) def test_filled_orders_recorded(self): config_path: str = "test_config" strategy_name: str = "test_strategy" sql = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id = None recorder = MarketsRecorder(sql, [self.connector], config_path, strategy_name) recorder.start() try: # Try to buy some token from the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("1.05") price = self.connector.quantize_order_price( self.trading_pair, price) amount = self.connector.quantize_order_amount( self.trading_pair, Decimal("0.0001")) order_id = self._place_order(True, amount, OrderType.LIMIT, price, 1, None, fixture.WS_TRADE) self.ev_loop.run_until_complete( self.event_logger.wait_for(BuyOrderCompletedEvent)) self.ev_loop.run_until_complete(asyncio.sleep(1)) # Reset the logs self.event_logger.clear() # Try to sell back the same amount to the exchange, and watch for completion event. price = self.connector.get_price(self.trading_pair, True) * Decimal("0.95") price = self.connector.quantize_order_price( self.trading_pair, price) amount = self.connector.quantize_order_amount( self.trading_pair, Decimal("0.0001")) order_id = self._place_order(False, amount, OrderType.LIMIT, price, 2, None, fixture.WS_TRADE) self.ev_loop.run_until_complete( self.event_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config( config_path) self.assertGreaterEqual(len(trade_fills), 2) buy_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "BUY" ] sell_fills: List[TradeFill] = [ t for t in trade_fills if t.trade_type == "SELL" ] self.assertGreaterEqual(len(buy_fills), 1) self.assertGreaterEqual(len(sell_fills), 1) order_id = None finally: if order_id is not None: self.connector.cancel(self.trading_pair, order_id) self.run_parallel( self.event_logger.wait_for(OrderCancelledEvent)) recorder.stop() # sql._engine.dispose() # on windows cannot unlink the sqlite db file before closing the db if os.name != 'nt': os.unlink(self.db_path)
class CoinbaseProMarketUnitTest(unittest.TestCase): events: List[MarketEvent] = [ MarketEvent.ReceivedAsset, MarketEvent.BuyOrderCompleted, MarketEvent.SellOrderCompleted, MarketEvent.WithdrawAsset, MarketEvent.OrderFilled, MarketEvent.OrderCancelled, MarketEvent.TransactionFailure, MarketEvent.BuyOrderCreated, MarketEvent.SellOrderCreated, MarketEvent.OrderCancelled ] market: CoinbaseProMarket market_logger: EventLogger stack: contextlib.ExitStack @classmethod def setUpClass(cls): cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: CoinbaseProMarket = CoinbaseProMarket( ethereum_rpc_url=conf.test_web3_provider_list[0], coinbase_pro_api_key=conf.coinbase_pro_api_key, coinbase_pro_secret_key=conf.coinbase_pro_secret_key, coinbase_pro_passphrase=conf.coinbase_pro_passphrase, symbols=["ETH-USDC", "ETH-USD"] ) cls.wallet: Web3Wallet = Web3Wallet(private_key=conf.web3_private_key_coinbase_pro, backend_urls=conf.test_web3_provider_list, erc20_token_addresses=[conf.mn_weth_token_address, conf.mn_zerox_token_address], chain=EthereumChain.MAIN_NET) print("Initializing Coinbase Pro market... this will take about a minute.") cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop() cls.clock.add_iterator(cls.market) cls.clock.add_iterator(cls.wallet) cls.stack = contextlib.ExitStack() cls._clock = cls.stack.enter_context(cls.clock) cls.ev_loop.run_until_complete(cls.wait_til_ready()) print("Ready.") @classmethod def tearDownClass(cls) -> None: cls.stack.close() @classmethod async def wait_til_ready(cls): while True: now = time.time() next_iteration = now // 1.0 + 1 if cls.market.ready: break else: await cls._clock.run_til(next_iteration) await asyncio.sleep(1.0) def setUp(self): self.db_path: str = realpath(join(__file__, "../coinbase_pro_test.sqlite")) try: os.unlink(self.db_path) except FileNotFoundError: pass self.market_logger = EventLogger() for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) def tearDown(self): for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market_logger = None async def run_parallel_async(self, *tasks): future: asyncio.Future = asyncio.ensure_future(asyncio.gather(*tasks)) while not future.done(): now = time.time() next_iteration = now // 1.0 + 1 await self.clock.run_til(next_iteration) return future.result() def run_parallel(self, *tasks): return self.ev_loop.run_until_complete(self.run_parallel_async(*tasks)) def test_get_fee(self): limit_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.LIMIT, TradeType.BUY, 1, 1) self.assertGreater(limit_fee.percent, 0) self.assertEqual(len(limit_fee.flat_fees), 0) market_fee: TradeFee = self.market.get_fee("ETH", "USDC", OrderType.MARKET, TradeType.BUY, 1) self.assertGreater(market_fee.percent, 0) self.assertEqual(len(market_fee.flat_fees), 0) def test_limit_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_limit_sell(self): symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) current_ask_price: float = self.market.get_price(symbol, False) ask_price: float = current_ask_price - 0.05 * current_ask_price quantize_ask_price: Decimal = self.market.quantize_order_price(symbol, ask_price) order_id = self.market.sell(symbol, amount, OrderType.LIMIT, quantize_ask_price) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USDC", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_buy(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) order_completed_event: BuyOrderCompletedEvent = order_completed_event trade_events: List[OrderFilledEvent] = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded: float = sum(t.amount for t in trade_events) quote_amount_traded: float = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, BuyOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() # NOTE that orders of non-USD pairs (including USDC pairs) are LIMIT only def test_market_sell(self): symbol = "ETH-USD" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.sell(symbol, amount, OrderType.MARKET, 0) [order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) order_completed_event: SellOrderCompletedEvent = order_completed_event trade_events = [t for t in self.market_logger.event_log if isinstance(t, OrderFilledEvent)] base_amount_traded = sum(t.amount for t in trade_events) quote_amount_traded = sum(t.amount * t.price for t in trade_events) self.assertTrue([evt.order_type == OrderType.LIMIT for evt in trade_events]) self.assertEqual(order_id, order_completed_event.order_id) self.assertAlmostEqual(float(quantized_amount), order_completed_event.base_asset_amount) self.assertEqual("ETH", order_completed_event.base_asset) self.assertEqual("USD", order_completed_event.quote_asset) self.assertAlmostEqual(base_amount_traded, float(order_completed_event.base_asset_amount)) self.assertAlmostEqual(quote_amount_traded, float(order_completed_event.quote_asset_amount)) self.assertTrue(any([isinstance(event, SellOrderCreatedEvent) and event.order_id == order_id for event in self.market_logger.event_log])) # Reset the logs self.market_logger.clear() def test_cancel_order(self): self.assertGreater(self.market.get_balance("ETH"), 10) symbol = "ETH-USDC" current_bid_price: float = self.market.get_price(symbol, True) amount: float = 10 / current_bid_price bid_price: float = current_bid_price - 0.1 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) client_order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.cancel(symbol, client_order_id) [order_cancelled_event] = self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_cancelled_event: OrderCancelledEvent = order_cancelled_event self.assertEqual(order_cancelled_event.order_id, client_order_id) def test_cancel_all(self): symbol = "ETH-USDC" bid_price: float = self.market.get_price(symbol, True) * 0.5 ask_price: float = self.market.get_price(symbol, False) * 2 amount: float = 10 / bid_price quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) # Intentionally setting invalid price to prevent getting filled quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price * 0.7) quantize_ask_price: Decimal = self.market.quantize_order_price(symbol, ask_price * 1.5) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.market.sell(symbol, quantized_amount, OrderType.LIMIT, quantize_ask_price) self.run_parallel(asyncio.sleep(1)) [cancellation_results] = self.run_parallel(self.market.cancel_all(5)) for cr in cancellation_results: self.assertEqual(cr.success, True) @unittest.skipUnless(any("test_list_orders" in arg for arg in sys.argv), "List order test requires manual action.") def test_list_orders(self): self.assertGreater(self.market.get_balance("ETH"), 0.1) symbol = "ETH-USDC" amount: float = 0.02 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price + 0.05 * current_bid_price quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) self.run_parallel(asyncio.sleep(1)) [order_details] = self.run_parallel(self.market.list_orders()) self.assertGreaterEqual(len(order_details), 1) self.market_logger.clear() @unittest.skipUnless(any("test_deposit_eth" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_eth(self): # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(self.wallet.get_balance("ETH"), 0.02) # Deposit ETH to Binance, and wait. tracking_id: str = self.market.deposit(self.wallet, "ETH", 0.01) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800) ) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ETH", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(self.wallet.address, received_asset_event.from_address) self.assertAlmostEqual(0.01, received_asset_event.amount_received) @unittest.skipUnless(any("test_deposit_zrx" in arg for arg in sys.argv), "Deposit test requires manual action.") def test_deposit_zrx(self): # Ensure the local wallet has enough balance for deposit testing. self.assertGreaterEqual(self.wallet.get_balance("ZRX"), 1) # Deposit ZRX to Coinbase Pro, and wait. tracking_id: str = self.market.deposit(self.wallet, "ZRX", 1) [received_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketReceivedAssetEvent, timeout_seconds=1800) ) received_asset_event: MarketReceivedAssetEvent = received_asset_event self.assertEqual("ZRX", received_asset_event.asset_name) self.assertEqual(tracking_id, received_asset_event.tx_hash) self.assertEqual(self.wallet.address, received_asset_event.from_address) self.assertEqual(1, received_asset_event.amount_received) @unittest.skipUnless(any("test_withdraw" in arg for arg in sys.argv), "Withdraw test requires manual action.") def test_withdraw(self): # Ensure the market account has enough balance for withdraw testing. self.assertGreaterEqual(self.market.get_balance("ZRX"), 1) # Withdraw ZRX from Coinbase Pro to test wallet. self.market.withdraw(self.wallet.address, "ZRX", 1) [withdraw_asset_event] = self.run_parallel( self.market_logger.wait_for(MarketWithdrawAssetEvent) ) withdraw_asset_event: MarketWithdrawAssetEvent = withdraw_asset_event self.assertEqual(self.wallet.address, withdraw_asset_event.to_address) self.assertEqual("ZRX", withdraw_asset_event.asset_name) self.assertEqual(1, withdraw_asset_event.amount) self.assertEqual(withdraw_asset_event.fee_amount, 0) def test_orders_saving_and_restoration(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: float = self.market.get_price(symbol, True) bid_price: float = current_bid_price * 0.8 quantize_bid_price: Decimal = self.market.quantize_order_price(symbol, bid_price) amount: float = 0.04 quantized_amount: Decimal = self.market.quantize_order_amount(symbol, amount) order_id = self.market.buy(symbol, quantized_amount, OrderType.LIMIT, quantize_bid_price) [order_created_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCreatedEvent)) order_created_event: BuyOrderCreatedEvent = order_created_event self.assertEqual(order_id, order_created_event.order_id) # Verify tracking states self.assertEqual(1, len(self.market.tracking_states)) self.assertEqual(order_id, list(self.market.tracking_states.keys())[0]) # Verify orders from recorder recorded_orders: List[Order] = recorder.get_orders_for_config_and_market(config_path, self.market) self.assertEqual(1, len(recorded_orders)) self.assertEqual(order_id, recorded_orders[0].id) # Verify saved market states saved_market_states: MarketState = recorder.get_market_states(config_path, self.market) self.assertIsNotNone(saved_market_states) self.assertIsInstance(saved_market_states.saved_state, dict) self.assertGreater(len(saved_market_states.saved_state), 0) # Close out the current market and start another market. self.clock.remove_iterator(self.market) for event_tag in self.events: self.market.remove_listener(event_tag, self.market_logger) self.market: CoinbaseProMarket = CoinbaseProMarket( ethereum_rpc_url=conf.test_web3_provider_list[0], coinbase_pro_api_key=conf.coinbase_pro_api_key, coinbase_pro_secret_key=conf.coinbase_pro_secret_key, coinbase_pro_passphrase=conf.coinbase_pro_passphrase, symbols=["ETH-USDC", "ETH-USD"] ) for event_tag in self.events: self.market.add_listener(event_tag, self.market_logger) recorder.stop() recorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() saved_market_states = recorder.get_market_states(config_path, self.market) self.clock.add_iterator(self.market) self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) self.market.restore_tracking_states(saved_market_states.saved_state) self.assertEqual(1, len(self.market.limit_orders)) self.assertEqual(1, len(self.market.tracking_states)) # Cancel the order and verify that the change is saved. self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) order_id = None self.assertEqual(0, len(self.market.limit_orders)) self.assertEqual(0, len(self.market.tracking_states)) saved_market_states = recorder.get_market_states(config_path, self.market) self.assertEqual(0, len(saved_market_states.saved_state)) finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path) def test_order_fill_record(self): config_path: str = "test_config" strategy_name: str = "test_strategy" symbol: str = "ETH-USDC" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: # Try to buy 0.04 ETH from the exchange, and watch for completion event. current_price: float = self.market.get_price(symbol, True) amount: float = 0.04 order_id = self.market.buy(symbol, amount) [buy_order_completed_event] = self.run_parallel(self.market_logger.wait_for(BuyOrderCompletedEvent)) # Reset the logs self.market_logger.clear() # Try to sell back the same amount of ETH to the exchange, and watch for completion event. amount = float(buy_order_completed_event.base_asset_amount) order_id = self.market.sell(symbol, amount) [sell_order_completed_event] = self.run_parallel(self.market_logger.wait_for(SellOrderCompletedEvent)) # Query the persisted trade logs trade_fills: List[TradeFill] = recorder.get_trades_for_config(config_path) self.assertEqual(2, len(trade_fills)) buy_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "BUY"] sell_fills: List[TradeFill] = [t for t in trade_fills if t.trade_type == "SELL"] self.assertEqual(1, len(buy_fills)) self.assertEqual(1, len(sell_fills)) order_id = None finally: if order_id is not None: self.market.cancel(symbol, order_id) self.run_parallel(self.market_logger.wait_for(OrderCancelledEvent)) recorder.stop() os.unlink(self.db_path)