def setUp(self) -> None: super().setUp() self.log_records = [] self.test_task: Optional[asyncio.Task] = None self.exchange = HuobiExchange( huobi_api_key="testAPIKey", huobi_secret_key="testSecret", trading_pairs=[self.trading_pair], ) self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self._initialize_event_loggers()
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.BALANCES) cls._t_nonce_patcher = unittest.mock.patch( "hummingbot.connector.exchange.huobi.huobi_exchange.get_tracking_nonce" ) cls._t_nonce_mock = cls._t_nonce_patcher.start() cls.clock: Clock = Clock(ClockMode.REALTIME) cls.market: HuobiExchange = HuobiExchange(API_KEY, API_SECRET, trading_pairs=["ETH-USDT"]) # Need 2nd instance of market to prevent events mixing up across tests cls.market_2: HuobiExchange = HuobiExchange(API_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())
def customSetUp(self): self.market: HuobiExchange = HuobiExchange(MOCK_HUOBI_API_KEY, MOCK_HUOBI_SECRET_KEY, trading_pairs=["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)
class HuobiExchangeTests(TestCase): # the level is required to receive logs from the data source logger level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.symbol = f"{cls.base_asset}{cls.quote_asset}" cls.listen_key = "TEST_LISTEN_KEY" def setUp(self) -> None: super().setUp() self.log_records = [] self.test_task: Optional[asyncio.Task] = None self.exchange = HuobiExchange( huobi_api_key="testAPIKey", huobi_secret_key="testSecret", trading_pairs=[self.trading_pair], ) self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self._initialize_event_loggers() def tearDown(self) -> None: self.test_task and self.test_task.cancel() super().tearDown() def _initialize_event_loggers(self): self.buy_order_completed_logger = EventLogger() self.sell_order_completed_logger = EventLogger() self.order_filled_logger = EventLogger() events_and_loggers = [ (MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger), (MarketEvent.SellOrderCompleted, self.sell_order_completed_logger), (MarketEvent.OrderFilled, self.order_filled_logger) ] for event, logger in events_and_loggers: self.exchange.add_listener(event, logger) def handle(self, record): self.log_records.append(record) def _is_logged(self, log_level: str, message: str) -> bool: return any( record.levelname == log_level and record.getMessage() == message for record in self.log_records) def async_run_with_timeout(self, coroutine: Awaitable, timeout: float = 1): ret = asyncio.get_event_loop().run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def test_order_fill_event_takes_fee_from_update_event(self): self.exchange.start_tracking_order( order_id="OID1", exchange_order_id="99998888", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), ) order = self.exchange.in_flight_orders.get("OID1") partial_fill = { "eventType": "trade", "symbol": "choinalphahbot", "orderId": 99998888, "tradePrice": "10050.0", "tradeVolume": "0.1", "orderSide": "buy", "aggressor": True, "tradeId": 1, "tradeTime": 998787897878, "transactFee": "10.00", "feeDeduct ": "0", "feeDeductType": "", "feeCurrency": "usdt", "accountId": 9912791, "source": "spot-api", "orderPrice": "10000", "orderSize": "1", "clientOrderId": "OID1", "orderCreateTime": 998787897878, "orderStatus": "partial-filled" } message = { "ch": CONSTANTS.HUOBI_TRADE_DETAILS_TOPIC, "data": partial_fill } mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = [message, asyncio.CancelledError()] self.exchange.user_stream_tracker._user_stream = mock_user_stream self.test_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) try: self.async_run_with_timeout(self.test_task) except asyncio.CancelledError: pass self.assertEqual(Decimal("10"), order.fee_paid) self.assertEqual(1, len(self.order_filled_logger.event_log)) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] self.assertEqual(Decimal("0"), fill_event.trade_fee.percent) self.assertEqual([ TokenAmount(partial_fill["feeCurrency"].upper(), Decimal(partial_fill["transactFee"])) ], fill_event.trade_fee.flat_fees) self.assertTrue( self._is_logged( "INFO", f"Filled {Decimal(partial_fill['tradeVolume'])} out of {order.amount} of order " f"{order.order_type.name}-{order.client_order_id}")) self.assertEqual(0, len(self.buy_order_completed_logger.event_log)) complete_fill = { "eventType": "trade", "symbol": "choinalphahbot", "orderId": 99998888, "tradePrice": "10060.0", "tradeVolume": "0.9", "orderSide": "buy", "aggressor": True, "tradeId": 2, "tradeTime": 998787897878, "transactFee": "30.0", "feeDeduct ": "0", "feeDeductType": "", "feeCurrency": "usdt", "accountId": 9912791, "source": "spot-api", "orderPrice": "10000", "orderSize": "1", "clientOrderId": "OID1", "orderCreateTime": 998787897878, "orderStatus": "partial-filled" } message["data"] = complete_fill mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = [message, asyncio.CancelledError()] self.exchange.user_stream_tracker._user_stream = mock_user_stream self.test_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) try: self.async_run_with_timeout(self.test_task) except asyncio.CancelledError: pass self.assertEqual(Decimal("40"), order.fee_paid) self.assertEqual(2, len(self.order_filled_logger.event_log)) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1] self.assertEqual(Decimal("0"), fill_event.trade_fee.percent) self.assertEqual([ TokenAmount(complete_fill["feeCurrency"].upper(), Decimal(complete_fill["transactFee"])) ], fill_event.trade_fee.flat_fees) # The order should be marked as complete only when the "done" event arrives, not with the fill event self.assertFalse( self._is_logged( "INFO", f"The LIMIT_BUY order {order.client_order_id} has completed according to order delta websocket API." )) self.assertEqual(0, len(self.buy_order_completed_logger.event_log)) def test_order_fill_event_processed_before_order_complete_event(self): self.exchange.start_tracking_order( order_id="OID1", exchange_order_id="99998888", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), ) order = self.exchange.in_flight_orders.get("OID1") complete_fill = { "eventType": "trade", "symbol": "choinalphahbot", "orderId": 99998888, "tradePrice": "10060.0", "tradeVolume": "1", "orderSide": "buy", "aggressor": True, "tradeId": 1, "tradeTime": 998787897878, "transactFee": "30.0", "feeDeduct ": "0", "feeDeductType": "", "feeCurrency": "usdt", "accountId": 9912791, "source": "spot-api", "orderPrice": "10000", "orderSize": "1", "clientOrderId": "OID1", "orderCreateTime": 998787897878, "orderStatus": "partial-filled" } fill_message = { "ch": CONSTANTS.HUOBI_TRADE_DETAILS_TOPIC, "data": complete_fill } update_data = { "tradePrice": "10060.0", "tradeVolume": "1", "tradeId": 1, "tradeTime": 1583854188883, "aggressor": True, "remainAmt": "0.0", "execAmt": "1", "orderId": 99998888, "type": "buy-limit", "clientOrderId": "OID1", "orderSource": "spot-api", "orderPrice": "10000", "orderSize": "1", "orderStatus": "filled", "symbol": "btcusdt", "eventType": "trade" } update_message = { "action": "push", "ch": CONSTANTS.HUOBI_ORDER_UPDATE_TOPIC, "data": update_data, } mock_user_stream = AsyncMock() # We simulate the case when the order update arrives before the order fill mock_user_stream.get.side_effect = [ update_message, fill_message, asyncio.CancelledError() ] self.exchange.user_stream_tracker._user_stream = mock_user_stream self.test_task = asyncio.get_event_loop().create_task( self.exchange._user_stream_event_listener()) try: self.async_run_with_timeout(self.test_task) except asyncio.CancelledError: pass self.async_run_with_timeout(order.wait_until_completely_filled()) self.assertEqual(Decimal("30"), order.fee_paid) self.assertEqual(1, len(self.order_filled_logger.event_log)) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] self.assertEqual(Decimal("0"), fill_event.trade_fee.percent) self.assertEqual([ TokenAmount(complete_fill["feeCurrency"].upper(), Decimal(complete_fill["transactFee"])) ], fill_event.trade_fee.flat_fees) self.assertTrue( self._is_logged( "INFO", f"Filled {Decimal(complete_fill['tradeVolume'])} out of {order.amount} of order " f"{order.order_type.name}-{order.client_order_id}")) self.assertTrue( self._is_logged( "INFO", f"The {order.trade_type.name} order {order.client_order_id} " f"has completed according to order delta websocket API.")) self.assertEqual(1, len(self.buy_order_completed_logger.event_log)) buy_event: BuyOrderCompletedEvent = self.buy_order_completed_logger.event_log[ 0] self.assertEqual(complete_fill["feeCurrency"].upper(), buy_event.fee_asset) self.assertEqual(Decimal(complete_fill["transactFee"]), buy_event.fee_amount)
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, FixtureHuobi.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: HuobiExchange = HuobiExchange( huobi_api_key=API_KEY, huobi_secret_key=API_SECRET, 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, FixtureHuobi.CANCEL_ORDER) 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_orders_saving_and_restoration(self): self.customSetUp() config_path: str = "test_config" strategy_name: str = "test_strategy" trading_pair: str = "ethusdt" sql: SQLConnectionManager = SQLConnectionManager(SQLConnectionType.TRADE_FILLS, db_path=self.db_path) order_id: Optional[str] = None recorder: MarketsRecorder = MarketsRecorder(sql, [self.market], config_path, strategy_name) recorder.start() try: self.assertEqual(0, len(self.market.tracking_states)) # Try to put limit buy order for 0.04 ETH, and watch for order creation event. current_bid_price: Decimal = self.market.get_price(trading_pair, True) bid_price: Decimal = current_bid_price * Decimal(0.8) quantize_bid_price: Decimal = self.market.quantize_order_price(trading_pair, bid_price) amount: Decimal = Decimal(0.04) quantized_amount: Decimal = self.market.quantize_order_amount(trading_pair, amount) self.mock_api.order_id = self.mock_api.MOCK_HUOBI_LIMIT_OPEN_ORDER_ID 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) # 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: HuobiExchange = HuobiExchange( huobi_api_key=MOCK_HUOBI_API_KEY, huobi_secret_key=MOCK_HUOBI_SECRET_KEY, trading_pairs=["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(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)