def setUp(self) -> None: super().setUp() self.log_records = [] self.connector = ConnectorBase() self.connector._set_current_timestamp(1640000000.0) self.tracker = ClientOrderTracker(connector=self.connector) self.tracker.logger().setLevel(1) self.tracker.logger().addHandler(self)
def setUp(self) -> None: super().setUp() self.log_records = [] self.connector = MockExchange() self.connector._set_current_timestamp(1640000000.0) self.tracker = ClientOrderTracker(connector=self.connector) self.tracker.logger().setLevel(1) self.tracker.logger().addHandler(self) self._initialize_event_loggers()
def __init__( self, client_config_map: "ClientConfigAdapter", bitmex_api_key: str = None, bitmex_api_secret: str = None, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DOMAIN, ): self._bitmex_time_synchronizer = TimeSynchronizer() self._auth: BitmexAuth = BitmexAuth(api_key=bitmex_api_key, api_secret=bitmex_api_secret) self._trading_pairs = trading_pairs self._trading_required = trading_required self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self._domain = domain self._api_factory = web_utils.build_api_factory(auth=self._auth) self._rest_assistant: Optional[RESTAssistant] = None self._ws_assistant: Optional[WSAssistant] = None ExchangeBase.__init__(self, client_config_map=client_config_map) self._user_stream_tracker = BitmexUserStreamTracker( auth=self._auth, domain=self._domain, throttler=self._throttler, api_factory=self._api_factory, time_synchronizer=self._bitmex_time_synchronizer) self._order_book_tracker = BitmexOrderBookTracker( trading_pairs=trading_pairs, domain=self._domain, throttler=self._throttler, api_factory=self._api_factory) self._ev_loop = asyncio.get_event_loop() self._poll_notifier = asyncio.Event() self._order_not_found_records = defaultdict(int) self._last_timestamp = 0 self._trading_rules = {} self._in_flight_orders = {} self._status_polling_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._user_stream_tracker_task = None self._last_poll_timestamp = 0 self._client_order_tracker: ClientOrderTracker = ClientOrderTracker( connector=self) self._trading_pair_to_multipliers = {} self._trading_pair_price_estimate_for_quantize = {} self._token_multiplier = {}
def __init__(self, binance_api_key: str, binance_api_secret: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DEFAULT_DOMAIN, ): self._domain = domain self._binance_time_synchronizer = TimeSynchronizer() super().__init__() self._trading_required = trading_required self._auth = BinanceAuth( api_key=binance_api_key, secret_key=binance_api_secret, time_provider=self._binance_time_synchronizer) self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self._api_factory = web_utils.build_api_factory( throttler=self._throttler, time_synchronizer=self._binance_time_synchronizer, domain=self._domain, auth=self._auth) self._rest_assistant = None self._order_book_tracker = OrderBookTracker( data_source=BinanceAPIOrderBookDataSource( trading_pairs=trading_pairs, domain=self._domain, api_factory=self._api_factory, throttler=self._throttler), trading_pairs=trading_pairs, domain=self._domain) self._user_stream_tracker = UserStreamTracker( data_source=BinanceAPIUserStreamDataSource( auth=self._auth, domain=self._domain, throttler=self._throttler, api_factory=self._api_factory, time_synchronizer=self._binance_time_synchronizer)) self._ev_loop = asyncio.get_event_loop() self._poll_notifier = asyncio.Event() self._last_timestamp = 0 self._order_not_found_records = {} # Dict[client_order_id:str, count:int] self._trading_rules = {} # Dict[trading_pair:str, TradingRule] self._trade_fees = {} # Dict[trading_pair:str, (maker_fee_percent:Decimal, taken_fee_percent:Decimal)] self._last_update_trade_fees_timestamp = 0 self._status_polling_task = None self._user_stream_tracker_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._last_poll_timestamp = 0 self._last_trades_poll_binance_timestamp = 0 self._order_tracker: ClientOrderTracker = ClientOrderTracker(connector=self)
def __init__(self, kucoin_api_key: str, kucoin_passphrase: str, kucoin_secret_key: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DEFAULT_DOMAIN): self._domain = domain self._time_synchronizer = TimeSynchronizer() super().__init__() self._auth = KucoinAuth( api_key=kucoin_api_key, passphrase=kucoin_passphrase, secret_key=kucoin_secret_key, time_provider=self._time_synchronizer) self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self._api_factory = web_utils.build_api_factory( throttler=self._throttler, time_synchronizer=self._time_synchronizer, domain=self._domain, auth=self._auth) self._last_poll_timestamp = 0 self._last_timestamp = 0 self._trading_pairs = trading_pairs self._order_book_tracker = OrderBookTracker( data_source=KucoinAPIOrderBookDataSource( trading_pairs=trading_pairs, domain=self._domain, api_factory=self._api_factory, throttler=self._throttler, time_synchronizer=self._time_synchronizer), trading_pairs=trading_pairs, domain=self._domain) self._user_stream_tracker = UserStreamTracker( data_source=KucoinAPIUserStreamDataSource( domain=self._domain, api_factory=self._api_factory, throttler=self._throttler)) self._poll_notifier = asyncio.Event() self._status_polling_task = None self._user_stream_tracker_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._trading_fees_polling_task = None self._trading_required = trading_required self._trading_rules = {} self._trading_fees = {} self._order_tracker: ClientOrderTracker = ClientOrderTracker(connector=self)
def __init__( self, client_config_map: "ClientConfigAdapter", bybit_api_key: str, bybit_api_secret: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DEFAULT_DOMAIN, ): self._domain = domain self._time_synchronizer = TimeSynchronizer() super().__init__(client_config_map) self._trading_required = trading_required self._auth = BybitAuth(api_key=bybit_api_key, secret_key=bybit_api_secret, time_provider=self._time_synchronizer) self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self._api_factory = web_utils.build_api_factory( throttler=self._throttler, time_synchronizer=self._time_synchronizer, domain=self._domain, auth=self._auth) self._rest_assistant = None self._order_book_tracker = OrderBookTracker( data_source=BybitAPIOrderBookDataSource( trading_pairs=trading_pairs, domain=self._domain, api_factory=self._api_factory, throttler=self._throttler), trading_pairs=trading_pairs, domain=self._domain) self._user_stream_tracker = UserStreamTracker( data_source=BybitAPIUserStreamDataSource( auth=self._auth, domain=self._domain, throttler=self._throttler, api_factory=self._api_factory, time_synchronizer=self._time_synchronizer)) self._poll_notifier = asyncio.Event() self._last_timestamp = 0 self._trading_rules = {} self._trade_fees = {} self._status_polling_task = None self._user_stream_tracker_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._last_poll_timestamp = 0 self._order_tracker: ClientOrderTracker = ClientOrderTracker( connector=self)
def test_cached_order_ttl_exceeded(self): tracker = ClientOrderTracker(self.connector) order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), ) tracker._cached_orders[order.client_order_id] = order self.ev_loop.run_until_complete(asyncio.sleep(0.2)) self.assertNotIn(order.client_order_id, tracker.cached_orders)
def __init__( self, client_config_map: "ClientConfigAdapter", connector_name: str, chain: str, network: str, wallet_address: str, trading_pairs: List[str] = [], additional_spenders: List[str] = [], # not implemented trading_required: bool = True): """ :param connector_name: name of connector on gateway :param chain: refers to a block chain, e.g. ethereum or avalanche :param network: refers to a network of a particular blockchain e.g. mainnet or kovan :param wallet_address: the address of the eth wallet which has been added on gateway :param trading_pairs: a list of trading pairs :param trading_required: Whether actual trading is needed. Useful for some functionalities or commands like the balance command """ self._connector_name = connector_name self._name = "_".join([connector_name, chain, network]) super().__init__(client_config_map) self._chain = chain self._network = network self._trading_pairs = trading_pairs self._tokens = set() [ self._tokens.update(set(trading_pair.split("-"))) for trading_pair in trading_pairs ] self._wallet_address = wallet_address self._trading_required = trading_required self._ev_loop = asyncio.get_event_loop() self._last_poll_timestamp = 0.0 self._last_balance_poll_timestamp = time.time() self._last_est_gas_cost_reported = 0 self._allowances = {} self._chain_info = {} self._status_polling_task = None self._get_chain_info_task = None self._auto_approve_task = None self._get_gas_estimate_task = None self._poll_notifier = None self._native_currency = None self._network_transaction_fee: Optional[TokenAmount] = None self._order_tracker: ClientOrderTracker = ClientOrderTracker( connector=self)
def __init__( self, ascend_ex_api_key: str, ascend_ex_secret_key: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, ): """ :param ascend_ex_api_key: The API key to connect to private AscendEx APIs. :param ascend_ex_secret_key: The API secret. :param trading_pairs: The market trading pairs which to track order book data. :param trading_required: Whether actual trading is needed. """ super().__init__() self._trading_required = trading_required self._trading_pairs = trading_pairs self._shared_client = aiohttp.ClientSession() self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self._order_book_tracker = AscendExOrderBookTracker( shared_client=self._shared_client, throttler=self._throttler, trading_pairs=self._trading_pairs ) self._ascend_ex_auth = AscendExAuth(ascend_ex_api_key, ascend_ex_secret_key) self._user_stream_tracker = AscendExUserStreamTracker( shared_client=self._shared_client, throttler=self._throttler, ascend_ex_auth=self._ascend_ex_auth, trading_pairs=self._trading_pairs, ) self._poll_notifier = asyncio.Event() self._last_timestamp = 0 self._trading_rules = {} # Dict[trading_pair:str, AscendExTradingRule] self._status_polling_task = None self._user_stream_tracker_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._last_poll_timestamp = 0 self._account_group = None # required in order to make post requests self._account_uid = None # required in order to produce deterministic order ids self._throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS) self._in_flight_order_tracker: ClientOrderTracker = ClientOrderTracker(connector=self) self._order_without_exchange_id_records = {}
def __init__(self, coinflex_api_key: str, coinflex_api_secret: str, trading_pairs: Optional[List[str]] = None, trading_required: bool = True, domain: str = CONSTANTS.DEFAULT_DOMAIN): self._domain = domain super().__init__() self._trading_required = trading_required self._auth = CoinflexAuth(api_key=coinflex_api_key, secret_key=coinflex_api_secret) self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS) self._api_factory = web_utils.build_api_factory(auth=self._auth) self._order_book_tracker = CoinflexOrderBookTracker( trading_pairs=trading_pairs, domain=domain, api_factory=self._api_factory, throttler=self._throttler) self._user_stream_tracker = CoinflexUserStreamTracker( auth=self._auth, domain=domain, throttler=self._throttler, api_factory=self._api_factory) self._ev_loop = asyncio.get_event_loop() self._poll_notifier = asyncio.Event() self._last_timestamp = 0 self._order_not_found_records = { } # Dict[client_order_id:str, count:int] self._trading_rules = {} # Dict[trading_pair:str, TradingRule] self._trade_fees = { } # Dict[trading_pair:str, (maker_fee_percent:Decimal, taken_fee_percent:Decimal)] self._last_update_trade_fees_timestamp = 0 self._status_polling_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._last_poll_timestamp = 0 self._last_trades_poll_coinflex_timestamp = 0 self._order_tracker: ClientOrderTracker = ClientOrderTracker( connector=self)
def __init__(self, client_config_map: "ClientConfigAdapter"): super().__init__(client_config_map) self._last_poll_timestamp = 0 self._last_timestamp = 0 self._trading_rules = {} self._trading_fees = {} self._status_polling_task = None self._user_stream_tracker_task = None self._user_stream_event_listener_task = None self._trading_rules_polling_task = None self._trading_fees_polling_task = None self._time_synchronizer = TimeSynchronizer() self._throttler = AsyncThrottler(self.rate_limits_rules) self._poll_notifier = asyncio.Event() # init Auth and Api factory self._auth: AuthBase = self.authenticator self._web_assistants_factory: WebAssistantsFactory = self._create_web_assistants_factory( ) # init OrderBook Data Source and Tracker self._orderbook_ds: OrderBookTrackerDataSource = self._create_order_book_data_source( ) self._set_order_book_tracker( OrderBookTracker(data_source=self._orderbook_ds, trading_pairs=self.trading_pairs, domain=self.domain)) # init UserStream Data Source and Tracker self._userstream_ds = self._create_user_stream_data_source() self._user_stream_tracker = UserStreamTracker( data_source=self._userstream_ds) self._order_tracker: ClientOrderTracker = ClientOrderTracker( connector=self)
class ClientOrderTrackerUnitTest(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.trade_fee_percent = Decimal("0.001") def setUp(self) -> None: super().setUp() self.log_records = [] self.connector = MockExchange( client_config_map=ClientConfigAdapter(ClientConfigMap())) self.connector._set_current_timestamp(1640000000.0) self.tracker = ClientOrderTracker(connector=self.connector) self.tracker.logger().setLevel(1) self.tracker.logger().addHandler(self) self._initialize_event_loggers() def _initialize_event_loggers(self): self.buy_order_completed_logger = EventLogger() self.buy_order_created_logger = EventLogger() self.order_cancelled_logger = EventLogger() self.order_failure_logger = EventLogger() self.order_filled_logger = EventLogger() self.sell_order_completed_logger = EventLogger() self.sell_order_created_logger = EventLogger() events_and_loggers = [ (MarketEvent.BuyOrderCompleted, self.buy_order_completed_logger), (MarketEvent.BuyOrderCreated, self.buy_order_created_logger), (MarketEvent.OrderCancelled, self.order_cancelled_logger), (MarketEvent.OrderFailure, self.order_failure_logger), (MarketEvent.OrderFilled, self.order_filled_logger), (MarketEvent.SellOrderCompleted, self.sell_order_completed_logger), (MarketEvent.SellOrderCreated, self.sell_order_created_logger) ] for event, logger in events_and_loggers: self.connector.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 = self.ev_loop.run_until_complete( asyncio.wait_for(coroutine, timeout)) return ret def test_start_tracking_order(self): self.assertEqual(0, len(self.tracker.active_orders)) order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) self.assertEqual(1, len(self.tracker.active_orders)) def test_stop_tracking_order(self): self.assertEqual(0, len(self.tracker.active_orders)) order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) self.assertEqual(1, len(self.tracker.active_orders)) self.tracker.stop_tracking_order(order.client_order_id) self.assertEqual(0, len(self.tracker.active_orders)) self.assertEqual(1, len(self.tracker.cached_orders)) def test_cached_order_max_cache_size(self): for i in range(ClientOrderTracker.MAX_CACHE_SIZE + 1): order: InFlightOrder = InFlightOrder( client_order_id=f"someClientOrderId_{i}", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker._cached_orders[order.client_order_id] = order self.assertEqual(ClientOrderTracker.MAX_CACHE_SIZE, len(self.tracker.cached_orders)) # First entry gets removed when the no. of cached order exceeds MAX_CACHE_SIZE self.assertNotIn("someClientOrderId_0", self.tracker._cached_orders) def test_cached_order_ttl_not_exceeded(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker._cached_orders[order.client_order_id] = order self.assertIn(order.client_order_id, self.tracker._cached_orders) @patch( "hummingbot.connector.client_order_tracker.ClientOrderTracker.CACHED_ORDER_TTL", 0.1) def test_cached_order_ttl_exceeded(self): tracker = ClientOrderTracker(self.connector) order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) tracker._cached_orders[order.client_order_id] = order self.ev_loop.run_until_complete(asyncio.sleep(0.2)) self.assertNotIn(order.client_order_id, tracker.cached_orders) def test_fetch_tracked_order_not_found(self): self.assertIsNone( self.tracker.fetch_tracked_order("someNonExistantOrderId")) def test_fetch_tracked_order(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) self.assertEqual(1, len(self.tracker.active_orders)) fetched_order: InFlightOrder = self.tracker.fetch_tracked_order( order.client_order_id) self.assertTrue(fetched_order == order) def test_fetch_cached_order_not_found(self): self.assertIsNone( self.tracker.fetch_cached_order("someNonExistantOrderId")) def test_fetch_cached_order(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker._cached_orders[order.client_order_id] = order self.assertEqual(1, len(self.tracker.cached_orders)) fetched_order: InFlightOrder = self.tracker.fetch_cached_order( order.client_order_id) self.assertTrue(fetched_order == order) def test_fetch_order_by_client_order_id(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) self.assertEqual(1, len(self.tracker.active_orders)) fetched_order: InFlightOrder = self.tracker.fetch_order( order.client_order_id) self.assertTrue(fetched_order == order) def test_fetch_order_by_exchange_order_id(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) self.assertEqual(1, len(self.tracker.active_orders)) fetched_order: InFlightOrder = self.tracker.fetch_order( exchange_order_id=order.exchange_order_id) self.assertTrue(fetched_order == order) def test_fetch_order_does_not_match_orders_with_undefined_exchange_id( self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) self.assertEqual(1, len(self.tracker.active_orders)) fetched_order = self.tracker.fetch_order("invalid_order_id") self.assertIsNone(fetched_order) def test_process_order_update_invalid_order_update(self): order_creation_update: OrderUpdate = OrderUpdate( # client_order_id="someClientOrderId", # client_order_id intentionally omitted # exchange_order_id="someExchangeOrderId", # client_order_id intentionally omitted trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) update_future = self.tracker.process_order_update( order_creation_update) self.async_run_with_timeout(update_future) self.assertTrue( self._is_logged( "ERROR", "OrderUpdate does not contain any client_order_id or exchange_order_id", )) def test_process_order_update_order_not_found(self): order_creation_update: OrderUpdate = OrderUpdate( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) update_future = self.tracker.process_order_update( order_creation_update) self.async_run_with_timeout(update_future) self.assertTrue( self._is_logged( "DEBUG", f"Order is not/no longer being tracked ({order_creation_update})", )) def test_process_order_update_trigger_order_creation_event(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) order_creation_update: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) update_future = self.tracker.process_order_update( order_creation_update) self.async_run_with_timeout(update_future) updated_order: InFlightOrder = self.tracker.fetch_tracked_order( order.client_order_id) # Check order update has been successfully applied self.assertEqual(updated_order.exchange_order_id, order_creation_update.exchange_order_id) self.assertTrue(updated_order.exchange_order_id_update_event.is_set()) self.assertEqual(updated_order.current_state, order_creation_update.new_state) self.assertTrue(updated_order.is_open) # Check that Logger has logged the correct log self.assertTrue( self._is_logged( "INFO", f"Created {order.order_type.name} {order.trade_type.name} order {order.client_order_id} for " f"{order.amount} {order.trading_pair}.", )) # Check that Buy/SellOrderCreatedEvent has been triggered. self.assertEqual(1, len(self.buy_order_created_logger.event_log)) event_logged = self.buy_order_created_logger.event_log[0] self.assertEqual(event_logged.amount, order.amount) self.assertEqual(event_logged.exchange_order_id, order_creation_update.exchange_order_id) self.assertEqual(event_logged.order_id, order.client_order_id) self.assertEqual(event_logged.price, order.price) self.assertEqual(event_logged.trading_pair, order.trading_pair) self.assertEqual(event_logged.type, order.order_type) def test_process_order_update_trigger_order_creation_event_without_client_order_id( self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id= "someExchangeOrderId", # exchange_order_id is provided when initialized. See AscendEx. trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) order_creation_update: OrderUpdate = OrderUpdate( # client_order_id=order.client_order_id, # client_order_id purposefully ommited exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) update_future = self.tracker.process_order_update( order_creation_update) self.async_run_with_timeout(update_future) updated_order: InFlightOrder = self.tracker.fetch_tracked_order( order.client_order_id) # Check order update has been successfully applied self.assertEqual(updated_order.exchange_order_id, order_creation_update.exchange_order_id) self.assertTrue(updated_order.exchange_order_id_update_event.is_set()) self.assertEqual(updated_order.current_state, order_creation_update.new_state) self.assertTrue(updated_order.is_open) # Check that Logger has logged the correct log self.assertTrue( self._is_logged( "INFO", f"Created {order.order_type.name} {order.trade_type.name} order {order.client_order_id} for " f"{order.amount} {order.trading_pair}.", )) # Check that Buy/SellOrderCreatedEvent has been triggered. self.assertEqual(1, len(self.buy_order_created_logger.event_log)) event_logged = self.buy_order_created_logger.event_log[0] self.assertEqual(event_logged.amount, order.amount) self.assertEqual(event_logged.exchange_order_id, order_creation_update.exchange_order_id) self.assertEqual(event_logged.order_id, order.client_order_id) self.assertEqual(event_logged.price, order.price) self.assertEqual(event_logged.trading_pair, order.trading_pair) self.assertEqual(event_logged.type, order.order_type) def test_process_order_update_trigger_order_cancelled_event(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) order_cancelled_update: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.CANCELED, ) update_future = self.tracker.process_order_update( order_cancelled_update) self.async_run_with_timeout(update_future) self.assertTrue( self._is_logged( "INFO", f"Successfully canceled order {order.client_order_id}.")) self.assertEqual(0, len(self.tracker.active_orders)) self.assertEqual(1, len(self.tracker.cached_orders)) self.assertEqual(1, len(self.order_cancelled_logger.event_log)) event_triggered = self.order_cancelled_logger.event_log[0] self.assertIsInstance(event_triggered, OrderCancelledEvent) self.assertEqual(event_triggered.exchange_order_id, order.exchange_order_id) self.assertEqual(event_triggered.order_id, order.client_order_id) def test_process_order_update_trigger_order_failure_event(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) order_failure_update: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.FAILED, ) update_future = self.tracker.process_order_update(order_failure_update) self.async_run_with_timeout(update_future) self.assertTrue( self._is_logged( "INFO", f"Order {order.client_order_id} has failed. Order Update: {order_failure_update}" )) self.assertEqual(0, len(self.tracker.active_orders)) self.assertEqual(1, len(self.tracker.cached_orders)) self.assertEqual(1, len(self.order_failure_logger.event_log)) event_triggered = self.order_failure_logger.event_log[0] self.assertIsInstance(event_triggered, MarketOrderFailureEvent) self.assertEqual(event_triggered.order_id, order.client_order_id) self.assertEqual(event_triggered.order_type, order.order_type) def test_process_order_update_trigger_completed_event_and_not_fill_event( self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) initial_order_filled_amount = order.amount / Decimal("2.0") order_update_1: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.PARTIALLY_FILLED, ) update_future = self.tracker.process_order_update(order_update_1) self.async_run_with_timeout(update_future) # Check order update has been successfully applied updated_order: InFlightOrder = self.tracker.fetch_tracked_order( order.client_order_id) self.assertEqual(updated_order.exchange_order_id, order_update_1.exchange_order_id) self.assertTrue(updated_order.exchange_order_id_update_event.is_set()) self.assertEqual(updated_order.current_state, order_update_1.new_state) self.assertTrue(updated_order.is_open) subsequent_order_filled_amount = order.amount - initial_order_filled_amount order_update_2: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=2, new_state=OrderState.FILLED, ) # Force order to not wait for filled events from TradeUpdate objects order.completely_filled_event.set() update_future = self.tracker.process_order_update(order_update_2) self.async_run_with_timeout(update_future) # Check order is not longer being actively tracked self.assertIsNone( self.tracker.fetch_tracked_order(order.client_order_id)) cached_order: InFlightOrder = self.tracker.fetch_cached_order( order.client_order_id) self.assertEqual(cached_order.current_state, order_update_2.new_state) self.assertTrue(cached_order.is_done) # Check that Logger has logged the appropriate logs self.assertFalse( self._is_logged( "INFO", f"The {order.trade_type.name.upper()} order {order.client_order_id} amounting to " f"{initial_order_filled_amount}/{order.amount} {order.base_asset} has been filled.", )) self.assertFalse( self._is_logged( "INFO", f"The {order.trade_type.name.upper()} order {order.client_order_id} amounting to " f"{initial_order_filled_amount + subsequent_order_filled_amount}/{order.amount} {order.base_asset} " f"has been filled.", )) self.assertTrue( self._is_logged( "INFO", f"{order.trade_type.name.upper()} order {order.client_order_id} completely filled." )) self.assertEqual(0, len(self.order_filled_logger.event_log)) self.assertEqual(1, len(self.buy_order_completed_logger.event_log)) def test_process_trade_update_trigger_filled_event_flat_fee(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) trade_filled_price: Decimal = Decimal("0.5") trade_filled_amount: Decimal = order.amount / Decimal("2.0") fee_paid: Decimal = self.trade_fee_percent * trade_filled_amount trade_update: TradeUpdate = TradeUpdate( trade_id=1, client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=order.trading_pair, fill_price=trade_filled_price, fill_base_amount=trade_filled_amount, fill_quote_amount=trade_filled_price * trade_filled_amount, fee=AddedToCostTradeFee(flat_fees=[ TokenAmount(token=self.quote_asset, amount=fee_paid) ]), fill_timestamp=1, ) self.tracker.process_trade_update(trade_update) self.assertTrue( self._is_logged( "INFO", f"The {order.trade_type.name.upper()} order {order.client_order_id} amounting to " f"{trade_filled_amount}/{order.amount} {order.base_asset} has been filled.", )) self.assertEqual(1, len(self.order_filled_logger.event_log)) order_filled_event: OrderFilledEvent = self.order_filled_logger.event_log[ 0] self.assertEqual(order_filled_event.order_id, order.client_order_id) self.assertEqual(order_filled_event.price, trade_update.fill_price) self.assertEqual(order_filled_event.amount, trade_update.fill_base_amount) self.assertEqual( order_filled_event.trade_fee, AddedToCostTradeFee( flat_fees=[TokenAmount(self.quote_asset, fee_paid)])) def test_process_trade_update_does_not_trigger_filled_event_update_status_when_completely_filled( self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) fee_paid: Decimal = self.trade_fee_percent * order.amount trade_update: TradeUpdate = TradeUpdate( trade_id=1, client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=order.trading_pair, fill_price=order.price, fill_base_amount=order.amount, fill_quote_amount=order.price * order.amount, fee=AddedToCostTradeFee(flat_fees=[ TokenAmount(token=self.quote_asset, amount=fee_paid) ]), fill_timestamp=1, ) self.tracker.process_trade_update(trade_update) fetched_order: InFlightOrder = self.tracker.fetch_order( order.client_order_id) self.assertTrue(fetched_order.is_filled) self.assertIn(fetched_order.client_order_id, self.tracker.active_orders) self.assertNotIn(fetched_order.client_order_id, self.tracker.cached_orders) self.assertTrue( self._is_logged( "INFO", f"The {order.trade_type.name.upper()} order {order.client_order_id} amounting to " f"{order.amount}/{order.amount} {order.base_asset} has been filled.", )) self.assertEqual(1, len(self.order_filled_logger.event_log)) self.assertEqual(0, len(self.buy_order_completed_logger.event_log)) order_filled_event: OrderFilledEvent = self.order_filled_logger.event_log[ 0] self.assertIsNotNone(order_filled_event) self.assertEqual(order_filled_event.order_id, order.client_order_id) self.assertEqual(order_filled_event.price, trade_update.fill_price) self.assertEqual(order_filled_event.amount, trade_update.fill_base_amount) self.assertEqual( order_filled_event.trade_fee, AddedToCostTradeFee( flat_fees=[TokenAmount(self.quote_asset, fee_paid)])) def test_updating_order_states_with_both_process_order_update_and_process_trade_update( self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), ) self.tracker.start_tracking_order(order) order_creation_update: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) update_future = self.tracker.process_order_update( order_creation_update) self.async_run_with_timeout(update_future) open_order: InFlightOrder = self.tracker.fetch_tracked_order( order.client_order_id) # Check order_creation_update has been successfully applied self.assertEqual(open_order.exchange_order_id, order_creation_update.exchange_order_id) self.assertTrue(open_order.exchange_order_id_update_event.is_set()) self.assertEqual(open_order.current_state, order_creation_update.new_state) self.assertTrue(open_order.is_open) self.assertEqual(0, len(open_order.order_fills)) trade_filled_price: Decimal = order.price trade_filled_amount: Decimal = order.amount fee_paid: Decimal = self.trade_fee_percent * trade_filled_amount trade_update: TradeUpdate = TradeUpdate( trade_id=1, client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=order.trading_pair, fill_price=trade_filled_price, fill_base_amount=trade_filled_amount, fill_quote_amount=trade_filled_price * trade_filled_amount, fee=AddedToCostTradeFee(flat_fees=[ TokenAmount(token=self.quote_asset, amount=fee_paid) ]), fill_timestamp=2, ) self.tracker.process_trade_update(trade_update) self.assertEqual(1, len(self.tracker.active_orders)) self.assertEqual(0, len(self.tracker.cached_orders)) def test_process_order_not_found_invalid_order(self): self.assertEqual(0, len(self.tracker.active_orders)) unknown_order_id = "UNKNOWN_ORDER_ID" self.async_run_with_timeout( self.tracker.process_order_not_found(unknown_order_id)) self._is_logged( "DEBUG", f"Order is not/no longer being tracked ({unknown_order_id})") def test_process_order_not_found_does_not_exceed_limit(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) self.async_run_with_timeout( self.tracker.process_order_not_found(order.client_order_id)) self.assertIn(order.client_order_id, self.tracker.active_orders) self.assertIn(order.client_order_id, self.tracker._order_not_found_records) self.assertEqual( 1, self.tracker._order_not_found_records[order.client_order_id]) def test_process_order_not_found_exceeded_limit(self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), creation_timestamp=1640001112.0, price=Decimal("1.0"), initial_state=OrderState.OPEN, ) self.tracker.start_tracking_order(order) self.tracker._order_not_found_records[order.client_order_id] = 10 self.async_run_with_timeout( self.tracker.process_order_not_found(order.client_order_id)) self.assertNotIn(order.client_order_id, self.tracker.active_orders) 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"), creation_timestamp=1640001112.223, price=Decimal("1.0"), )) 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"), creation_timestamp=1640001112.223, price=Decimal("1.0"), initial_state=OrderState.CANCELED)) 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.tracker.restore_tracking_states(tracking_states) self.assertIn("OID1", self.tracker.active_orders) self.assertNotIn("OID2", self.tracker.all_orders) self.assertNotIn("OID3", self.tracker.all_orders) self.assertNotIn("OID4", self.tracker.all_orders) def test_update_to_close_order_is_not_processed_until_order_completelly_filled( self): order: InFlightOrder = InFlightOrder( client_order_id="someClientOrderId", 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, ) self.tracker.start_tracking_order(order) order_creation_update: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) trade_update: TradeUpdate = TradeUpdate( trade_id="1", client_order_id=order.client_order_id, exchange_order_id=order.exchange_order_id, trading_pair=order.trading_pair, fill_price=Decimal("1100"), fill_base_amount=order.amount, fill_quote_amount=order.amount * Decimal("1100"), fee=AddedToCostTradeFee(flat_fees=[ TokenAmount(token=self.quote_asset, amount=Decimal("10")) ]), fill_timestamp=10, ) order_completion_update: OrderUpdate = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id="someExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=2, new_state=OrderState.FILLED, ) # We invert the orders update processing on purpose, to force the test scenario without using sleeps self.connector._set_current_timestamp(1640001100) completion_update_future = self.tracker.process_order_update( order_completion_update) self.connector._set_current_timestamp(1640001105) creation_update_future = self.tracker.process_order_update( order_creation_update) self.async_run_with_timeout(creation_update_future) order: InFlightOrder = self.tracker.fetch_order( client_order_id=order.client_order_id) # Check order_creation_update has been successfully applied self.assertFalse(order.is_done) self.assertFalse(order.is_filled) self.assertFalse(order.completely_filled_event.is_set()) fill_timetamp = 1640001115 self.connector._set_current_timestamp(fill_timetamp) self.tracker.process_trade_update(trade_update) self.assertTrue(order.completely_filled_event.is_set()) self.connector._set_current_timestamp(1640001120) self.async_run_with_timeout(completion_update_future) self.assertTrue(order.is_filled) fill_event: OrderFilledEvent = self.order_filled_logger.event_log[0] self.assertEqual(fill_timetamp, fill_event.timestamp) complete_event: BuyOrderCompletedEvent = self.buy_order_completed_logger.event_log[ 0] self.assertGreaterEqual(complete_event.timestamp, 1640001120)