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 / self.UPDATE_ORDER_STATUS_MIN_INTERVAL) current_tick = int(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 = [] for tracked_order in tracked_orders: order_id = await tracked_order.get_exchange_order_id() trading_pair = tracked_order.trading_pair tasks.append(self._api_request("get", CONSTANTS.GET_ORDER_DETAIL_PATH_URL, {"order_id": int(order_id), "symbol": bitmart_utils.convert_to_exchange_trading_pair(trading_pair)}, "KEYED" )) self.logger().debug(f"Polling for order status updates of {len(tasks)} orders.") responses = await safe_gather(*tasks, return_exceptions=True) for response in responses: if isinstance(response, Exception): raise response if "data" not in response: self.logger().info(f"_update_order_status data not in resp: {response}") continue result = response["data"] await self._process_trade_message_rest(result) await self._process_order_message(result)
async def cancel_all(self, timeout_seconds: float): """ Cancels all in-flight orders and waits for cancellation results. Used by bot's top level stop and exit commands (cancelling outstanding orders on exit) :param timeout_seconds: The timeout at which the operation will be canceled. :returns List of CancellationResult which indicates whether each order is successfully cancelled. """ if self._trading_pairs is None: raise Exception( "cancel_all can only be used when trading_pairs are specified." ) for order in self._in_flight_orders.values(): await order.get_exchange_order_id() tracked_orders: Dict[ str, BitmartInFlightOrder] = self._in_flight_orders.copy().items() cancellation_results = [] try: tasks = [] for _, order in tracked_orders: api_params = { "symbol": bitmart_utils.convert_to_exchange_trading_pair( order.trading_pair), "order_id": int(order.exchange_order_id), } tasks.append( self._api_request("post", CONSTANTS.CANCEL_ORDER_PATH_URL, api_params, "SIGNED")) await safe_gather(*tasks) open_orders = await self.get_open_orders() for cl_order_id, tracked_order in tracked_orders: open_order = [ o for o in open_orders if o.client_order_id == cl_order_id ] if not open_order: cancellation_results.append( CancellationResult(cl_order_id, True)) self.trigger_event( MarketEvent.OrderCancelled, OrderCancelledEvent(self.current_timestamp, cl_order_id)) self.stop_tracking_order(cl_order_id) else: cancellation_results.append( CancellationResult(cl_order_id, False)) except Exception: self.logger().network( "Failed to cancel all orders.", exc_info=True, app_warning_msg= "Failed to cancel all orders on BitMart. Check API key and network connection." ) return cancellation_results
def test_trading_pair_convertion(self): hbot_trading_pair = "BTC-USDT" exchange_trading_pair = "BTC_USDT" self.assertEqual( exchange_trading_pair, utils.convert_to_exchange_trading_pair(hbot_trading_pair)) self.assertEqual( hbot_trading_pair, utils.convert_from_exchange_trading_pair(exchange_trading_pair))
async def get_open_orders(self) -> List[OpenOrder]: if self._trading_pairs is None: raise Exception("get_open_orders can only be used when trading_pairs are specified.") page_len = 100 responses = [] for trading_pair in self._trading_pairs: page = 1 while True: response = await self._api_request("get", CONSTANTS.GET_OPEN_ORDERS_PATH_URL, {"symbol": bitmart_utils.convert_to_exchange_trading_pair(trading_pair), "offset": page, "limit": page_len, "status": "9"}, "KEYED") responses.append(response) count = len(response["data"]["orders"]) if count < page_len: break else: page += 1 for order in self._in_flight_orders.values(): await order.get_exchange_order_id() ret_val = [] for response in responses: for order in response["data"]["orders"]: exchange_order_id = str(order["order_id"]) tracked_orders = list(self._in_flight_orders.values()) tracked_order = [o for o in tracked_orders if exchange_order_id == o.exchange_order_id] if not tracked_order: continue tracked_order = tracked_order[0] if order["type"] != "limit": raise Exception(f"Unsupported order type {order['type']}") ret_val.append( OpenOrder( client_order_id=tracked_order.client_order_id, trading_pair=bitmart_utils.convert_from_exchange_trading_pair(order["symbol"]), price=Decimal(str(order["price"])), amount=Decimal(str(order["size"])), executed_amount=Decimal(str(order["filled_size"])), status=CONSTANTS.ORDER_STATUS[int(order["status"])], order_type=OrderType.LIMIT, is_buy=True if order["side"].lower() == "buy" else False, time=int(order["create_time"]), exchange_order_id=str(order["order_id"]) ) ) return ret_val
async def _execute_cancel(self, trading_pair: str, order_id: str) -> str: """ Executes order cancellation process by first calling cancel-order API. The API result doesn't confirm whether the cancellation is successful, it simply states it receives the request. :param trading_pair: The market trading pair :param order_id: The internal order id order.last_state to change to CANCELED """ try: tracked_order = self._in_flight_orders.get(order_id) if tracked_order is None: raise ValueError( f"Failed to cancel order - {order_id}. Order not found.") if tracked_order.exchange_order_id is None: await tracked_order.get_exchange_order_id() ex_order_id = tracked_order.exchange_order_id response = await self._api_request( "post", CONSTANTS.CANCEL_ORDER_PATH_URL, { "symbol": bitmart_utils.convert_to_exchange_trading_pair( trading_pair), "order_id": int(ex_order_id) }, "SIGNED") # result = True is a successful cancel, False indicates fcancel failed due to already cancelled or matched if "result" in response["data"] and not response["data"]["result"]: raise ValueError( f"Failed to cancel order - {order_id}. Order was already matched or cancelled on the exchange." ) return order_id except asyncio.CancelledError: raise 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 Bitmart. " f"Check API key and network connection.")
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] try: 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 = {"symbol": bitmart_utils.convert_to_exchange_trading_pair(trading_pair), "side": trade_type.name.lower(), "type": "limit", "size": f"{amount:f}", "price": f"{price:f}" } self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type ) order_result = await self._api_request("post", CONSTANTS.CREATE_ORDER_PATH_URL, api_params, "SIGNED") exchange_order_id = str(order_result["data"]["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, tracked_order.creation_timestamp )) 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 BitMart 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 get_open_orders(self) -> List[OpenOrder]: """ Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by BitmartAPIUserStreamDataSource. """ if self._trading_pairs is None: raise Exception( "get_open_orders can only be used when trading_pairs are specified." ) tasks = [] for trading_pair in self._trading_pairs: for page in range(1, 6): tasks.append( self._api_request( "get", CONSTANTS.GET_OPEN_ORDERS_PATH_URL, { "symbol": bitmart_utils.convert_to_exchange_trading_pair( trading_pair), "offset": page, "limit": 100, "status": "9" }, "KEYED")) responses = await safe_gather(*tasks, return_exceptions=True) for order in self._in_flight_orders.values(): await order.get_exchange_order_id() ret_val = [] for response in responses: for order in response["data"]["orders"]: exchange_order_id = str(order["order_id"]) tracked_orders = list(self._in_flight_orders.values()) tracked_order = [ o for o in tracked_orders if exchange_order_id == o.exchange_order_id ] if not tracked_order: continue tracked_order = tracked_order[0] if order["type"] != "limit": raise Exception(f"Unsupported order type {order['type']}") ret_val.append( OpenOrder( client_order_id=tracked_order.client_order_id, trading_pair=bitmart_utils. convert_from_exchange_trading_pair(order["symbol"]), price=Decimal(str(order["price"])), amount=Decimal(str(order["size"])), executed_amount=Decimal(str(order["filled_size"])), status=CONSTANTS.ORDER_STATUS[int(order["status"])], order_type=OrderType.LIMIT, is_buy=True if order["side"].lower() == "buy" else False, time=int(order["create_time"]), exchange_order_id=str(order["order_id"]))) return ret_val