async def update_canceling_transactions( self, canceled_tracked_orders: List[GatewayInFlightOrder]): """ Update tracked orders that have a cancel_tx_hash. :param canceled_tracked_orders: Canceled tracked_orders (cancel_tx_has is not None). """ if len(canceled_tracked_orders) < 1: return self.logger().debug( "Polling for order status updates of %d canceled orders.", len(canceled_tracked_orders)) update_results: List[Union[Dict[ str, Any], Exception]] = await safe_gather(*[ self._get_gateway_instance().get_transaction_status( self.chain, self.network, tx_hash) for tx_hash in [t.cancel_tx_hash for t in canceled_tracked_orders] ], return_exceptions=True) for tracked_order, update_result in zip(canceled_tracked_orders, update_results): if isinstance(update_result, Exception): raise update_result if "txHash" not in update_result: self.logger().error( f"No txHash field for transaction status of {tracked_order.client_order_id}: " f"{update_result}.") continue if update_result["txStatus"] == 1: if update_result["txReceipt"]["status"] == 1: if tracked_order.current_state == OrderState.PENDING_CANCEL: if not tracked_order.is_approval_request: order_update: OrderUpdate = OrderUpdate( trading_pair=tracked_order.trading_pair, client_order_id=tracked_order.client_order_id, update_timestamp=self.current_timestamp, new_state=OrderState.CANCELED) self._order_tracker.process_order_update( order_update) elif tracked_order.is_approval_request: order_update: OrderUpdate = OrderUpdate( trading_pair=tracked_order.trading_pair, client_order_id=tracked_order.client_order_id, update_timestamp=self.current_timestamp, new_state=OrderState.CANCELED) token_symbol: str = self.get_token_symbol_from_approval_order_id( tracked_order.client_order_id) self.trigger_event( TokenApprovalEvent.ApprovalCancelled, TokenApprovalCancelledEvent( self.current_timestamp, self.connector_name, token_symbol)) self.logger().info( f"Token approval for {tracked_order.client_order_id} on " f"{self.connector_name} has been canceled.") self.stop_tracking_order( tracked_order.client_order_id)
def test_order_life_cycle_of_token_approval_requests(self): order: GatewayInFlightOrder = GatewayInFlightOrder( client_order_id=self.client_order_id, trading_pair=self.quote_asset, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, creation_timestamp=1652324823, initial_state=OrderState.PENDING_APPROVAL, ) # Assert that order is in fact a Approval Request self.assertTrue(order.is_approval_request) self.assertTrue(order.is_pending_approval) order_update: OrderUpdate = OrderUpdate( trading_pair=order.trading_pair, update_timestamp=1652324824, new_state=OrderState.APPROVED, client_order_id=order.client_order_id, exchange_order_id=self.exchange_order_id, ) order.update_with_order_update(order_update=order_update) self.assertFalse(order.is_pending_approval)
async def _execute_cancel(self, client_order_id: str) -> Dict[str, Any]: """ Requests the exchange to cancel an active order :param client_order_id: the client id of the order to cancel """ tracked_order = self._order_tracker.fetch_tracked_order( client_order_id) if tracked_order is not None: try: api_params = { "orderLinkId": client_order_id, } cancel_result = await self._api_request( method=RESTMethod.DELETE, path_url=CONSTANTS.ORDER_PATH_URL, params=api_params, is_auth_required=True) order_update: OrderUpdate = OrderUpdate( client_order_id=client_order_id, trading_pair=tracked_order.trading_pair, update_timestamp=self.current_timestamp, new_state=OrderState.CANCELED, ) self._order_tracker.process_order_update(order_update) return cancel_result except asyncio.CancelledError: raise except Exception: self.logger().network( f"There was a an error when requesting cancellation of order {client_order_id}" ) raise
def _process_order_message(self, order_msg: Dict[str, Any]): """ Updates in-flight order and triggers cancelation or failure event if needed. :param order_msg: The order response from either REST or web socket API (they are of the same format) Example Order: https://www.gate.io/docs/apiv4/en/#list-orders """ state = None client_order_id = str(order_msg.get("text", "")) tracked_order = self.in_flight_orders.get(client_order_id, None) if not tracked_order: self.logger().debug( f"Ignoring order message with id {client_order_id}: not in in_flight_orders." ) return state = self._normalise_order_message_state(order_msg, tracked_order) if state: order_update = OrderUpdate( trading_pair=tracked_order.trading_pair, update_timestamp=int(order_msg["update_time"]), new_state=state, client_order_id=client_order_id, exchange_order_id=str(order_msg["id"]), ) self._order_tracker.process_order_update(order_update=order_update) self.logger().info( f"Successfully updated order {tracked_order.client_order_id}.")
def test_update_with_order_update_client_order_id_mismatch(self): order: InFlightOrder = InFlightOrder( client_order_id=self.client_order_id, trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), ) mismatch_order_update: OrderUpdate = OrderUpdate( client_order_id="mismatchClientOrderId", exchange_order_id="mismatchExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, fill_price=Decimal("1.0"), executed_amount_base=Decimal("1000.0"), executed_amount_quote=Decimal("1000.0"), fee_asset=self.base_asset, cumulative_fee_paid=self.trade_fee_percent * Decimal("1000.0"), ) self.assertFalse(order.update_with_order_update(mismatch_order_update)) self.assertEqual(Decimal("0"), order.executed_amount_base) self.assertEqual(Decimal("0"), order.executed_amount_quote) self.assertIsNone(order.fee_asset) self.assertEqual(Decimal("0"), order.cumulative_fee_paid) self.assertEqual(Decimal("0"), order.last_filled_price) self.assertEqual(Decimal("0"), order.last_filled_amount) self.assertEqual(Decimal("0"), order.last_fee_paid) self.assertEqual(-1, order.last_update_timestamp)
def test_update_with_order_update_open_order(self): order: InFlightOrder = InFlightOrder( client_order_id=self.client_order_id, trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), ) open_order_update: OrderUpdate = OrderUpdate( client_order_id=self.client_order_id, exchange_order_id=self.exchange_order_id, trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) self.assertTrue(order.update_with_order_update(open_order_update)) self.assertEqual(Decimal("0"), order.executed_amount_base) self.assertEqual(Decimal("0"), order.executed_amount_quote) self.assertIsNone(order.fee_asset) self.assertEqual(Decimal("0"), order.cumulative_fee_paid) self.assertEqual(Decimal("0"), order.last_filled_price) self.assertEqual(Decimal("0"), order.last_filled_amount) self.assertEqual(Decimal("0"), order.last_fee_paid) self.assertEqual(1, order.last_update_timestamp)
async def process_order_not_found(self, client_order_id: str): """ Increments and checks if the order specified has exceeded the ORDER_NOT_FOUND_COUNT_LIMIT. A failed event is triggered if necessary. :param client_order_id: Client order id of an order. :type client_order_id: str """ # Only concerned with active orders. tracked_order: Optional[InFlightOrder] = self.fetch_tracked_order( client_order_id=client_order_id) if tracked_order is None: self.logger().debug( f"Order is not/no longer being tracked ({client_order_id})") else: self._order_not_found_records[client_order_id] += 1 if self._order_not_found_records[ client_order_id] > self.ORDER_NOT_FOUND_COUNT_LIMIT: # Only mark the order as failed if it has not been marked as done already asynchronously if not tracked_order.is_done: order_update: OrderUpdate = OrderUpdate( client_order_id=client_order_id, trading_pair=tracked_order.trading_pair, update_timestamp=self.current_timestamp, new_state=OrderState.FAILED, ) await self._process_order_update(order_update)
async def _process_order_update(self, order: Dict[str, Any]): symbol = f"{order['baseCurrency']}/{order['quoteCurrency']}" trading_pair = await self.trading_pair_associated_to_exchange_symbol( symbol=symbol) client_order_id = order['clientOrderId'] change_type = order['changeType'] status = order['status'] quantity = Decimal(order["quantity"]) filled = Decimal(order['filled']) delta_filled = Decimal(order['deltaFilled']) state = web_utils.get_order_status_ws(change_type=change_type, status=status, quantity=quantity, filled=filled, delta_filled=delta_filled) if state is None: return timestamp = float(order["timestamp"]) * 1e-3 order_update = OrderUpdate( trading_pair=trading_pair, update_timestamp=timestamp, new_state=state, client_order_id=client_order_id, ) self._order_tracker.process_order_update(order_update=order_update)
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.CANCELLED, ) 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 cancelled 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 _update_order_after_failure(self, order_id: str, trading_pair: str): order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, trading_pair=trading_pair, update_timestamp=self.current_timestamp, new_state=OrderState.FAILED, ) self._order_tracker.process_order_update(order_update)
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_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))
async def _execute_cancel(self, trading_pair: str, order_id: str): """ Requests the exchange to cancel an active order :param trading_pair: the trading pair the order to cancel operates with :param order_id: the client id of the order to cancel """ tracked_order = self._order_tracker.fetch_tracked_order(order_id) if tracked_order is not None: try: symbol = await CoinflexAPIOrderBookDataSource.exchange_symbol_associated_to_pair( trading_pair=trading_pair, domain=self._domain, api_factory=self._api_factory, throttler=self._throttler) api_params = { "responseType": "FULL", } cancel_params = { "marketCode": symbol, "clientOrderId": order_id, } api_params["orders"] = [cancel_params] try: result = await self._api_request( method=RESTMethod.DELETE, path_url=CONSTANTS.ORDER_CANCEL_PATH_URL, data=api_params, is_auth_required=True) cancel_result = result["data"][0] except web_utils.CoinflexAPIError as e: # Catch order not found as cancelled. result = {} cancel_result = {} if e.error_payload.get("errors") in CONSTANTS.ORDER_NOT_FOUND_ERRORS: cancel_result = e.error_payload["data"][0] else: self.logger().error(f"Unhandled error canceling order: {order_id}. Error: {e.error_payload}", exc_info=True) if cancel_result.get("status", result.get("event")) in CONSTANTS.ORDER_CANCELED_STATES: cancelled_timestamp = cancel_result.get("timestamp", result.get("timestamp")) order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, trading_pair=tracked_order.trading_pair, update_timestamp=int(cancelled_timestamp) * 1e-3 if cancelled_timestamp else self.current_timestamp, new_state=OrderState.CANCELED, ) self._order_tracker.process_order_update(order_update) else: if not self._process_order_not_found(order_id, tracked_order): raise IOError return cancel_result except asyncio.CancelledError: raise except Exception: self.logger().exception(f"There was an error when requesting cancelation of order {order_id}")
async def _update_order_status(self): # This is intended to be a backup measure to close straggler orders, in case Binance's user stream events # are not working. # The minimum poll interval for order status is 10 seconds. last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL tracked_orders: List[InFlightOrder] = list( self.in_flight_orders.values()) if current_tick > last_tick and len(tracked_orders) > 0: tasks = [ self._api_get(path_url=CONSTANTS.ORDER_PATH_URL, params={ "symbol": await self.exchange_symbol_associated_to_pair( trading_pair=o.trading_pair), "origClientOrderId": o.client_order_id }, is_auth_required=True) for o in tracked_orders ] self.logger().debug( f"Polling for order status updates of {len(tasks)} orders.") results = await safe_gather(*tasks, return_exceptions=True) for order_update, tracked_order in zip(results, tracked_orders): client_order_id = tracked_order.client_order_id # If the order has already been canceled or has failed do nothing if client_order_id not in self.in_flight_orders: continue if isinstance(order_update, Exception): self.logger().network( f"Error fetching status update for the order {client_order_id}: {order_update}.", app_warning_msg= f"Failed to fetch status update for the order {client_order_id}." ) # Wait until the order not found error have repeated a few times before actually treating # it as failed. See: https://github.com/CoinAlpha/hummingbot/issues/601 await self._order_tracker.process_order_not_found( client_order_id) else: # Update order execution status new_state = CONSTANTS.ORDER_STATE[order_update["status"]] update = OrderUpdate( client_order_id=client_order_id, exchange_order_id=str(order_update["orderId"]), trading_pair=tracked_order.trading_pair, update_timestamp=order_update["updateTime"] * 1e-3, new_state=new_state, ) self._order_tracker.process_order_update(update)
def test_update_with_order_update_multiple_order_updates(self): order: InFlightOrder = InFlightOrder( client_order_id=self.client_order_id, 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"), ) order_update_1: OrderUpdate = OrderUpdate( client_order_id=self.client_order_id, exchange_order_id=self.exchange_order_id, trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.PARTIALLY_FILLED, ) self.assertTrue(order.update_with_order_update(order_update_1)) # Order updates should not modify executed values self.assertEqual(Decimal(0), order.executed_amount_base) self.assertEqual(Decimal(0), order.executed_amount_quote) self.assertEqual(order.last_update_timestamp, 1) self.assertEqual(0, len(order.order_fills)) self.assertTrue(order.is_open) order_update_2: OrderUpdate = OrderUpdate( client_order_id=self.client_order_id, exchange_order_id=self.exchange_order_id, trading_pair=self.trading_pair, update_timestamp=2, new_state=OrderState.FILLED, ) self.assertTrue(order.update_with_order_update(order_update_2)) # Order updates should not modify executed values self.assertEqual(Decimal(0), order.executed_amount_base) self.assertEqual(Decimal(0), order.executed_amount_quote) self.assertEqual(order.last_update_timestamp, 2) self.assertEqual(0, len(order.order_fills)) self.assertTrue(order.is_done)
async def _process_order_update(self, order: InFlightOrder, order_update: Dict[str, Any]): order_data = order_update["data"] new_state = CONSTANTS.ORDER_STATE[order_data["status"]] update = OrderUpdate( client_order_id=order.client_order_id, exchange_order_id=str(order_data["order_id"]), trading_pair=order.trading_pair, update_timestamp=self.current_timestamp, new_state=new_state, ) self._order_tracker.process_order_update(update)
async def _execute_cancel(self, order_id: str, cancel_age: int) -> Optional[str]: """ Cancel an existing order if the age of the order is greater than its cancel_age, and if the order is not done or already in the cancelling state. """ try: tracked_order: GatewayInFlightOrder = self._order_tracker.fetch_order( client_order_id=order_id) if tracked_order is None: self.logger().error( f"The order {order_id} is not being tracked.") raise ValueError(f"The order {order_id} is not being tracked.") if (self.current_timestamp - tracked_order.creation_timestamp) < cancel_age: return None if tracked_order.is_done: return None if tracked_order.is_pending_cancel_confirmation: return order_id self.logger().info( f"The blockchain transaction for {order_id} with nonce {tracked_order.nonce} has " f"expired. Canceling the order...") resp: Dict[str, Any] = await self._get_gateway_instance( ).cancel_evm_transaction(self.chain, self.network, self.address, tracked_order.nonce) tx_hash: Optional[str] = resp.get("txHash") if tx_hash is not None: tracked_order.cancel_tx_hash = tx_hash else: raise EnvironmentError( f"Missing txHash from cancel_evm_transaction() response: {resp}." ) order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, trading_pair=tracked_order.trading_pair, update_timestamp=self.current_timestamp, new_state=OrderState.PENDING_CANCEL) self._order_tracker.process_order_update(order_update) return order_id except asyncio.CancelledError: raise except Exception as err: self.logger().error( f"Failed to cancel order {order_id}: {str(err)}.", exc_info=True)
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"), 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, ) self.tracker.process_order_update(order_creation_update) 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.connector.event_logs)) event_logged = self.connector.event_logs[0] self.assertIsInstance(event_logged, BuyOrderCreatedEvent) 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)
async def _update_order_status(self): # This is intended to be a backup measure to close straggler orders, in case Latoken's user stream events # are not working. # The minimum poll interval for order status is 10 seconds. last_tick = self._last_poll_timestamp / CONSTANTS.UPDATE_ORDER_STATUS_MIN_INTERVAL current_tick = self.current_timestamp / CONSTANTS.UPDATE_ORDER_STATUS_MIN_INTERVAL tracked_orders: List[InFlightOrder] = list( self.in_flight_orders.values()) if current_tick <= last_tick or len(tracked_orders) == 0: return update_to_tracked = await self._update_to_tracked_order( tracked_orders=tracked_orders) for order_update, tracked_order in zip(*update_to_tracked): client_order_id = tracked_order.client_order_id # If the order has already been cancelled or has failed do nothing if client_order_id not in self.in_flight_orders: continue if isinstance(order_update, Exception): self.logger().network( f"Error fetching status update for the order {client_order_id}: {order_update}.", app_warning_msg= f"Failed to fetch status update for the order {client_order_id}." ) # Wait until the order not found error have repeated a few times before actually treating # it as failed. See: https://github.com/CoinAlpha/hummingbot/issues/601 await self._order_tracker.process_order_not_found( client_order_id=client_order_id) else: # Update order execution status status = order_update["status"] filled = Decimal(order_update["filled"]) quantity = Decimal(order_update["quantity"]) new_state = web_utils.get_order_status_rest(status=status, filled=filled, quantity=quantity) update = OrderUpdate( client_order_id=client_order_id, exchange_order_id=order_update["id"], trading_pair=tracked_order.trading_pair, update_timestamp=float(order_update["timestamp"]) * 1e-3, new_state=new_state, ) self._order_tracker.process_order_update(update)
def _process_order_message(self, order_msg: AscendExOrder): """ Updates in-flight order and triggers cancellation or failure event if needed. :param order_msg: The order response from either REST or web socket API (they are of the same format) """ tracked_order = self._in_flight_order_tracker.fetch_order(exchange_order_id=order_msg.orderId) if tracked_order is not None: order_status = CONSTANTS.ORDER_STATE[order_msg.status] cumulative_filled_amount = Decimal(order_msg.cumFilledQty) if (order_status in [OrderState.PARTIALLY_FILLED, OrderState.FILLED] and cumulative_filled_amount > tracked_order.executed_amount_base): filled_amount = cumulative_filled_amount - tracked_order.executed_amount_base cumulative_fee = Decimal(order_msg.cumFee) fee_already_paid = tracked_order.cumulative_fee_paid(token=order_msg.feeAsset, exchange=self) if cumulative_fee > fee_already_paid: fee = TradeFeeBase.new_spot_fee( fee_schema=self.trade_fee_schema(), trade_type=tracked_order.trade_type, percent_token=order_msg.feeAsset, flat_fees=[TokenAmount(amount=cumulative_fee - fee_already_paid, token=order_msg.feeAsset)] ) else: fee = TradeFeeBase.new_spot_fee( fee_schema=self.trade_fee_schema(), trade_type=tracked_order.trade_type) trade_update = TradeUpdate( trade_id=str(order_msg.lastExecTime), client_order_id=tracked_order.client_order_id, exchange_order_id=tracked_order.exchange_order_id, trading_pair=tracked_order.trading_pair, fee=fee, fill_base_amount=filled_amount, fill_quote_amount=filled_amount * Decimal(order_msg.avgPx), fill_price=Decimal(order_msg.avgPx), fill_timestamp=int(order_msg.lastExecTime), ) self._in_flight_order_tracker.process_trade_update(trade_update) order_update = OrderUpdate( exchange_order_id=order_msg.orderId, trading_pair=ascend_ex_utils.convert_to_exchange_trading_pair(order_msg.symbol), update_timestamp=order_msg.lastExecTime * 1e-3, new_state=order_status, ) self._in_flight_order_tracker.process_order_update(order_update=order_update)
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, ) self.tracker.process_order_update(order_creation_update) 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, ) self.tracker.process_order_update(order_creation_update) self.assertTrue( self._is_logged( "DEBUG", f"Order is not/no longer being tracked ({order_creation_update})", ))
def _process_order_not_found(self, client_order_id: str, tracked_order: InFlightOrder) -> bool: self._order_not_found_records[client_order_id] = ( self._order_not_found_records.get(client_order_id, 0) + 1) if (self._order_not_found_records[client_order_id] >= self.MAX_ORDER_UPDATE_RETRIEVAL_RETRIES_WITH_FAILURES): # Wait until the order not found error have repeated a few times before actually treating # it as failed. See: https://github.com/CoinAlpha/hummingbot/issues/601 order_update: OrderUpdate = OrderUpdate( client_order_id=client_order_id, trading_pair=tracked_order.trading_pair, update_timestamp=self.current_timestamp, new_state=OrderState.FAILED, ) self._order_tracker.process_order_update(order_update) return True return False
def _stop_tracking_order_exceed_no_exchange_id_limit(self, tracked_order: InFlightOrder): """ Increments and checks if the tracked order has exceed the STOP_TRACKING_ORDER_NOT_FOUND_LIMIT limit. If true, Triggers a MarketOrderFailureEvent and stops tracking the order. """ client_order_id = tracked_order.client_order_id self._order_without_exchange_id_records[client_order_id] = ( self._order_without_exchange_id_records.get(client_order_id, 0) + 1) if self._order_without_exchange_id_records[client_order_id] >= self.STOP_TRACKING_ORDER_NOT_FOUND_LIMIT: # Wait until the absence of exchange id has repeated a few times before actually treating it as failed. order_update = OrderUpdate( trading_pair=tracked_order.trading_pair, client_order_id=tracked_order.client_order_id, update_timestamp=time.time(), new_state=OrderState.FAILED, ) self._in_flight_order_tracker.process_order_update(order_update) del self._order_without_exchange_id_records[client_order_id]
def _process_order_message(self, order_msg: AscendExOrder): """ Updates in-flight order and triggers cancellation or failure event if needed. :param order_msg: The order response from either REST or web socket API (they are of the same format) """ order_update = OrderUpdate( exchange_order_id=order_msg.orderId, trading_pair=ascend_ex_utils.convert_to_exchange_trading_pair(order_msg.symbol), update_timestamp=order_msg.lastExecTime, new_state=CONSTANTS.ORDER_STATE[order_msg.status], fill_price=Decimal(order_msg.avgPx), executed_amount_base=Decimal(order_msg.cumFilledQty), executed_amount_quote=Decimal(order_msg.avgPx) * Decimal(order_msg.cumFilledQty), fee_asset=order_msg.feeAsset, cumulative_fee_paid=Decimal(order_msg.cumFee), ) self._in_flight_order_tracker.process_order_update(order_update=order_update)
async def _execute_cancel(self, trading_pair: str, order_id: str): """ Requests the exchange to cancel an active order :param trading_pair: the trading pair the order to cancel operates with :param order_id: the client id of the order to cancel """ tracked_order = self._order_tracker.fetch_tracked_order(order_id) if tracked_order is not None: try: symbol = await BinanceAPIOrderBookDataSource.exchange_symbol_associated_to_pair( trading_pair=trading_pair, domain=self._domain, api_factory=self._api_factory, throttler=self._throttler) api_params = { "symbol": symbol, "origClientOrderId": order_id, } cancel_result = await self._api_request( method=RESTMethod.DELETE, path_url=CONSTANTS.ORDER_PATH_URL, params=api_params, is_auth_required=True) if cancel_result.get("status") == "CANCELED": order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, trading_pair=tracked_order.trading_pair, update_timestamp=int(self.current_timestamp * 1e3), new_state=OrderState.CANCELLED, ) self._order_tracker.process_order_update(order_update) return cancel_result except asyncio.CancelledError: raise except Exception: self.logger().exception( f"There was a an error when requesting cancellation of order {order_id}" ) raise
async def _execute_cancel(self, trading_pair: str, order_id: str) -> str: """ Requests the exchange to cancel an active order :param trading_pair: the trading pair the order to cancel operates with :param order_id: the client id of the order to cancel """ tracked_order = self._order_tracker.fetch_tracked_order(order_id) if tracked_order is not None: try: exchange_order_id = await tracked_order.get_exchange_order_id() path_url = f"{CONSTANTS.ORDERS_PATH_URL}/{exchange_order_id}" cancel_result = await self._api_request( path_url=path_url, method=RESTMethod.DELETE, is_auth_required=True, limit_id=CONSTANTS.DELETE_ORDER_LIMIT_ID ) if tracked_order.exchange_order_id in cancel_result["data"].get("cancelledOrderIds", []): order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, trading_pair=tracked_order.trading_pair, update_timestamp=self.current_timestamp, new_state=OrderState.CANCELED, ) self._order_tracker.process_order_update(order_update) return order_id except asyncio.CancelledError: raise except asyncio.TimeoutError: self.logger().warning(f"Failed to cancel the order {order_id} because it does not have an exchange" f" order id yet") await self._order_tracker.process_order_not_found(order_id) except Exception as e: self.logger().network( f"Failed to cancel order {order_id}: {str(e)}", exc_info=True, app_warning_msg=f"Failed to cancel the order {order_id} on Kucoin. " f"Check API key and network connection." )
async def _user_stream_event_listener(self): """ This functions runs in background continuously processing the events received from the exchange by the user stream data source. It keeps reading events from the queue until the task is interrupted. The events received are balance updates, order updates and trade events. """ async for event_message in self._iter_user_event_queue(): try: event_type = event_message.get("table") if event_type == "order": order_data = event_message["data"][0] client_order_id = order_data.get("clientOrderId") tracked_order = self.in_flight_orders.get(client_order_id) if not tracked_order: return try: await tracked_order.get_exchange_order_id() except asyncio.TimeoutError: self.logger().error(f"Failed to get exchange order id for order: {tracked_order.client_order_id}") raise await self._update_order_fills_from_event_or_create(tracked_order, order_data) order_update = OrderUpdate( trading_pair=tracked_order.trading_pair, update_timestamp=int(order_data["timestamp"]) * 1e-3, new_state=CONSTANTS.ORDER_STATE[order_data["status"]], client_order_id=client_order_id, exchange_order_id=str(order_data["orderId"]), ) self._order_tracker.process_order_update(order_update=order_update) elif event_type == "balance": self._process_balance_message(event_message) except asyncio.CancelledError: raise except Exception: self.logger().error("Unexpected error in user stream listener loop.", exc_info=True) await asyncio.sleep(5.0)
def test_update_with_order_update_client_order_id_mismatch(self): order: InFlightOrder = InFlightOrder( client_order_id=self.client_order_id, 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"), ) mismatch_order_update: OrderUpdate = OrderUpdate( client_order_id="mismatchClientOrderId", exchange_order_id="mismatchExchangeOrderId", trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) self.assertFalse(order.update_with_order_update(mismatch_order_update)) self.assertEqual(Decimal("0"), order.executed_amount_base) self.assertEqual(Decimal("0"), order.executed_amount_quote) self.assertEqual(order.creation_timestamp, order.last_update_timestamp)
def test_update_exchange_id_with_order_update(self): order: InFlightOrder = InFlightOrder( client_order_id=self.client_order_id, trading_pair=self.trading_pair, order_type=OrderType.LIMIT, trade_type=TradeType.BUY, amount=Decimal("1000.0"), price=Decimal("1.0"), ) order_update: OrderUpdate = OrderUpdate( client_order_id=self.client_order_id, exchange_order_id=self.exchange_order_id, trading_pair=self.trading_pair, update_timestamp=1, new_state=OrderState.OPEN, ) result = order.update_with_order_update(order_update) self.assertTrue(result) self.assertEqual(self.exchange_order_id, order.exchange_order_id) self.assertTrue(order.exchange_order_id_update_event.is_set()) self.assertEqual(0, len(order.order_fills))