def _process_order_event_message(self, order_msg: Dict[str, Any]): """ Updates in-flight order and triggers cancellation or failure event if needed. :param order_msg: The order event message payload """ client_order_id = str(order_msg["ClientOrderId"]) if client_order_id in self.in_flight_orders: tracked_order = self.in_flight_orders[client_order_id] was_locally_working = tracked_order.is_locally_working # Update order execution status tracked_order.last_state = order_msg["OrderState"] if was_locally_working and tracked_order.is_working: self.trigger_order_created_event(tracked_order) elif tracked_order.is_cancelled: self.logger().info( f"Successfully cancelled order {client_order_id}") self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id)) self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info( f"The market order {client_order_id} has failed according to order status event. " f"Reason: {order_msg['ChangeReason']}") self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id)
async def _process_stuck_order(self, tracked_order): order_id = tracked_order.client_order_id open_orders = await self.get_open_orders() matched_orders = [ order for order in open_orders if str(order.client_order_id) == str(order_id) ] if len(matched_orders) == 1: tracked_order.update_exchange_order_id( str(matched_orders[0].exchange_order_id)) del self._order_not_created_records[order_id] return self._order_not_created_records[ order_id] = self._order_not_created_records.get(order_id, 0) + 1 if self._order_not_created_records[ order_id] >= self.ORDER_NOT_CREATED_ID_COUNT: self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, tracked_order.order_type)) tracked_order.last_state = "fail" self.stop_tracking_order(order_id)
def test_remaining_quantity_updated_after_failed_order_event(self): self.limit_buy_strategy.logger().setLevel(1) self.limit_buy_strategy.logger().addHandler(self) self.clock.add_iterator(self.limit_buy_strategy) # check no orders are placed before time delay self.clock.backtest_til(self.start_timestamp) self.assertEqual(0, len(self.limit_buy_strategy.active_bids)) # one order created after first tick self.clock.backtest_til(self.start_timestamp + math.ceil(self.clock_tick_size / self.order_delay_time)) self.assertEqual(1, len(self.limit_buy_strategy.active_bids)) bid_order: LimitOrder = self.limit_buy_strategy.active_bids[0][1] self.assertEqual(1, bid_order.quantity) self.assertEqual(self.limit_buy_strategy._quantity_remaining, 1) # Simulate order cancel self.market.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.market.current_timestamp, bid_order.client_order_id, OrderType.LIMIT)) self.assertEqual(0, len(self.limit_buy_strategy.active_bids)) self.assertEqual(self.limit_buy_strategy._quantity_remaining, 2) self.assertTrue( self._is_logged( 'INFO', f"Updating status after order fail (id: {bid_order.client_order_id})" ))
async def execute_sell( self, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, price: Optional[Decimal] = Decimal("NaN"), ): try: await self.execute_order(TradeType.SELL, order_id, trading_pair, amount, order_type, price) tracked_order = self.in_flight_orders.get(order_id) if tracked_order is not None: self.trigger_event( SELL_ORDER_CREATED_EVENT, SellOrderCreatedEvent( now(), order_type, trading_pair, Decimal(amount), Decimal(price), order_id, tracked_order.creation_timestamp, ), ) except ValueError as e: # never tracked, so no need to stop tracking self.trigger_event( ORDER_FAILURE_EVENT, MarketOrderFailureEvent(now(), order_id, order_type)) self.logger().warning( f"Failed to place {order_id} on bitmex. {str(e)}")
def _process_order_message(self, order_msg: Dict[str, Any]): """ 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) """ exchange_order_id = str(order_msg['id']) """ Currently wazirx api are not supporting client_order_id, so looping through each in flight order and matching exchange order id. """ for order in list(self._in_flight_orders.values()): if str(order.exchange_order_id) == str(exchange_order_id): client_order_id = order.client_order_id tracked_order = self._in_flight_orders[client_order_id] tracked_order.last_state = order_msg["status"] if tracked_order.is_cancelled: self.logger().info( f"Successfully cancelled order {client_order_id}.") self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id)) tracked_order.cancelled_event.set() self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info( f"The market order {client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id)
def _process_order_message(self, order_msg: Dict[str, Any]): """ 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) """ client_order_id = order_msg["client_oid"] if client_order_id not in self._in_flight_orders: return tracked_order = self._in_flight_orders[client_order_id] # Update order execution status tracked_order.last_state = order_msg["status"] if tracked_order.is_cancelled: self.logger().info( f"Successfully cancelled order {client_order_id}.") self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id)) tracked_order.cancelled_event.set() self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info( f"The market order {client_order_id} has failed according to order status API. " f"Reason: {crypto_com_utils.get_api_reason(order_msg['reason'])}" ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id)
async def _update_order_status(self): """ Calls REST API to get status update for each in-flight order. """ last_tick = int(self._last_poll_timestamp / CONSTANTS.UPDATE_ORDER_STATUS_INTERVAL) current_tick = (0 if math.isnan(self.current_timestamp) else int( self.current_timestamp / CONSTANTS.UPDATE_ORDER_STATUS_INTERVAL)) if current_tick > last_tick and len(self._in_flight_orders) > 0: tracked_orders = list(self._in_flight_orders.values()) tasks = [] for tracked_order in tracked_orders: try: exchange_order_id = await tracked_order.get_exchange_order_id( ) except asyncio.TimeoutError: self.logger().network( f"Skipped order status update for {tracked_order.client_order_id} " "- waiting for exchange order id.") continue trading_pair = convert_to_exchange_trading_pair( tracked_order.trading_pair) tasks.append( self._api_request( "GET", CONSTANTS.ORDER_STATUS_PATH_URL.format( id=exchange_order_id), params={'currency_pair': trading_pair}, is_auth_required=True, limit_id=CONSTANTS.ORDER_STATUS_LIMIT_ID)) self.logger().debug( f"Polling for order status updates of {len(tasks)} orders.") responses = await safe_gather(*tasks, return_exceptions=True) for response, tracked_order in zip(responses, tracked_orders): client_order_id = tracked_order.client_order_id if isinstance(response, GateIoAPIError): if response.error_label == 'ORDER_NOT_FOUND': 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.ORDER_NOT_EXIST_CONFIRMATION_COUNT: # 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 continue self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id) else: continue elif "id" not in response: self.logger().info( f"_update_order_status id not in resp: {response}") continue else: self._process_order_message(response)
def simulate_order_failed(market_info: MarketTradingPairTuple, order: Union[LimitOrder, MarketOrder]): market_info.market.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent( int(time.time() * 1e3), order.client_order_id if isinstance( order, LimitOrder) else order.order_id, OrderType.LIMIT if isinstance(order, LimitOrder) else OrderType.MARKET))
def _trigger_failure_event(self, order: InFlightOrder): self._connector.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent( timestamp=self.current_timestamp, order_id=order.client_order_id, order_type=order.order_type, ), )
async def update_swap_order(self, update_result: Dict[str, any], tracked_order: UniswapInFlightOrder): if update_result.get("confirmed", False): if update_result["receipt"].get("status", 0) == 1: order_id = await tracked_order.get_exchange_order_id() gas_used = update_result["receipt"]["gasUsed"] gas_price = tracked_order.gas_price fee = Decimal(str(gas_used)) * Decimal( str(gas_price)) / Decimal(str(1e9)) self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent(self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(str(tracked_order.price)), Decimal(str(tracked_order.amount)), AddedToCostTradeFee(flat_fees=[ TokenAmount(tracked_order.fee_asset, Decimal(str(fee))) ]), exchange_trade_id=order_id)) tracked_order.last_state = "FILLED" self.logger().info( f"The {tracked_order.trade_type.name} order " f"{tracked_order.client_order_id} has completed " f"according to order status API.") event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY \ else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY \ else SellOrderCompletedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, float(fee), tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id) else: self.logger().info( f"The {tracked_order.type} order {tracked_order.client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, tracked_order.client_order_id, tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id)
def _process_order_message(self, order_msg: Dict[str, Any]): """ 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) Example Order: { "id": 9401, "market": "rogerbtc", "kind": "ask", "side": "sell", "ord_type": "limit", "price": "0.00000099", "avg_price": "0.00000099", "state": "wait", "origin_volume": "7000.0", "remaining_volume": "2810.1", "executed_volume": "4189.9", "at": 1596481983, "created_at": 1596481983, "updated_at": 1596553643, "trades_count": 272 } """ exchange_order_id = str(order_msg["id"]) tracked_orders = list(self._in_flight_orders.values()) track_order = [o for o in tracked_orders if exchange_order_id == o.exchange_order_id] if not track_order: return tracked_order = track_order[0] # Estimate fee order_msg["trade_fee"] = self.estimate_fee_pct(tracked_order.order_type is OrderType.LIMIT_MAKER) try: updated = tracked_order.update_with_order_update(order_msg) except Exception as e: self.logger().error(f"Error in order update for {tracked_order.exchange_order_id}. Message: {order_msg}\n{e}") traceback.print_exc() raise e if updated: safe_ensure_future(self._trigger_order_fill(tracked_order, order_msg)) if tracked_order.is_cancelled: self.logger().info(f"Successfully cancelled order {tracked_order.client_order_id}.") self.stop_tracking_order(tracked_order.client_order_id) self.trigger_event(MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, tracked_order.client_order_id)) tracked_order.cancelled_event.set() elif tracked_order.is_failure: self.logger().info(f"The market order {tracked_order.client_order_id} has failed according to order status API. ") self.trigger_event(MarketEvent.OrderFailure, MarketOrderFailureEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.order_type)) tracked_order.last_state = "fail" self.stop_tracking_order(tracked_order.client_order_id)
def _process_order_message(self, order_msg: Dict[str, Any]): """ 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) Example Order: { "id": "4345613661", "clientOrderId": "57d5525562c945448e3cbd559bd068c3", "symbol": "BCCBTC", "side": "sell", "status": "new", "type": "limit", "timeInForce": "GTC", "quantity": "0.013", "price": "0.100000", "cumQuantity": "0.000", "postOnly": false, "createdAt": "2017-10-20T12:17:12.245Z", "updatedAt": "2017-10-20T12:17:12.245Z", "reportType": "status" } """ client_order_id = order_msg["clientOrderId"] if client_order_id not in self._in_flight_orders: return tracked_order = self._in_flight_orders[client_order_id] # Update order execution status tracked_order.last_state = order_msg["status"] # update order tracked_order.executed_amount_base = Decimal(order_msg["cumQuantity"]) tracked_order.executed_amount_quote = Decimal( order_msg["price"]) * Decimal(order_msg["cumQuantity"]) if tracked_order.is_cancelled: self.logger().info( f"Successfully canceled order {client_order_id}.") self.stop_tracking_order(client_order_id) self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id)) tracked_order.cancelled_event.set() elif tracked_order.is_failure: self.logger().info( f"The market order {client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id)
async def _update_order_status(self): """ Calls REST API to get status update for each in-flight order. """ last_tick = int(self._last_poll_timestamp / Constants.UPDATE_ORDER_STATUS_INTERVAL) current_tick = int(self.current_timestamp / Constants.UPDATE_ORDER_STATUS_INTERVAL) if current_tick > last_tick and len(self._in_flight_orders) > 0: tracked_orders = list(self._in_flight_orders.values()) tasks = [] for tracked_order in tracked_orders: # exchange_order_id = await tracked_order.get_exchange_order_id() order_id = tracked_order.client_order_id tasks.append( self._api_request( "GET", Constants.ENDPOINT["ORDER_STATUS"].format(id=order_id), is_auth_required=True)) self.logger().debug( f"Polling for order status updates of {len(tasks)} orders.") responses = await safe_gather(*tasks, return_exceptions=True) for response, tracked_order in zip(responses, tracked_orders): client_order_id = tracked_order.client_order_id if isinstance(response, HitbtcAPIError): err = response.error_payload.get('error', response.error_payload) if err.get('code') == 20002: 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.ORDER_NOT_EXIST_CONFIRMATION_COUNT: # 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 continue self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id) else: continue elif "clientOrderId" not in response: self.logger().info( f"_update_order_status clientOrderId not in resp: {response}" ) continue else: self._process_order_message(response)
def stop_tracking_order_exceed_not_found_limit(self, tracked_order: AltmarketsInFlightOrder): """ Increments and checks if the tracked order has exceed the ORDER_NOT_EXIST_CONFIRMATION_COUNT limit. If true, Triggers a MarketOrderFailureEvent and stops tracking the order. """ client_order_id = tracked_order.client_order_id 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.ORDER_NOT_EXIST_CONFIRMATION_COUNT: # 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 self.trigger_event(MarketEvent.OrderFailure, MarketOrderFailureEvent( self.current_timestamp, client_order_id, tracked_order.order_type)) tracked_order.last_state = "fail" self.stop_tracking_order(client_order_id)
async def _update_order_status(self): """ Calls REST API to get status update for each in-flight order. """ last_tick = int(self._last_poll_timestamp / Constants.UPDATE_ORDER_STATUS_INTERVAL) current_tick = int(self.current_timestamp / Constants.UPDATE_ORDER_STATUS_INTERVAL) if current_tick > last_tick and len(self._in_flight_orders) > 0: tracked_orders = list(self._in_flight_orders.values()) api_params = { 'symbol': None, 'orderSide': None, 'orderStatuses': ["NEW", "PARTIALLY_FILLED"], 'size': 500, 'bookmarkOrderId': None } self.logger().debug( f"Polling for order status updates of {len(tracked_orders)} orders." ) open_orders = await self._api_request( "POST", Constants.ENDPOINT["ORDER_STATUS"], api_params, is_auth_required=True) open_orders_dict = {o['id']: o for o in open_orders} found_ex_order_ids = list(open_orders_dict.keys()) for tracked_order in tracked_orders: client_order_id = tracked_order.client_order_id ex_order_id = tracked_order.exchange_order_id if ex_order_id not in found_ex_order_ids: 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.ORDER_NOT_EXIST_CONFIRMATION_COUNT: # Wait until the order is not found a few times before actually treating it as failed. continue self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id) else: self._process_order_message(open_orders_dict[ex_order_id])
async def _process_order_message(self, order_msg: Dict[str, Any]): """ 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) """ for order in self._in_flight_orders.values(): await order.get_exchange_order_id() exchange_order_id = str(order_msg["order_id"]) tracked_orders = list(self._in_flight_orders.values()) tracked_order = [ order for order in tracked_orders if exchange_order_id == order.exchange_order_id ] if not tracked_order: return tracked_order = tracked_order[0] client_order_id = tracked_order.client_order_id # Update order execution status if "status" in order_msg: # REST API tracked_order.last_state = CONSTANTS.ORDER_STATUS[int( order_msg["status"])] elif "state" in order_msg: # WebSocket tracked_order.last_state = CONSTANTS.ORDER_STATUS[int( order_msg["state"])] if tracked_order.is_cancelled: self.logger().info( f"Successfully cancelled order {client_order_id}.") self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id)) tracked_order.cancelled_event.set() self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info( f"The market order {client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id)
def _process_order_message(self, order_msg: Dict[str, Any]): """ 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) """ ex_order_id = order_msg["orderid"] client_order_id = None for tracked_order in self.in_flight_orders.values(): if tracked_order.exchange_order_id == ex_order_id: client_order_id = tracked_order.client_order_id break if client_order_id not in self.in_flight_orders: return tracked_order: K2InFlightOrder = self.in_flight_orders[client_order_id] # Update order execution status tracked_order.last_state = constants.ORDER_STATUS[order_msg["status"]] if tracked_order.is_cancelled: self.logger().info( f"Successfully cancelled order {client_order_id}.") self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id)) tracked_order.cancelled_event.set() self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info( f"The order {client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id)
async def _create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, price: Decimal): """ Calls buy or sell API end point to place an order, starts tracking the order and triggers relevant order events. :param trade_type: BUY or SELL :param order_id: Internal order id (also called client_order_id) :param trading_pair: The market to place order :param amount: The order amount (in base token value) :param price: The order price """ amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) base, quote = trading_pair.split("-") gas_price = get_gas_price() api_params = { "base": self._token_addresses[base], "quote": self._token_addresses[quote], "amount": str(amount), "maxPrice": str(price), "maxSwaps": str(self._max_swaps), "gasPrice": str(gas_price), } self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, gas_price) try: order_result = await self._api_request( "post", f"balancer/{trade_type.name.lower()}", api_params) hash = order_result.get("txHash") tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info( f"Created {trade_type.name} order {order_id} txHash: {hash} " f"for {amount} {trading_pair}.") tracked_order.update_exchange_order_id(hash) tracked_order.gas_price = gas_price if hash is not None: tracked_order.fee_asset = "ETH" tracked_order.executed_amount_base = amount tracked_order.executed_amount_quote = amount * price event_tag = MarketEvent.BuyOrderCreated if trade_type is TradeType.BUY else MarketEvent.SellOrderCreated event_class = BuyOrderCreatedEvent if trade_type is TradeType.BUY else SellOrderCreatedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, OrderType.LIMIT, trading_pair, amount, price, order_id, hash)) else: self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, OrderType.LIMIT)) except asyncio.CancelledError: raise except Exception as e: self.stop_tracking_order(order_id) self.logger().network( f"Error submitting {trade_type.name} order to Balancer for " f"{amount} {trading_pair} " f"{price}.", exc_info=True, app_warning_msg=str(e)) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, OrderType.LIMIT))
async def _create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, price: Decimal): """ Calls create-order API end point to place an order, starts tracking the order and triggers order created event. :param trade_type: BUY or SELL :param order_id: Internal order id (also called client_order_id) :param trading_pair: The market to place order :param amount: The order amount (in base token value) :param order_type: The order type :param price: The order price """ if not order_type.is_limit_type(): raise Exception(f"Unsupported order type: {order_type}") trading_rule = self._trading_rules[trading_pair] amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) if amount < trading_rule.min_order_size: raise ValueError( f"Buy order amount {amount} is lower than the minimum order size " f"{trading_rule.min_order_size}.") api_params = { "instrument_name": crypto_com_utils.convert_to_exchange_trading_pair(trading_pair), "side": trade_type.name, "type": "LIMIT", "price": f"{price:f}", "quantity": f"{amount:f}", "client_oid": order_id } if order_type is OrderType.LIMIT_MAKER: api_params["exec_inst"] = "POST_ONLY" self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type) try: order_result = await self._api_request("post", "private/create-order", api_params, True) exchange_order_id = str(order_result["result"]["order_id"]) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info( f"Created {order_type.name} {trade_type.name} order {order_id} for " f"{amount} {trading_pair}.") tracked_order.update_exchange_order_id(exchange_order_id) event_tag = MarketEvent.BuyOrderCreated if trade_type is TradeType.BUY else MarketEvent.SellOrderCreated event_class = BuyOrderCreatedEvent if trade_type is TradeType.BUY else SellOrderCreatedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, order_type, trading_pair, amount, price, order_id)) except asyncio.CancelledError: raise except Exception as e: self.stop_tracking_order(order_id) self.logger().network( f"Error submitting {trade_type.name} {order_type.name} order to Crypto.com for " f"{amount} {trading_pair} " f"{price}.", exc_info=True, app_warning_msg=str(e)) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type))
async def _create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, price: Decimal = s_decimal_0, order_type: OrderType = OrderType.MARKET): """ Calls create-order API end point to place an order, starts tracking the order and triggers order created event. :param trade_type: BUY or SELL :param order_id: Internal order id (also called client_order_id) :param trading_pair: The market to place order :param amount: The order amount (in base token value) :param price: The order price :param order_type: The order type """ trading_rule: TradingRule = self._trading_rules[trading_pair] trading_pair_ids: Dict[ str, int] = await self._order_book_tracker.data_source.get_instrument_ids( ) try: amount: Decimal = self.quantize_order_amount(trading_pair, amount) if amount < trading_rule.min_order_size: raise ValueError( f"{trade_type.name} order amount {amount} is lower than the minimum order size " f"{trading_rule.min_order_size}.") params = { "InstrumentId": trading_pair_ids[trading_pair], "OMSId": 1, "AccountId": await self.initialized_account_id(), "ClientOrderId": int(order_id), "Side": 0 if trade_type == TradeType.BUY else 1, "Quantity": amount, "TimeInForce": 1, # GTC } if order_type.is_limit_type(): price: Decimal = self.quantize_order_price(trading_pair, price) params.update({ "OrderType": 2, # Limit "LimitPrice": price, }) else: params.update({ "OrderType": 1 # Market }) self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type) send_order_results = await self._api_request( method="POST", path_url=CONSTANTS.SEND_ORDER_PATH_URL, data=params, is_auth_required=True) if send_order_results["status"] == "Rejected": raise ValueError( f"Order is rejected by the API. " f"Parameters: {params} Error Msg: {send_order_results['errormsg']}" ) exchange_order_id = str(send_order_results["OrderId"]) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info( f"Created {order_type.name} {trade_type.name} order {order_id} for " f"{amount} {trading_pair}.") tracked_order.update_exchange_order_id(exchange_order_id) except asyncio.CancelledError: raise except Exception as e: self.stop_tracking_order(order_id) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type)) self.logger().network( f"Error submitting {trade_type.name} {order_type.name} order to NDAX for " f"{amount} {trading_pair} {price}. Error: {str(e)}", exc_info=True, app_warning_msg="Error submitting order to NDAX. ")
async def _create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, price: Decimal): """ Calls create-order API end point to place an order, starts tracking the order and triggers order created event. :param trade_type: BUY or SELL :param order_id: Internal order id (aka client_order_id) :param trading_pair: The market to place order :param amount: The order amount (in base token value) :param order_type: The order type :param price: The order price """ if not order_type.is_limit_type(): raise Exception(f"Unsupported order type: {order_type}") amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) if amount <= s_decimal_0: raise ValueError("Order amount must be greater than zero.") try: timestamp = ascend_ex_utils.get_ms_timestamp() api_params = { "id": order_id, "time": timestamp, "symbol": ascend_ex_utils.convert_to_exchange_trading_pair(trading_pair), "orderPrice": f"{price:f}", "orderQty": f"{amount:f}", "orderType": "limit", "side": "buy" if trade_type == TradeType.BUY else "sell", "respInst": "ACCEPT", } self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type) resp = await self._api_request(method="post", path_url=CONSTANTS.ORDER_PATH_URL, data=api_params, is_auth_required=True, force_auth_path_url="order") exchange_order_id = str(resp["data"]["info"]["orderId"]) tracked_order: AscendExInFlightOrder = self._in_flight_orders.get( order_id) tracked_order.update_exchange_order_id(exchange_order_id) if resp["data"]["status"] == "Ack": # Ack status means the server has received the request return tracked_order.update_status(resp["data"]["info"]["status"]) if tracked_order.is_failure: raise Exception( f'Failed to create an order, reason: {resp["data"]["info"]["errorCode"]}' ) self.logger().info( f"Created {order_type.name} {trade_type.name} order {order_id} for " f"{amount} {trading_pair}.") self.trigger_order_created_event(tracked_order) except asyncio.CancelledError: raise except Exception: self.stop_tracking_order(order_id) msg = f"Error submitting {trade_type.name} {order_type.name} order to AscendEx for " \ f"{amount} {trading_pair} " \ f"{price}." self.logger().network(msg, exc_info=True, app_warning_msg=msg) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type))
async def _create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, price: Decimal): """ Calls create-order API end point to place an order, starts tracking the order and triggers order created event. :param trade_type: BUY or SELL :param order_id: Internal order id (also called client_order_id) :param trading_pair: The market to place order :param amount: The order amount (in base token value) :param order_type: The order type :param price: The order price """ trading_rule = self._trading_rules[trading_pair] try: amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price( trading_pair, s_decimal_0 if math.isnan(price) else price) if amount < trading_rule.min_order_size: raise ValueError( f"Buy order amount {amount} is lower than the minimum order size " f"{trading_rule.min_order_size}.") order_type_str = order_type.name.lower().split("_")[0] api_params = { "market": convert_to_exchange_trading_pair(trading_pair), "side": trade_type.name.lower(), "ord_type": order_type_str, # "price": f"{price:f}", "client_id": order_id, "volume": f"{amount:f}", } if order_type is not OrderType.MARKET: api_params['price'] = f"{price:f}" # if order_type is OrderType.LIMIT_MAKER: # api_params["postOnly"] = "true" self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type) order_result = await self._api_request( "POST", Constants.ENDPOINT["ORDER_CREATE"], params=api_params, is_auth_required=True, limit_id=Constants.RL_ID_ORDER_CREATE, disable_retries=True) exchange_order_id = str(order_result["id"]) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info( f"Created {order_type.name} {trade_type.name} order {order_id} for " f"{amount} {trading_pair}.") tracked_order.update_exchange_order_id(exchange_order_id) else: raise Exception('Order not tracked.') if trade_type is TradeType.BUY: event_tag = MarketEvent.BuyOrderCreated event_cls = BuyOrderCreatedEvent else: event_tag = MarketEvent.SellOrderCreated event_cls = SellOrderCreatedEvent self.trigger_event( event_tag, event_cls(self.current_timestamp, order_type, trading_pair, amount, price, order_id, exchange_order_id)) except asyncio.CancelledError: raise except Exception as e: if isinstance(e, AltmarketsAPIError): error_reason = e.error_payload.get('error', {}).get( 'message', e.error_payload.get('errors')) else: error_reason = e if error_reason and "upstream connect error" not in str( error_reason): self.stop_tracking_order(order_id) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type)) else: self._order_not_created_records[order_id] = 0 self.logger().network( f"Error submitting {trade_type.name} {order_type.name} order to {Constants.EXCHANGE_NAME} for " f"{amount} {trading_pair} {price} - {error_reason}.", exc_info=True, app_warning_msg= (f"Error submitting order to {Constants.EXCHANGE_NAME} - {error_reason}." ))
async def execute_sell(self, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, price: Optional[Decimal] = s_decimal_0): trading_rule = self._trading_rules[trading_pair] if not order_type.is_limit_type(): self.trigger_event( self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type)) raise Exception(f"Unsupported order type: {order_type}") decimal_price = self.quantize_order_price(trading_pair, price) decimal_amount = self.quantize_order_amount(trading_pair, amount, decimal_price) if decimal_price * decimal_amount < trading_rule.min_notional_size: self.trigger_event( self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type)) raise ValueError( f"Sell order amount {decimal_amount} is lower than the notional size " ) try: exchange_order_id = await self.place_order(order_id, trading_pair, decimal_amount, False, order_type, decimal_price) self.start_tracking_order(order_id=order_id, exchange_order_id=exchange_order_id, trading_pair=trading_pair, order_type=order_type, trade_type=TradeType.SELL, price=decimal_price, amount=decimal_amount) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info( f"Created {order_type.name.upper()} sell order {order_id} for {decimal_amount} {trading_pair}." ) self.trigger_event( self.MARKET_SELL_ORDER_CREATED_EVENT_TAG, SellOrderCreatedEvent(self.current_timestamp, order_type, trading_pair, decimal_amount, decimal_price, order_id)) except asyncio.CancelledError: raise except Exception as ex: self.stop_tracking_order(order_id) order_type_str = order_type.name.lower() self.logger().network( f"Error submitting sell {order_type_str} order to Mexc for " f"{decimal_amount} {trading_pair} " f"{decimal_price if order_type is OrderType.LIMIT else ''}." f"{decimal_price}." + ",ex:" + repr(ex), exc_info=True, app_warning_msg= "Failed to submit sell order to Mexc. Check API key and network connection." ) self.trigger_event( self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type))
async def _update_order_status(self): last_tick = int(self._last_poll_timestamp / self.UPDATE_ORDERS_INTERVAL) current_tick = int(self.current_timestamp / self.UPDATE_ORDERS_INTERVAL) if current_tick > last_tick and len(self._in_flight_orders) > 0: tracked_orders = list(self._in_flight_orders.values()) for tracked_order in tracked_orders: try: exchange_order_id = await tracked_order.get_exchange_order_id( ) try: order_update = await self.get_order_status( exchange_order_id, tracked_order.trading_pair) except MexcAPIError as ex: err_code = ex.error_payload.get("error").get( 'err-code') self.stop_tracking_order(tracked_order.client_order_id) self.logger().info( f"The limit order {tracked_order.client_order_id} " f"has failed according to order status API. - {err_code}" ) self.trigger_event( self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.order_type)) continue if order_update is None: self.logger().network( f"Error fetching status update for the order {tracked_order.client_order_id}: " f"{exchange_order_id}.", app_warning_msg= f"Could not fetch updates for the order {tracked_order.client_order_id}. " f"The order has either been filled or canceled.") continue tracked_order.last_state = order_update['state'] order_status = order_update['state'] new_confirmed_amount = Decimal( order_update['deal_quantity']) execute_amount_diff = new_confirmed_amount - tracked_order.executed_amount_base if execute_amount_diff > s_decimal_0: execute_price = Decimal( Decimal(order_update['deal_amount']) / Decimal(order_update['deal_quantity'])) tracked_order.executed_amount_base = Decimal( order_update['deal_quantity']) tracked_order.executed_amount_quote = Decimal( order_update['deal_amount']) order_filled_event = OrderFilledEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, execute_price, execute_amount_diff, self.get_fee( tracked_order.base_asset, tracked_order.quote_asset, tracked_order.order_type, tracked_order.trade_type, execute_amount_diff, execute_price, ), exchange_trade_id=exchange_order_id) self.logger().info( f"Filled {execute_amount_diff} out of {tracked_order.amount} of the " f"order {tracked_order.client_order_id}.") self.trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, order_filled_event) if order_status == "FILLED": fee_paid, fee_currency = await self.get_deal_detail_fee( tracked_order.exchange_order_id) tracked_order.fee_paid = fee_paid tracked_order.fee_asset = fee_currency tracked_order.last_state = order_status self.stop_tracking_order(tracked_order.client_order_id) if tracked_order.trade_type is TradeType.BUY: self.logger().info( f"The BUY {tracked_order.order_type} order {tracked_order.client_order_id} has completed " f"according to order delta restful API.") self.trigger_event( self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG, BuyOrderCompletedEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset or tracked_order.quote_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type)) elif tracked_order.trade_type is TradeType.SELL: self.logger().info( f"The SELL {tracked_order.order_type} order {tracked_order.client_order_id} has completed " f"according to order delta restful API.") self.trigger_event( self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG, SellOrderCompletedEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset or tracked_order.quote_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type)) continue if order_status == "CANCELED" or order_status == "PARTIALLY_CANCELED": tracked_order.last_state = order_status self.stop_tracking_order(tracked_order.client_order_id) self.logger().info( f"Order {tracked_order.client_order_id} has been cancelled " f"according to order delta restful API.") self.trigger_event( self.MARKET_ORDER_CANCELLED_EVENT_TAG, OrderCancelledEvent(self.current_timestamp, tracked_order.client_order_id)) except Exception as ex: self.logger().error("_update_order_status error ..." + repr(ex), exc_info=True)
async def _create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, price: Decimal): """ Calls create-order API end point to place an order, starts tracking the order and triggers order created event. :param trade_type: BUY or SELL :param order_id: Internal order id (also called client_order_id) :param trading_pair: The market to place order :param amount: The order amount (in base token value) :param order_type: The order type :param price: The order price """ if not order_type.is_limit_type(): raise Exception(f"Unsupported order type: {order_type}") trading_rule = self._trading_rules[trading_pair] amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) if amount < trading_rule.min_order_size: raise ValueError(f"Buy order amount {amount} is lower than the minimum order size " f"{trading_rule.min_order_size}.") order_type_str = order_type.name.lower().split("_")[0] api_params = {"symbol": convert_to_exchange_trading_pair(trading_pair), "side": trade_type.name.lower(), "type": order_type_str, "price": f"{price:f}", "quantity": f"{amount:f}", "clientOrderId": order_id, # Without strict validate, HitBTC might adjust order prices/sizes. "strictValidate": "true", } if order_type is OrderType.LIMIT_MAKER: api_params["postOnly"] = "true" self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type) try: order_result = await self._api_request("POST", Constants.ENDPOINT["ORDER_CREATE"], api_params, True) exchange_order_id = str(order_result["id"]) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info(f"Created {order_type.name} {trade_type.name} order {order_id} for " f"{amount} {trading_pair}.") tracked_order.update_exchange_order_id(exchange_order_id) if trade_type is TradeType.BUY: event_tag = MarketEvent.BuyOrderCreated event_cls = BuyOrderCreatedEvent else: event_tag = MarketEvent.SellOrderCreated event_cls = SellOrderCreatedEvent self.trigger_event(event_tag, event_cls(self.current_timestamp, order_type, trading_pair, amount, price, order_id)) except asyncio.CancelledError: raise except HitbtcAPIError as e: error_reason = e.error_payload.get('error', {}).get('message') self.stop_tracking_order(order_id) self.logger().network( f"Error submitting {trade_type.name} {order_type.name} order to {Constants.EXCHANGE_NAME} for " f"{amount} {trading_pair} {price} - {error_reason}.", exc_info=True, app_warning_msg=(f"Error submitting order to {Constants.EXCHANGE_NAME} - {error_reason}.") ) self.trigger_event(MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type))
async def _update_order_status(self): """ Calls REST API to get status update for each in-flight order. """ if len(self._in_flight_orders) > 0: tracked_orders = list(self._in_flight_orders.values()) tasks = [] for tracked_order in tracked_orders: order_id = await tracked_order.get_exchange_order_id() tasks.append( self._api_request("post", "eth/get-receipt", {"txHash": order_id})) update_results = await safe_gather(*tasks, return_exceptions=True) for update_result in update_results: self.logger().info( f"Polling for order status updates of {len(tasks)} orders." ) if isinstance(update_result, Exception): raise update_result if "txHash" not in update_result: self.logger().info( f"_update_order_status txHash not in resp: {update_result}" ) continue if update_result["confirmed"] is True: if update_result["receipt"]["status"] == 1: gas_used = update_result["receipt"]["gasUsed"] gas_price = tracked_order.gas_price fee = Decimal(str(gas_used)) * Decimal( str(gas_price)) / Decimal(str(1e9)) self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(str(tracked_order.price)), Decimal(str(tracked_order.amount)), TradeFee(0.0, [(tracked_order.fee_asset, Decimal(str(fee)))]), exchange_trade_id=order_id)) tracked_order.last_state = "FILLED" self.logger().info( f"The {tracked_order.trade_type.name} order " f"{tracked_order.client_order_id} has completed " f"according to order status API.") event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY \ else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY \ else SellOrderCompletedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, tracked_order.client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, float(fee), tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id) else: self.logger().info( f"The market order {tracked_order.client_order_id} has failed according to order status API. " ) self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent( self.current_timestamp, tracked_order.client_order_id, tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id)
async def _update_order_status(self): last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL if current_tick > last_tick and len(self._in_flight_orders) > 0: tracked_orders = list(self._in_flight_orders.values()) tasks = [self.request(path="/fapi/v1/order", params={ "symbol": convert_to_exchange_trading_pair(order.trading_pair), "origClientOrderId": order.client_order_id }, method=MethodType.GET, add_timestamp=True, is_signed=True, return_err=True) for order 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 client_order_id not in self._in_flight_orders: continue if isinstance(order_update, Exception): # NO_SUCH_ORDER code if order_update["code"] == -2013 or order_update["msg"] == "Order does not exist.": 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.ORDER_NOT_EXIST_CONFIRMATION_COUNT: continue self.trigger_event( self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type) ) self.stop_tracking_order(client_order_id) else: self.logger().network(f"Error fetching status update for the order {client_order_id}: " f"{order_update}.") continue tracked_order.last_state = order_update.get("status") order_type = OrderType.LIMIT if order_update.get("type") == "LIMIT" else OrderType.MARKET executed_amount_base = Decimal(order_update.get("executedQty", "0")) executed_amount_quote = Decimal(order_update.get("cumQuote", "0")) if tracked_order.is_done: if not tracked_order.is_failure: event_tag = None event_class = None if tracked_order.trade_type is TradeType.BUY: event_tag = self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG event_class = BuyOrderCompletedEvent else: event_tag = self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG event_class = SellOrderCompletedEvent self.logger().info(f"The {order_type.name.lower()} {tracked_order.trade_type.name.lower()} order {client_order_id} has " f"completed according to order status API.") self.trigger_event(event_tag, event_class(self.current_timestamp, client_order_id, tracked_order.base_asset, tracked_order.quote_asset, (tracked_order.fee_asset or tracked_order.base_asset), executed_amount_base, executed_amount_quote, tracked_order.fee_paid, order_type)) else: if tracked_order.is_cancelled: self.logger().info(f"Successfully cancelled order {client_order_id} according to order status API.") self.trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG, OrderCancelledEvent(self.current_timestamp, client_order_id)) else: self.logger().info(f"The {order_type.name.lower()} order {client_order_id} has failed according to " f"order status API.") self.trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, client_order_id, order_type)) self.stop_tracking_order(client_order_id)
async def _user_stream_event_listener(self): async for event_message in self._iter_user_event_queue(): try: event_type = event_message.get("e") if event_type == "ORDER_TRADE_UPDATE": order_message = event_message.get("o") client_order_id = order_message.get("c") # If the order has already been cancelled if client_order_id not in self._in_flight_orders: continue tracked_order = self._in_flight_orders.get(client_order_id) tracked_order.update_with_execution_report(event_message) # Execution Type: Trade => Filled trade_type = TradeType.BUY if order_message.get("S") == "BUY" else TradeType.SELL if order_message.get("X") in ["PARTIALLY_FILLED", "FILLED"]: order_filled_event = OrderFilledEvent( timestamp=event_message.get("E") * 1e-3, order_id=client_order_id, trading_pair=convert_from_exchange_trading_pair(order_message.get("s")), trade_type=trade_type, order_type=OrderType.LIMIT if order_message.get("o") == "LIMIT" else OrderType.MARKET, price=Decimal(order_message.get("L")), amount=Decimal(order_message.get("l")), trade_fee=self.get_fee( base_currency=tracked_order.base_asset, quote_currency=tracked_order.quote_asset, order_type=tracked_order.order_type, order_side=trade_type, amount=Decimal(order_message.get("q")), price=Decimal(order_message.get("p")) ), exchange_trade_id=order_message.get("t") ) self.trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, order_filled_event) if tracked_order.is_done: if not tracked_order.is_failure: event_tag = None event_class = None if trade_type is TradeType.BUY: event_tag = self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG event_class = BuyOrderCompletedEvent else: event_tag = self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG event_class = SellOrderCompletedEvent self.logger().info(f"The {tracked_order.order_type.name.lower()} {trade_type} order {client_order_id} has completed " f"according to websocket delta.") self.trigger_event(event_tag, event_class(self.current_timestamp, client_order_id, tracked_order.base_asset, tracked_order.quote_asset, (tracked_order.fee_asset or tracked_order.quote_asset), tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type)) else: if tracked_order.is_cancelled: if tracked_order.client_order_id in self._in_flight_orders: self.logger().info(f"Successfully cancelled order {tracked_order.client_order_id} according to websocket delta.") self.trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG, OrderCancelledEvent(self.current_timestamp, tracked_order.client_order_id)) else: self.logger().info(f"The {tracked_order.order_type.name.lower()} order {tracked_order.client_order_id} has failed " f"according to websocket delta.") self.trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, tracked_order.client_order_id, tracked_order.order_type)) self.stop_tracking_order(tracked_order.client_order_id) elif event_type == "ACCOUNT_UPDATE": update_data = event_message.get("a", {}) # update balances for asset in update_data.get("B", []): asset_name = asset["a"] self._account_balances[asset_name] = Decimal(asset["wb"]) self._account_available_balances[asset_name] = Decimal(asset["cw"]) # update position for asset in update_data.get("P", []): position = self._account_positions.get(f"{asset['s']}{asset['ps']}", None) if position is not None: position.update_position(position_side=PositionSide[asset["ps"]], unrealized_pnl = Decimal(asset["up"]), entry_price = Decimal(asset["ep"]), amount = Decimal(asset["pa"])) else: await self._update_positions() elif event_type == "MARGIN_CALL": positions = event_message.get("p", []) total_maint_margin_required = 0 # total_pnl = 0 negative_pnls_msg = "" for position in positions: existing_position = self._account_positions.get(f"{asset['s']}{asset['ps']}", None) if existing_position is not None: existing_position.update_position(position_side=PositionSide[asset["ps"]], unrealized_pnl = Decimal(asset["up"]), amount = Decimal(asset["pa"])) total_maint_margin_required += position.get("mm", 0) if position.get("up", 0) < 1: negative_pnls_msg += f"{position.get('s')}: {position.get('up')}, " self.logger().warning("Margin Call: Your position risk is too high, and you are at risk of " "liquidation. Close your positions or add additional margin to your wallet.") self.logger().info(f"Margin Required: {total_maint_margin_required}. Total Unrealized PnL: " f"{negative_pnls_msg}. Negative PnL assets: {negative_pnls_msg}.") except asyncio.CancelledError: raise except Exception as e: self.logger().error(f"Unexpected error in user stream listener loop: {e}", exc_info=True) await asyncio.sleep(5.0)
async def create_order(self, trade_type: TradeType, order_id: str, trading_pair: str, amount: Decimal, order_type: OrderType, position_action: PositionAction, price: Optional[Decimal] = Decimal("NaN")): trading_rule: TradingRule = self._trading_rules[trading_pair] if position_action not in [PositionAction.OPEN, PositionAction.CLOSE]: raise ValueError("Specify either OPEN_POSITION or CLOSE_POSITION position_action.") amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) if amount < trading_rule.min_order_size: raise ValueError(f"Buy order amount {amount} is lower than the minimum order size " f"{trading_rule.min_order_size}") order_result = None api_params = {"symbol": convert_to_exchange_trading_pair(trading_pair), "side": "BUY" if trade_type is TradeType.BUY else "SELL", "type": "LIMIT" if order_type is OrderType.LIMIT else "MARKET", "quantity": f"{amount}", "newClientOrderId": order_id } if order_type == OrderType.LIMIT: api_params["price"] = f"{price}" api_params["timeInForce"] = "GTC" if self._position_mode == PositionMode.HEDGE: if position_action == PositionAction.OPEN: api_params["positionSide"] = "LONG" if trade_type is TradeType.BUY else "SHORT" else: api_params["positionSide"] = "SHORT" if trade_type is TradeType.BUY else "LONG" self.start_tracking_order(order_id, "", trading_pair, trade_type, price, amount, order_type) try: order_result = await self.request(path="/fapi/v1/order", params=api_params, method=MethodType.POST, add_timestamp = True, is_signed=True) exchange_order_id = str(order_result["orderId"]) tracked_order = self._in_flight_orders.get(order_id) if tracked_order is not None: self.logger().info(f"Created {order_type.name.lower()} {trade_type.name.lower()} order {order_id} for " f"{amount} {trading_pair}.") tracked_order.exchange_order_id = exchange_order_id event_tag = self.MARKET_BUY_ORDER_CREATED_EVENT_TAG if trade_type is TradeType.BUY \ else self.MARKET_SELL_ORDER_CREATED_EVENT_TAG event_class = BuyOrderCreatedEvent if trade_type is TradeType.BUY else SellOrderCreatedEvent self.trigger_event(event_tag, event_class(self.current_timestamp, order_type, trading_pair, amount, price, order_id)) return order_result except asyncio.CancelledError: raise except Exception as e: self.stop_tracking_order(order_id) self.logger().network( f"Error submitting order to Binance Perpetuals for {amount} {trading_pair} " f"{'' if order_type is OrderType.MARKET else price}.", exc_info=True, app_warning_msg=str(e) ) self.trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG, MarketOrderFailureEvent(self.current_timestamp, order_id, order_type))
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) """ exchange_order_id = order_msg.orderId client_order_id = None for in_flight_order in self._in_flight_orders.values(): if in_flight_order.exchange_order_id == exchange_order_id: client_order_id = in_flight_order.client_order_id if client_order_id is None: return tracked_order: AscendExInFlightOrder = self._in_flight_orders[ client_order_id] # This could happen for Ack request type when placing new order, we don't know if the order is open until # we get order status update if tracked_order.is_locally_new and AscendExInFlightOrder.is_open_status( order_msg.status): self.trigger_order_created_event(tracked_order) tracked_order.update_status(order_msg.status) if tracked_order.executed_amount_base != Decimal( order_msg.cumFilledQty): # Update the relevant order information when there is fill event new_filled_amount = Decimal( order_msg.cumFilledQty) - tracked_order.executed_amount_base new_fee_paid = Decimal(order_msg.cumFee) - tracked_order.fee_paid tracked_order.executed_amount_base = Decimal( order_msg.cumFilledQty) tracked_order.executed_amount_quote = Decimal( order_msg.avgPx) * tracked_order.executed_amount_base tracked_order.fee_paid = Decimal(order_msg.cumFee) tracked_order.fee_asset = order_msg.feeAsset self.trigger_event( MarketEvent.OrderFilled, OrderFilledEvent( self.current_timestamp, client_order_id, tracked_order.trading_pair, tracked_order.trade_type, tracked_order.order_type, Decimal(order_msg.avgPx), new_filled_amount, TradeFee(0.0, [(tracked_order.fee_asset, new_fee_paid)]), exchange_order_id)) if tracked_order.is_cancelled: self.logger().info( f"Successfully cancelled order {client_order_id}.") self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, client_order_id, exchange_order_id)) tracked_order.cancelled_event.set() self.stop_tracking_order(client_order_id) elif tracked_order.is_failure: self.logger().info( f"Order {client_order_id} has failed according to order status API. " f"API order response: {order_msg}") self.trigger_event( MarketEvent.OrderFailure, MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)) self.stop_tracking_order(client_order_id) elif tracked_order.is_filled: event_tag = MarketEvent.BuyOrderCompleted if tracked_order.trade_type is TradeType.BUY else MarketEvent.SellOrderCompleted event_class = BuyOrderCompletedEvent if tracked_order.trade_type is TradeType.BUY else SellOrderCompletedEvent self.trigger_event( event_tag, event_class(self.current_timestamp, client_order_id, tracked_order.base_asset, tracked_order.quote_asset, tracked_order.fee_asset, tracked_order.executed_amount_base, tracked_order.executed_amount_quote, tracked_order.fee_paid, tracked_order.order_type, exchange_order_id)) self.stop_tracking_order(client_order_id)