async def _subscribe_to_order_book_streams(self) -> aiohttp.ClientWebSocketResponse: try: trading_pairs = ",".join([ convert_to_exchange_trading_pair(trading_pair) for trading_pair in self._trading_pairs ]) subscription_payloads = [ { "op": CONSTANTS.SUB_ENDPOINT_NAME, "ch": f"{topic}:{trading_pairs}" } for topic in [self.DIFF_TOPIC_ID, self.TRADE_TOPIC_ID] ] ws = await aiohttp.ClientSession().ws_connect(url=CONSTANTS.WS_URL, heartbeat=self.HEARTBEAT_PING_INTERVAL) for payload in subscription_payloads: async with self._throttler.execute_task(CONSTANTS.SUB_ENDPOINT_NAME): await ws.send_json(payload) self.logger().info(f"Subscribed to {self._trading_pairs} orderbook trading and delta streams...") return ws except asyncio.CancelledError: raise except Exception: self.logger().error("Unexpected error occurred subscribing to order book trading and delta streams...") raise
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. """ cancellation_results = [] try: tracked_orders: Dict[ str, AscendExInFlightOrder] = self._in_flight_orders.copy() api_params = { "orders": [{ 'id': ascend_ex_utils.uuid32(), "orderId": await order.get_exchange_order_id(), "symbol": ascend_ex_utils.convert_to_exchange_trading_pair( order.trading_pair), "time": int(time.time() * 1e3) } for order in tracked_orders.values()] } await self._api_request(method="delete", path_url=CONSTANTS.ORDER_BATCH_PATH_URL, data=api_params, is_auth_required=True, force_auth_path_url="order/batch") open_orders = await self.get_open_orders() for cl_order_id, tracked_order in tracked_orders.items(): 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 AscendEx. Check API key and network connection." ) return cancellation_results
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)
async def listen_for_order_book_diffs(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): while True: try: trading_pairs = ",".join( list( map( lambda trading_pair: convert_to_exchange_trading_pair(trading_pair), self._trading_pairs))) ch = f"depth:{trading_pairs}" payload = {"op": CONSTANTS.SUB_ENDPOINT_NAME, "ch": ch} async with websockets.connect(CONSTANTS.WS_URL) as ws: ws: websockets.WebSocketClientProtocol = ws async with self._throttler.execute_task( CONSTANTS.SUB_ENDPOINT_NAME): await ws.send(ujson.dumps(payload)) async for raw_msg in self._inner_messages(ws): msg = ujson.loads(raw_msg) if msg is None: continue if msg.get("m", '') == "ping": async with self._throttler.execute_task( CONSTANTS.PONG_ENDPOINT_NAME): await ws.send( ujson.dumps( dict(op=CONSTANTS.PONG_ENDPOINT_NAME))) if msg.get("m", '') == "depth": msg_timestamp: int = msg.get("data").get("ts") trading_pair: str = convert_from_exchange_trading_pair( msg.get("symbol")) order_book_message: OrderBookMessage = AscendExOrderBook.diff_message_from_exchange( msg.get("data"), msg_timestamp, metadata={"trading_pair": trading_pair}) output.put_nowait(order_book_message) except asyncio.CancelledError: raise except Exception as e: self.logger().debug(str(e)) self.logger().error( "Unexpected error with WebSocket connection. Retrying after 30 seconds...", exc_info=True) await asyncio.sleep(30.0)
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 api_params = { "symbol": ascend_ex_utils.convert_to_exchange_trading_pair(trading_pair), "orderId": ex_order_id, "time": ascend_ex_utils.get_ms_timestamp() } await self._api_request(method="delete", path_url=CONSTANTS.ORDER_PATH_URL, data=api_params, is_auth_required=True, force_auth_path_url="order") return order_id except asyncio.CancelledError: raise except Exception as e: if str(e).find("Order not found") != -1: self.stop_tracking_order(order_id) return 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 AscendEx. " f"Check API key and network connection.")
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. """ order_ids_to_cancel = [] cancel_payloads = [] successful_cancellations = [] failed_cancellations = [] for order in filter(lambda active_order: not active_order.is_done, self._in_flight_order_tracker.active_orders.values()): if order.exchange_order_id is not None: cancel_payloads.append({ "id": ascend_ex_utils.uuid32(), "orderId": order.exchange_order_id, "symbol": ascend_ex_utils.convert_to_exchange_trading_pair(order.trading_pair), "time": int(time.time() * 1e3), }) order_ids_to_cancel.append(order.client_order_id) else: failed_cancellations.append(CancellationResult(order.client_order_id, False)) if cancel_payloads: try: api_params = {"orders": cancel_payloads} await self._api_request( method="delete", path_url=CONSTANTS.ORDER_BATCH_PATH_URL, data=api_params, is_auth_required=True, force_auth_path_url="order/batch", ) successful_cancellations = [CancellationResult(order_id, True) for order_id in order_ids_to_cancel] except Exception: self.logger().network( "Failed to cancel all orders.", exc_info=True, app_warning_msg="Failed to cancel all orders on AscendEx. Check API key and network connection.", ) return successful_cancellations + failed_cancellations
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 listen_for_trades(self, ev_loop: asyncio.BaseEventLoop, output: asyncio.Queue): while True: try: trading_pairs = ",".join( list( map( lambda trading_pair: convert_to_exchange_trading_pair(trading_pair), self._trading_pairs))) payload = {"op": "sub", "ch": f"trades:{trading_pairs}"} async with websockets.connect(WS_URL) as ws: ws: websockets.WebSocketClientProtocol = ws await ws.send(ujson.dumps(payload)) async for raw_msg in self._inner_messages(ws): try: msg = ujson.loads(raw_msg) if (msg is None or msg.get("m") != "trades"): continue trading_pair: str = convert_from_exchange_trading_pair( msg.get("symbol")) for trade in msg.get("data"): trade_timestamp: int = trade.get("ts") trade_msg: OrderBookMessage = AscendExOrderBook.trade_message_from_exchange( trade, trade_timestamp, metadata={"trading_pair": trading_pair}) output.put_nowait(trade_msg) except Exception: raise except asyncio.CancelledError: raise except Exception as e: self.logger().debug(str(e)) self.logger().error( "Unexpected error with WebSocket connection. Retrying after 30 seconds...", exc_info=True) await asyncio.sleep(30.0)
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 """ try: tracked_order = self._in_flight_order_tracker.fetch_tracked_order(order_id) if tracked_order is None: non_tracked_order = self._in_flight_order_tracker.fetch_cached_order(order_id) if non_tracked_order is None: raise ValueError(f"Failed to cancel order - {order_id}. Order not found.") else: self.logger().info(f"The order {order_id} was finished before being cancelled") else: ex_order_id = await tracked_order.get_exchange_order_id() api_params = { "symbol": ascend_ex_utils.convert_to_exchange_trading_pair(trading_pair), "orderId": ex_order_id, "time": ascend_ex_utils.get_ms_timestamp(), } await self._api_request( method="delete", path_url=CONSTANTS.ORDER_PATH_URL, data=api_params, is_auth_required=True, force_auth_path_url="order", ) return order_id except asyncio.CancelledError: raise except asyncio.TimeoutError: self._stop_tracking_order_exceed_no_exchange_id_limit(tracked_order=tracked_order) except Exception as e: self.logger().error( f"Failed to cancel order {order_id}: {str(e)}", exc_info=True, )
def test_convert_to_exchange_trading_pair(self): trading_pair = "BTC-USDT" self.assertEqual("BTC/USDT", utils.convert_to_exchange_trading_pair(trading_pair))
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 (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}") ascend_ex_trading_rule = self._ascend_ex_trading_rules[trading_pair] amount = self.quantize_order_amount(trading_pair, amount) price = self.quantize_order_price(trading_pair, price) try: # ascend_ex has a unique way of determening if the order has enough "worth" to be posted # see https://ascendex.github.io/ascendex-pro-api/#place-order notional = Decimal(price * amount) if notional < ascend_ex_trading_rule.minNotional or notional > ascend_ex_trading_rule.maxNotional: raise ValueError( f"Notional amount {notional} is not withing the range of {ascend_ex_trading_rule.minNotional}-{ascend_ex_trading_rule.maxNotional}." ) # TODO: check balance [exchange_order_id, timestamp ] = ascend_ex_utils.gen_exchange_order_id(self._account_uid, order_id) api_params = { "id": exchange_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": trade_type.name } self.start_tracking_order(order_id, exchange_order_id, trading_pair, trade_type, price, amount, order_type) await self._api_request("post", "cash/order", api_params, True, force_auth_path_url="order") 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}.") 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, exchange_order_id=exchange_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 AscendEx 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, 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() # Order UUID is strictly used to enable AscendEx to construct a unique(still questionable) exchange_order_id order_uuid = f"{ascend_ex_utils.HBOT_BROKER_ID}-{ascend_ex_utils.uuid32()}"[:32] api_params = { "id": order_uuid, "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=order_id, trading_pair=trading_pair, trade_type=trade_type, price=price, amount=amount, order_type=order_type, ) try: 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", ) resp_status = resp["data"]["status"].upper() order_data = resp["data"]["info"] if resp_status == "ACK": # Ack request status means the server has received the request return order_update = None if resp_status == "ACCEPT": order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, exchange_order_id=str(order_data["orderId"]), trading_pair=trading_pair, update_timestamp=order_data["lastExecTime"] * 1e-3, new_state=OrderState.OPEN, ) elif resp_status == "DONE": order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, exchange_order_id=str(order_data["orderId"]), trading_pair=trading_pair, update_timestamp=order_data["lastExecTime"] * 1e-3, new_state=CONSTANTS.ORDER_STATE[order_data["status"]], ) elif resp_status == "ERR": order_update: OrderUpdate = OrderUpdate( client_order_id=order_id, exchange_order_id=str(order_data["orderId"]), trading_pair=trading_pair, update_timestamp=order_data["lastExecTime"] * 1e-3, new_state=OrderState.FAILED, ) self._in_flight_order_tracker.process_order_update(order_update) except IOError: self.logger().exception(f"The request to create the order {order_id} failed") self.stop_tracking_order(order_id) except asyncio.CancelledError: raise except Exception: msg = (f"Error submitting {trade_type.name} {order_type.name} order to AscendEx for " f"{amount} {trading_pair} {price}.") self.logger().exception(msg)
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: # TODO: check balance [exchange_order_id, timestamp ] = ascend_ex_utils.gen_exchange_order_id(self._account_uid, order_id) api_params = { "id": exchange_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": trade_type.name } self.start_tracking_order(order_id, exchange_order_id, trading_pair, trade_type, price, amount, order_type) await self._api_request(method="post", path_url="cash/order", params=api_params, is_auth_required=True, force_auth_path_url="order") 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}.") 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, exchange_order_id=exchange_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 AscendEx 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))