def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task: Optional[asyncio.Task] = None self.exchange = AscendExExchange(self.api_key, self.api_secret_key, trading_pairs=[self.trading_pair]) self.mocking_assistant = NetworkMockingAssistant() self._initialize_event_loggers() self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self.exchange._in_flight_order_tracker.logger().setLevel(1) self.exchange._in_flight_order_tracker.logger().addHandler(self)
class TestAscendExExchange(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.api_key = "someKey" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.exchange = AscendExExchange(self.api_key, self.api_secret_key, trading_pairs=[self.trading_pair]) def simulate_trading_rules_initialized(self): self.exchange._trading_rules = { self.trading_pair: AscendExTradingRule( trading_pair=self.trading_pair, min_price_increment=Decimal(str(0.0001)), min_base_amount_increment=Decimal(str(0.000001)), min_notional_size=Decimal("0.001"), max_notional_size=Decimal("99999999"), commission_type=AscendExCommissionType.QUOTE, commission_reserve_rate=Decimal("0.002"), ), } def test_get_fee(self): self.simulate_trading_rules_initialized() trading_rule: AscendExTradingRule = self.exchange._trading_rules[self.trading_pair] amount = Decimal("1") price = Decimal("2") trading_rule.commission_reserve_rate = Decimal("0.002") trading_rule.commission_type = AscendExCommissionType.QUOTE buy_fee = self.exchange.get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, amount, price) sell_fee = self.exchange.get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.SELL, amount, price) self.assertEqual(Decimal("0.002"), buy_fee.percent) self.assertEqual(Decimal("0"), sell_fee.percent) trading_rule.commission_type = AscendExCommissionType.BASE buy_fee = self.exchange.get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, amount, price) sell_fee = self.exchange.get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.SELL, amount, price) self.assertEqual(Decimal("0"), buy_fee.percent) self.assertEqual(Decimal("0.002"), sell_fee.percent) trading_rule.commission_type = AscendExCommissionType.RECEIVED buy_fee = self.exchange.get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, amount, price) sell_fee = self.exchange.get_fee(self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.SELL, amount, price) self.assertEqual(Decimal("0"), buy_fee.percent) self.assertEqual(Decimal("0"), sell_fee.percent)
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.")
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 setUp(self) -> None: super().setUp() self.exchange = AscendExExchange(self.api_key, self.api_secret_key, trading_pairs=[self.trading_pair])
class TestAscendExExchange(unittest.TestCase): # logging.Level required to receive logs from the exchange level = 0 @classmethod def setUpClass(cls) -> None: super().setUpClass() cls.ev_loop = asyncio.get_event_loop() cls.base_asset = "COINALPHA" cls.quote_asset = "HBOT" cls.trading_pair = f"{cls.base_asset}-{cls.quote_asset}" cls.ex_trading_pair = f"{cls.base_asset}/{cls.quote_asset}" cls.api_key = "someKey" cls.api_secret_key = "someSecretKey" def setUp(self) -> None: super().setUp() self.log_records = [] self.async_task: Optional[asyncio.Task] = None self.exchange = AscendExExchange(self.api_key, self.api_secret_key, trading_pairs=[self.trading_pair]) self.mocking_assistant = NetworkMockingAssistant() self._initialize_event_loggers() self.exchange.logger().setLevel(1) self.exchange.logger().addHandler(self) self.exchange._in_flight_order_tracker.logger().setLevel(1) self.exchange._in_flight_order_tracker.logger().addHandler(self) def tearDown(self) -> None: self.exchange._shared_client and self.exchange._shared_client.close() self.async_task and self.async_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() self.order_failure_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), (MarketEvent.OrderFailure, self.order_failure_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: int = 1): ret = self.ev_loop.run_until_complete(asyncio.wait_for(coroutine, timeout)) return ret def simulate_trading_rules_initialized(self): self.exchange._trading_rules = { self.trading_pair: AscendExTradingRule( trading_pair=self.trading_pair, min_price_increment=Decimal(str(0.0001)), min_base_amount_increment=Decimal(str(0.000001)), min_notional_size=Decimal("0.001"), max_notional_size=Decimal("99999999"), commission_type=AscendExCommissionType.QUOTE, commission_reserve_rate=Decimal("0.002"), ), } def test_get_fee(self): self.simulate_trading_rules_initialized() trading_rule: AscendExTradingRule = self.exchange._trading_rules[self.trading_pair] amount = Decimal("1") price = Decimal("2") trading_rule.commission_reserve_rate = Decimal("0.002") trading_rule.commission_type = AscendExCommissionType.QUOTE buy_fee = self.exchange.get_fee( self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, amount, price ) sell_fee = self.exchange.get_fee( self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.SELL, amount, price ) self.assertEqual(Decimal("0.002"), buy_fee.percent) self.assertEqual(Decimal("0"), sell_fee.percent) trading_rule.commission_type = AscendExCommissionType.BASE buy_fee = self.exchange.get_fee( self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, amount, price ) sell_fee = self.exchange.get_fee( self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.SELL, amount, price ) self.assertEqual(Decimal("0"), buy_fee.percent) self.assertEqual(Decimal("0.002"), sell_fee.percent) trading_rule.commission_type = AscendExCommissionType.RECEIVED buy_fee = self.exchange.get_fee( self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.BUY, amount, price ) sell_fee = self.exchange.get_fee( self.base_asset, self.quote_asset, OrderType.LIMIT, TradeType.SELL, amount, price ) self.assertEqual(Decimal("0"), buy_fee.percent) self.assertEqual(Decimal("0"), sell_fee.percent) @aioresponses() def test_cancel_all_does_not_cancel_orders_without_exchange_id(self, mock_api): self.exchange._account_group = 0 url = f"{ascend_ex_utils.get_rest_url_private(0)}/{CONSTANTS.ORDER_BATCH_PATH_URL}" regex_url = re.compile(f"^{url}".replace(".", r"\.").replace("?", r"\?")) mock_response = { "code": 0, "data": { "ac": "CASH", "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", "action": "batch-cancel-order", "status": "Ack", "info": [ { "id": "0a8bXHbAwwoqDo3b485d7ea0b09c2cd8", "orderId": "16e61d5ff43s8bXHbAwwoqDo9d817339", "orderType": "NULL_VAL", "symbol": f"{self.base_asset}/{self.quote_asset}", "timestamp": 1573619097746 }, ] } } mock_api.delete(regex_url, body=json.dumps(mock_response)) self.exchange.start_tracking_order( order_id="testOrderId1", trading_pair=self.trading_pair, trade_type=TradeType.BUY, price=Decimal("10000"), amount=Decimal("1"), order_type=OrderType.LIMIT, exchange_order_id="16e61d5ff43s8bXHbAwwoqDo9d817339" ) self.exchange.start_tracking_order( order_id="testOrderId2", trading_pair=self.trading_pair, trade_type=TradeType.BUY, price=Decimal("20000"), amount=Decimal("2"), order_type=OrderType.LIMIT ) self.async_task = asyncio.get_event_loop().create_task(self.exchange.cancel_all(10)) result: List[CancellationResult] = self.async_run_with_timeout(self.async_task) self.assertEqual(2, len(result)) self.assertEqual("testOrderId1", result[0].order_id) self.assertTrue(result[0].success) self.assertEqual("testOrderId2", result[1].order_id) self.assertFalse(result[1].success) def test_order_without_exchange_id_marked_as_failure_and_removed_during_cancellation(self): self.exchange._set_current_timestamp(1640780000) self.exchange.start_tracking_order( order_id="testOrderId1", trading_pair=self.trading_pair, trade_type=TradeType.BUY, price=Decimal("20000"), amount=Decimal("2"), order_type=OrderType.LIMIT ) order = self.exchange.in_flight_orders["testOrderId1"] event_mock = MagicMock() event_mock.wait.side_effect = asyncio.TimeoutError() order.exchange_order_id_update_event = event_mock for i in range(self.exchange.STOP_TRACKING_ORDER_NOT_FOUND_LIMIT): self.async_run_with_timeout( self.exchange._execute_cancel(trading_pair=self.trading_pair, order_id=order.client_order_id)) self.assertEqual(0, len(self.exchange.in_flight_orders)) self.assertEqual(1, len(self.order_failure_logger.event_log)) failure_event: MarketOrderFailureEvent = self.order_failure_logger.event_log[0] self.assertEqual(order.client_order_id, failure_event.order_id) self.assertEqual(order.order_type, failure_event.order_type) self.assertEqual(1, len(self.log_records)) self.assertEqual("INFO", self.log_records[0].levelname) self.assertTrue( self.log_records[0].getMessage().startswith(f"Order {order.client_order_id} has failed. Order Update:")) def test_order_without_exchange_id_marked_as_failure_and_removed_during_status_update(self): self.exchange._set_current_timestamp(1640780000) self.exchange.start_tracking_order( order_id="testOrderId1", trading_pair=self.trading_pair, trade_type=TradeType.BUY, price=Decimal("20000"), amount=Decimal("2"), order_type=OrderType.LIMIT ) order = self.exchange.in_flight_orders["testOrderId1"] event_mock = MagicMock() event_mock.wait.side_effect = asyncio.TimeoutError() order.exchange_order_id_update_event = event_mock for i in range(self.exchange.STOP_TRACKING_ORDER_NOT_FOUND_LIMIT): self.async_run_with_timeout(self.exchange._update_order_status()) self.assertEqual(0, len(self.exchange.in_flight_orders)) self.assertEqual(1, len(self.order_failure_logger.event_log)) failure_event: MarketOrderFailureEvent = self.order_failure_logger.event_log[0] self.assertEqual(order.client_order_id, failure_event.order_id) self.assertEqual(order.order_type, failure_event.order_type) self.assertEqual(4, len(self.log_records)) self.assertEqual("INFO", self.log_records[3].levelname) self.assertTrue( self.log_records[3].getMessage().startswith(f"Order {order.client_order_id} has failed. Order Update:")) def test_restore_tracking_states_only_registers_open_orders(self): orders = [] orders.append(InFlightOrder( client_order_id="OID1", exchange_order_id="EOID1", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), creation_timestamp=1640001112.223, )) orders.append(InFlightOrder( client_order_id="OID2", exchange_order_id="EOID2", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), creation_timestamp=1640001112.223, initial_state=OrderState.CANCELLED )) orders.append(InFlightOrder( client_order_id="OID3", exchange_order_id="EOID3", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), creation_timestamp=1640001112.223, initial_state=OrderState.FILLED )) orders.append(InFlightOrder( client_order_id="OID4", exchange_order_id="EOID4", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), creation_timestamp=1640001112.223, initial_state=OrderState.FAILED )) tracking_states = {order.client_order_id: order.to_json() for order in orders} self.exchange.restore_tracking_states(tracking_states) self.assertIn("OID1", self.exchange.in_flight_orders) self.assertNotIn("OID2", self.exchange.in_flight_orders) self.assertNotIn("OID3", self.exchange.in_flight_orders) self.assertNotIn("OID4", self.exchange.in_flight_orders) def test_partial_fill_and_full_fill_generate_fill_events(self): self.exchange._set_current_timestamp(1640780000) self.exchange.start_tracking_order( order_id="OID1", trading_pair=self.trading_pair, trade_type=TradeType.BUY, price=Decimal("20000"), amount=Decimal("2"), order_type=OrderType.LIMIT, exchange_order_id="EOID1" ) order = self.exchange.in_flight_orders.get("OID1") partial_fill = { "m": "order", "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", "ac": "CASH", "data": { "s": f"{self.base_asset}/{self.quote_asset}", "sn": 8159711, "sd": "Buy", "ap": "20050", "bab": "2006.5974027", "btb": "2006.5974027", "cf": "5", "cfq": "1", "err": "", "fa": self.quote_asset, "orderId": "EOID1", "ot": "Market", "p": "20000", "q": "2", "qab": "793.23", "qtb": "860.23", "sp": "", "st": "PartiallyFilled", "t": 1576019215402, "ei": "NULL_VAL" } } total_fill = { "m": "order", "accountId": "cshQtyfq8XLAA9kcf19h8bXHbAwwoqDo", "ac": "CASH", "data": { "s": f"{self.base_asset}/{self.quote_asset}", "sn": 8159712, "sd": "Buy", "ap": "20050", "bab": "2006.5974027", "btb": "2006.5974027", "cf": "15", "cfq": "2", "err": "", "fa": self.quote_asset, "orderId": "EOID1", "ot": "Market", "p": "20000", "q": "2", "qab": "793.23", "qtb": "860.23", "sp": "", "st": "Filled", "t": 1576019215412, "ei": "NULL_VAL" } } mock_user_stream = AsyncMock() mock_user_stream.get.side_effect = [partial_fill, total_fill, 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: # Ignore cancellation errors, it is the expected signal to cut the background loop pass self.assertEqual(2, len(self.order_filled_logger.event_log)) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] self.assertEqual(self.exchange.current_timestamp, fill_event.timestamp) self.assertEqual(order.client_order_id, fill_event.order_id) self.assertEqual(order.trading_pair, fill_event.trading_pair) self.assertEqual(order.trade_type, fill_event.trade_type) self.assertEqual(order.order_type, fill_event.order_type) self.assertEqual(Decimal(partial_fill["data"]["ap"]), fill_event.price) self.assertEqual(Decimal(1), fill_event.amount) self.assertEqual(0.0, fill_event.trade_fee.percent) self.assertEqual([TokenAmount(partial_fill["data"]["fa"], Decimal("5"))], fill_event.trade_fee.flat_fees) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[1] self.assertEqual(self.exchange.current_timestamp, fill_event.timestamp) self.assertEqual(order.client_order_id, fill_event.order_id) self.assertEqual(order.trading_pair, fill_event.trading_pair) self.assertEqual(order.trade_type, fill_event.trade_type) self.assertEqual(order.order_type, fill_event.order_type) self.assertEqual(Decimal(total_fill["data"]["ap"]), fill_event.price) self.assertEqual(Decimal(1), fill_event.amount) self.assertEqual(0.0, fill_event.trade_fee.percent) self.assertEqual([TokenAmount(total_fill["data"]["fa"], Decimal("10"))], fill_event.trade_fee.flat_fees) buy_event: BuyOrderCompletedEvent = self.buy_order_completed_logger.event_log[0] self.assertEqual(self.exchange.current_timestamp, buy_event.timestamp) self.assertEqual(order.client_order_id, buy_event.order_id) self.assertEqual(order.base_asset, buy_event.base_asset) self.assertEqual(order.quote_asset, buy_event.quote_asset) self.assertEqual(order.amount, buy_event.base_asset_amount) self.assertEqual(order.amount * Decimal(total_fill["data"]["ap"]), buy_event.quote_asset_amount) self.assertEqual(order.order_type, buy_event.order_type) self.assertEqual(order.exchange_order_id, buy_event.exchange_order_id) self.assertNotIn(order.client_order_id, self.exchange.in_flight_orders) self.assertTrue( self._is_logged( "INFO", f"BUY order {order.client_order_id} completely filled." ) )