Example #1
0
async def api_call_with_retries(request: CoinflexRESTRequest,
                                rest_assistant: RESTAssistant,
                                throttler: AsyncThrottler,
                                logger: logging.Logger = None,
                                try_count: int = 0) -> Dict[str, Any]:

    async with throttler.execute_task(limit_id=request.throttler_limit_id):
        http_status, resp, request_errors = await rest_response_with_errors(rest_assistant, request)

    if isinstance(resp, dict):
        _extract_error_from_response(resp)

    if request_errors or resp is None:
        if try_count < CONSTANTS.API_MAX_RETRIES and not request.disable_retries:
            try_count += 1
            time_sleep = retry_sleep_time(try_count)

            suppress_msgs = ['Forbidden']

            err_msg = (f"Error fetching data from {request.url}. HTTP status is {http_status}. "
                       f"Retrying in {time_sleep:.0f}s. {resp or ''}")

            if (resp is not None and resp not in suppress_msgs) or try_count > 1:
                if logger:
                    logger.network(err_msg)
                else:
                    print(err_msg)
            elif logger:
                logger.debug(err_msg, exc_info=True)
            await asyncio.sleep(time_sleep)
            return await api_call_with_retries(request=request, rest_assistant=rest_assistant, throttler=throttler,
                                               logger=logger, try_count=try_count)
        else:
            raise CoinflexAPIError({"errors": resp, "status": http_status})
    return resp
 async def _get_last_traded_price(cls, trading_pair: str,
                                  throttler: AsyncThrottler) -> float:
     url = (f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}"
            f"?pair={convert_to_exchange_trading_pair(trading_pair)}")
     async with aiohttp.ClientSession() as client:
         async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL):
             resp = await client.get(url)
         resp_json = await resp.json()
         record = list(resp_json["result"].values())[0]
         return float(record["c"][0])
    async def _get_last_traded_price(cls, trading_pair: str,
                                     throttler: AsyncThrottler) -> float:
        url = (f"{CONSTANTS.BASE_URL}{CONSTANTS.TICKER_PATH_URL}"
               f"?pair={convert_to_exchange_trading_pair(trading_pair)}")

        request = RESTRequest(method=RESTMethod.GET, url=url)
        rest_assistant = await build_api_factory().get_rest_assistant()

        async with throttler.execute_task(CONSTANTS.TICKER_PATH_URL):
            resp = await rest_assistant.call(request)
        resp_json = await resp.json()
        record = list(resp_json["result"].values())[0]
        return float(record["c"][0])
Example #4
0
async def api_call_with_retries(request: GateIORESTRequest,
                                rest_assistant: RESTAssistant,
                                throttler: AsyncThrottler,
                                logger: logging.Logger,
                                gate_io_auth: Optional[GateIoAuth] = None,
                                try_count: int = 0) -> Dict[str, Any]:
    headers = {"Content-Type": "application/json"}

    async with throttler.execute_task(limit_id=request.throttler_limit_id):
        if request.is_auth_required:
            if gate_io_auth is None:
                raise RuntimeError(
                    f"Authentication required for request, but no GateIoAuth object supplied."
                    f" Request: {request}.")
            auth_params = request.data if request.method == RESTMethod.POST else request.params
            request.data = auth_params
            headers: dict = gate_io_auth.get_headers(str(request.method),
                                                     request.auth_url,
                                                     auth_params)
        request.headers = headers
        response_coro = asyncio.wait_for(rest_assistant.call(request),
                                         CONSTANTS.API_CALL_TIMEOUT)
        http_status, parsed_response, request_errors = await rest_response_with_errors(
            response_coro)

    if request_errors or parsed_response is None:
        if try_count < CONSTANTS.API_MAX_RETRIES:
            try_count += 1
            time_sleep = retry_sleep_time(try_count)
            logger.info(
                f"Error fetching data from {request.url}. HTTP status is {http_status}."
                f" Retrying in {time_sleep:.0f}s.")
            await _sleep(time_sleep)
            return await api_call_with_retries(request, rest_assistant,
                                               throttler, logger, gate_io_auth,
                                               try_count)
        else:
            raise GateIoAPIError({
                "label": "HTTP_ERROR",
                "message": parsed_response,
                "status": http_status
            })

    if "message" in parsed_response:
        raise GateIoAPIError(parsed_response)

    return parsed_response
Example #5
0
    async def _get_last_traded_price(cls, trading_pair: str, domain: str,
                                     rest_assistant: RESTAssistant,
                                     throttler: AsyncThrottler) -> float:

        url = binance_utils.public_rest_url(
            path_url=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL, domain=domain)
        symbol = await cls.exchange_symbol_associated_to_pair(
            trading_pair=trading_pair, domain=domain, throttler=throttler)
        request = RESTRequest(method=RESTMethod.GET,
                              url=f"{url}?symbol={symbol}")

        async with throttler.execute_task(
                limit_id=CONSTANTS.TICKER_PRICE_CHANGE_PATH_URL):
            resp: RESTResponse = await rest_assistant.call(request=request)
            if resp.status == 200:
                resp_json = await resp.json()
                return float(resp_json["lastPrice"])
Example #6
0
class BinanceExchange(ExchangeBase):
    SHORT_POLL_INTERVAL = 5.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    LONG_POLL_INTERVAL = 120.0

    MAX_ORDER_UPDATE_RETRIEVAL_RETRIES_WITH_FAILURES = 3

    def __init__(self,
                 binance_api_key: str,
                 binance_api_secret: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True,
                 domain="com"):
        self._domain = domain
        self._binance_time_synchronizer = TimeSynchronizer()
        super().__init__()
        self._trading_required = trading_required
        self._auth = BinanceAuth(api_key=binance_api_key,
                                 secret_key=binance_api_secret,
                                 time_provider=self._binance_time_synchronizer)
        self._api_factory = WebAssistantsFactory(auth=self._auth)
        self._rest_assistant = None
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._order_book_tracker = BinanceOrderBookTracker(
            trading_pairs=trading_pairs,
            domain=domain,
            api_factory=self._api_factory,
            throttler=self._throttler)
        self._user_stream_tracker = BinanceUserStreamTracker(
            auth=self._auth, domain=domain, throttler=self._throttler)
        self._ev_loop = asyncio.get_event_loop()
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._order_not_found_records = {
        }  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, TradingRule]
        self._trade_fees = {
        }  # Dict[trading_pair:str, (maker_fee_percent:Decimal, taken_fee_percent:Decimal)]
        self._last_update_trade_fees_timestamp = 0
        self._status_polling_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._last_poll_timestamp = 0
        self._last_trades_poll_binance_timestamp = 0
        self._order_tracker: ClientOrderTracker = ClientOrderTracker(
            connector=self)

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global s_logger
        if s_logger is None:
            s_logger = logging.getLogger(__name__)
        return s_logger

    @property
    def name(self) -> str:
        if self._domain == "com":
            return "binance"
        else:
            return f"binance_{self._domain}"

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, InFlightOrder]:
        return self._order_tracker.active_orders

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self.in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, any]:
        """
        Returns a dictionary associating current active orders client id to their JSON representation
        """
        return {
            key: value.to_json()
            for key, value in self.in_flight_orders.items()
        }

    @property
    def order_book_tracker(self) -> BinanceOrderBookTracker:
        return self._order_book_tracker

    @property
    def user_stream_tracker(self) -> BinanceUserStreamTracker:
        return self._user_stream_tracker

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        Returns a dictionary with the values of all the conditions that determine if the connector is ready to operate.
        The key of each entry is the condition name, and the value is True if condition is ready, False otherwise.
        """
        return {
            "symbols_mapping_initialized":
            BinanceAPIOrderBookDataSource.trading_pair_symbol_map_ready(
                domain=self._domain),
            "order_books_initialized":
            self._order_book_tracker.ready,
            "account_balance":
            len(self._account_balances) > 0
            if self._trading_required else True,
            "trading_rule_initialized":
            len(self._trading_rules) > 0,
        }

    @property
    def ready(self) -> bool:
        """
        Returns True if the connector is ready to operate (all connections established with the exchange). If it is
        not ready it returns False.
        """
        return all(self.status_dict.values())

    @staticmethod
    def binance_order_type(order_type: OrderType) -> str:
        return order_type.name.upper()

    @staticmethod
    def to_hb_order_type(binance_type: str) -> OrderType:
        return OrderType[binance_type]

    def supported_order_types(self):
        return [OrderType.LIMIT, OrderType.LIMIT_MAKER]

    async def start_network(self):
        """
        Start all required tasks to update the status of the connector. Those tasks include:
        - The order book tracker
        - The polling loop to update the trading rules
        - The polling loop to update order status and balance status using REST API (backup for main update process)
        - The background task to process the events received through the user stream tracker (websocket connection)
        """
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(
            self._trading_rules_polling_loop())
        if self._trading_required:
            self._status_polling_task = safe_ensure_future(
                self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(
                self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(
                self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is executed when the connector is stopped. It perform a general cleanup and stops all background
        tasks that require the connection with the exchange to work.
        """
        # Reset timestamps and _poll_notifier for status_polling_loop
        self._last_poll_timestamp = 0
        self._last_timestamp = 0
        self._poll_notifier = asyncio.Event()

        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
        self._status_polling_task = self._user_stream_tracker_task = self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        Checks connectivity with the exchange using the API
        """
        try:
            await self._api_request(
                method=RESTMethod.GET,
                path_url=CONSTANTS.PING_PATH_URL,
            )
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    def restore_tracking_states(self, saved_states: Dict[str, any]):
        """
        Restore in-flight orders from saved tracking states, this is st the connector can pick up on where it left off
        when it disconnects.
        :param saved_states: The saved tracking_states.
        """
        self._order_tracker.restore_tracking_states(
            tracking_states=saved_states)

    def tick(self, timestamp: float):
        """
        Includes the logic that has to be processed every time a new tick happens in the bot. Particularly it enables
        the execution of the status update polling loop using an event.
        """
        now = time.time()
        poll_interval = (self.SHORT_POLL_INTERVAL if
                         now - self.user_stream_tracker.last_recv_time > 60.0
                         else self.LONG_POLL_INTERVAL)
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)

        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_order_book(self, trading_pair: str) -> OrderBook:
        """
        Returns the current order book for a particular market
        :param trading_pair: the pair of tokens for which the order book should be retrieved
        """
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def start_tracking_order(self, order_id: str,
                             exchange_order_id: Optional[str],
                             trading_pair: str, trade_type: TradeType,
                             price: Decimal, amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by adding it to the order tracker.
        :param order_id: the order identifier
        :param exchange_order_id: the identifier for the order in the exchange
        :param trading_pair: the token pair for the operation
        :param trade_type: the type of order (buy or sell)
        :param price: the price for the order
        :param amount: the amount for the order
        :order type: type of execution for the order (MARKET, LIMIT, LIMIT_MAKER)
        """
        self._order_tracker.start_tracking_order(
            InFlightOrder(
                client_order_id=order_id,
                exchange_order_id=exchange_order_id,
                trading_pair=trading_pair,
                order_type=order_type,
                trade_type=trade_type,
                amount=amount,
                price=price,
            ))

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order
        :param order_id: The id of the order that will not be tracked any more
        """
        self._order_tracker.stop_tracking_order(client_order_id=order_id)

    def get_order_price_quantum(self, trading_pair: str,
                                price: Decimal) -> Decimal:
        """
        Used by quantize_order_price() in _create_order()
        Returns a price step, a minimum price increment for a given trading pair.
        :param trading_pair: the trading pair to check for market conditions
        :param price: the starting point price
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str,
                               order_size: Decimal) -> Decimal:
        """
        Used by quantize_order_price() in _create_order()
        Returns an order amount step, a minimum amount increment for a given trading pair.
        :param trading_pair: the trading pair to check for market conditions
        :param order_size: the starting point order price
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_base_amount_increment

    def quantize_order_amount(self,
                              trading_pair: str,
                              amount: Decimal,
                              price: Decimal = s_decimal_0) -> Decimal:
        """
        Applies the trading rules to calculate the correct order amount for the market
        :param trading_pair: the token pair for which the order will be created
        :param amount: the intended amount for the order
        :param price: the intended price for the order
        :return: the quantized order amount after applying the trading rules
        """
        trading_rule = self._trading_rules[trading_pair]
        quantized_amount: Decimal = super().quantize_order_amount(
            trading_pair, amount)

        # Check against min_order_size and min_notional_size. If not passing either check, return 0.
        if quantized_amount < trading_rule.min_order_size:
            return s_decimal_0

        if price == s_decimal_0:
            current_price: Decimal = self.get_price(trading_pair, False)
            notional_size = current_price * quantized_amount
        else:
            notional_size = price * quantized_amount

        # Add 1% as a safety factor in case the prices changed while making the order.
        if notional_size < trading_rule.min_notional_size * Decimal("1.01"):
            return s_decimal_0

        return quantized_amount

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN,
                is_maker: Optional[bool] = None) -> TradeFeeBase:
        """
        Calculates the estimated fee an order would pay based on the connector configuration
        :param base_currency: the order base currency
        :param quote_currency: the order quote currency
        :param order_type: the type of order (MARKET, LIMIT, LIMIT_MAKER)
        :param order_side: if the order is for buying or selling
        :param amount: the order amount
        :param price: the order price
        :return: the estimated fee for the order
        """
        """
        To get trading fee, this function is simplified by using fee override configuration. Most parameters to this
        function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for
        maker order.
        """
        is_maker = order_type is OrderType.LIMIT_MAKER
        return DeductedFromReturnsTradeFee(
            percent=self.estimate_fee_pct(is_maker))

    def buy(self,
            trading_pair: str,
            amount: Decimal,
            order_type: OrderType = OrderType.LIMIT,
            price: Decimal = s_decimal_NaN,
            **kwargs) -> str:
        """
        Creates a promise to create a buy order using the parameters.
        :param trading_pair: the token pair to operate with
        :param amount: the order amount
        :param order_type: the type of order to create (MARKET, LIMIT, LIMIT_MAKER)
        :param price: the order price
        :return: the id assigned by the connector to the order (the client id)
        """
        new_order_id = binance_utils.get_new_client_order_id(
            is_buy=True, trading_pair=trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.BUY, new_order_id, trading_pair,
                               amount, order_type, price))
        return new_order_id

    def sell(self,
             trading_pair: str,
             amount: Decimal,
             order_type: OrderType = OrderType.MARKET,
             price: Decimal = s_decimal_NaN,
             **kwargs) -> str:
        """
        Creates a promise to create a sell order using the parameters.
        :param trading_pair: the token pair to operate with
        :param amount: the order amount
        :param order_type: the type of order to create (MARKET, LIMIT, LIMIT_MAKER)
        :param price: the order price
        :return: the id assigned by the connector to the order (the client id)
        """
        order_id = binance_utils.get_new_client_order_id(
            is_buy=False, trading_pair=trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.SELL, order_id, trading_pair, amount,
                               order_type, price))
        return order_id

    def cancel(self, trading_pair: str, order_id: str):
        """
        Creates a promise to cancel an order in the exchange
        :param trading_pair: the trading pair the order to cancel operates with
        :param order_id: the client id of the order to cancel
        :return: the client id of the order to cancel
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    async def cancel_all(self,
                         timeout_seconds: float) -> List[CancellationResult]:
        """
        Cancels all currently active orders. The cancellations are performed in parallel tasks.
        :param timeout_seconds: the maximum time (in seconds) the cancel logic should run
        :return: a list of CancellationResult instances, one for each of the orders to be cancelled
        """
        incomplete_orders = [
            o for o in self.in_flight_orders.values() if not o.is_done
        ]
        tasks = [
            self._execute_cancel(o.trading_pair, o.client_order_id)
            for o in incomplete_orders
        ]
        order_id_set = set([o.client_order_id for o in incomplete_orders])
        successful_cancellations = []

        try:
            async with timeout(timeout_seconds):
                cancellation_results = await safe_gather(
                    *tasks, return_exceptions=True)
                for cr in cancellation_results:
                    if isinstance(cr, Exception):
                        continue
                    if isinstance(cr, dict) and "origClientOrderId" in cr:
                        client_order_id = cr.get("origClientOrderId")
                        order_id_set.remove(client_order_id)
                        successful_cancellations.append(
                            CancellationResult(client_order_id, True))
        except Exception:
            self.logger().network(
                "Unexpected error cancelling orders.",
                exc_info=True,
                app_warning_msg=
                "Failed to cancel order with Binance. Check API key and network connection."
            )

        failed_cancellations = [
            CancellationResult(oid, False) for oid in order_id_set
        ]
        return successful_cancellations + failed_cancellations

    async def _create_order(self,
                            trade_type: TradeType,
                            order_id: str,
                            trading_pair: str,
                            amount: Decimal,
                            order_type: OrderType,
                            price: Optional[Decimal] = Decimal("NaN")):
        """
        Creates a an order in the exchange using the parameters to configure it
        :param trade_type: the side of the order (BUY of SELL)
        :param order_id: the id that should be assigned to the order (the client id)
        :param trading_pair: the token pair to operate with
        :param amount: the order amount
        :param order_type: the type of order to create (MARKET, LIMIT, LIMIT_MAKER)
        :param price: the order price
        """
        trading_rule: TradingRule = self._trading_rules[trading_pair]
        price = self.quantize_order_price(trading_pair, price)
        quantize_amount_price = Decimal("0") if price.is_nan() else price
        amount = self.quantize_order_amount(trading_pair=trading_pair,
                                            amount=amount,
                                            price=quantize_amount_price)

        self.start_tracking_order(order_id=order_id,
                                  exchange_order_id=None,
                                  trading_pair=trading_pair,
                                  trade_type=trade_type,
                                  price=price,
                                  amount=amount,
                                  order_type=order_type)

        if amount < trading_rule.min_order_size:
            self.logger().warning(
                f"{trade_type.name.title()} order amount {amount} is lower than the minimum order"
                f" size {trading_rule.min_order_size}. The order will not be created."
            )
            order_update: OrderUpdate = OrderUpdate(
                client_order_id=order_id,
                trading_pair=trading_pair,
                update_timestamp=int(self.current_timestamp * 1e3),
                new_state=OrderState.FAILED,
            )
            self._order_tracker.process_order_update(order_update)
            return

        order_result = None
        amount_str = f"{amount:f}"
        price_str = f"{price:f}"
        type_str = BinanceExchange.binance_order_type(order_type)
        side_str = CONSTANTS.SIDE_BUY if trade_type is TradeType.BUY else CONSTANTS.SIDE_SELL
        symbol = await BinanceAPIOrderBookDataSource.exchange_symbol_associated_to_pair(
            trading_pair=trading_pair,
            domain=self._domain,
            api_factory=self._api_factory,
            throttler=self._throttler)
        api_params = {
            "symbol": symbol,
            "side": side_str,
            "quantity": amount_str,
            "type": type_str,
            "newClientOrderId": order_id,
            "price": price_str
        }
        if order_type == OrderType.LIMIT:
            api_params["timeInForce"] = CONSTANTS.TIME_IN_FORCE_GTC

        try:
            order_result = await self._api_request(
                method=RESTMethod.POST,
                path_url=CONSTANTS.ORDER_PATH_URL,
                data=api_params,
                is_auth_required=True)

            exchange_order_id = str(order_result["orderId"])

            order_update: OrderUpdate = OrderUpdate(
                client_order_id=order_id,
                exchange_order_id=exchange_order_id,
                trading_pair=trading_pair,
                update_timestamp=int(order_result["transactTime"]),
                new_state=OrderState.OPEN,
            )
            self._order_tracker.process_order_update(order_update)

        except asyncio.CancelledError:
            raise
        except Exception as e:
            self.logger().network(
                f"Error submitting {side_str} {type_str} order to Binance for "
                f"{amount} {trading_pair} "
                f"{price}.",
                exc_info=True,
                app_warning_msg=str(e))
            order_update: OrderUpdate = OrderUpdate(
                client_order_id=order_id,
                trading_pair=trading_pair,
                update_timestamp=int(self.current_timestamp * 1e3),
                new_state=OrderState.FAILED,
            )
            self._order_tracker.process_order_update(order_update)

    async def _execute_cancel(self, trading_pair: str, order_id: str):
        """
        Requests the exchange to cancel an active order
        :param trading_pair: the trading pair the order to cancel operates with
        :param order_id: the client id of the order to cancel
        """
        tracked_order = self._order_tracker.fetch_tracked_order(order_id)
        if tracked_order is not None:
            try:
                symbol = await BinanceAPIOrderBookDataSource.exchange_symbol_associated_to_pair(
                    trading_pair=trading_pair,
                    domain=self._domain,
                    api_factory=self._api_factory,
                    throttler=self._throttler)
                api_params = {
                    "symbol": symbol,
                    "origClientOrderId": order_id,
                }
                cancel_result = await self._api_request(
                    method=RESTMethod.DELETE,
                    path_url=CONSTANTS.ORDER_PATH_URL,
                    params=api_params,
                    is_auth_required=True)

                if cancel_result.get("status") == "CANCELED":
                    order_update: OrderUpdate = OrderUpdate(
                        client_order_id=order_id,
                        trading_pair=tracked_order.trading_pair,
                        update_timestamp=int(self.current_timestamp * 1e3),
                        new_state=OrderState.CANCELLED,
                    )
                    self._order_tracker.process_order_update(order_update)
                return cancel_result

            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().exception(
                    f"There was a an error when requesting cancellation of order {order_id}"
                )
                raise

    async def _status_polling_loop(self):
        """
        Performs all required operation to keep the connector updated and synchronized with the exchange.
        It contains the backup logic to update status using API requests in case the main update source (the user stream
        data source websocket) fails.
        It also updates the time synchronizer. This is necessary because Binance require the time of the client to be
        the same as the time in the exchange.
        Executes when the _poll_notifier event is enabled by the `tick` function.
        """
        while True:
            try:
                await self._poll_notifier.wait()
                await self._update_time_synchronizer()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_fills_from_trades(),
                )
                await self._update_order_status()
                self._last_poll_timestamp = self.current_timestamp
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unexpected error while fetching account updates.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch account updates from Binance. "
                    "Check API key and network connection.")
                await asyncio.sleep(0.5)
            finally:
                self._poll_notifier = asyncio.Event()

    async def _trading_rules_polling_loop(self):
        """
        Updates the trading rules by requesting the latest definitions from the exchange.
        Executes regularly every 30 minutes
        """
        while True:
            try:
                await safe_gather(self._update_trading_rules(), )
                await asyncio.sleep(30 * 60)
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unexpected error while fetching trading rules.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch new trading rules from Binance. "
                    "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        exchange_info = await self._api_request(
            method=RESTMethod.GET, path_url=CONSTANTS.EXCHANGE_INFO_PATH_URL)
        trading_rules_list = await self._format_trading_rules(exchange_info)
        self._trading_rules.clear()
        for trading_rule in trading_rules_list:
            self._trading_rules[trading_rule.trading_pair] = trading_rule

    async def _format_trading_rules(
            self, exchange_info_dict: Dict[str, Any]) -> List[TradingRule]:
        """
        Example:
        {
            "symbol": "ETHBTC",
            "baseAssetPrecision": 8,
            "quotePrecision": 8,
            "orderTypes": ["LIMIT", "MARKET"],
            "filters": [
                {
                    "filterType": "PRICE_FILTER",
                    "minPrice": "0.00000100",
                    "maxPrice": "100000.00000000",
                    "tickSize": "0.00000100"
                }, {
                    "filterType": "LOT_SIZE",
                    "minQty": "0.00100000",
                    "maxQty": "100000.00000000",
                    "stepSize": "0.00100000"
                }, {
                    "filterType": "MIN_NOTIONAL",
                    "minNotional": "0.00100000"
                }
            ]
        }
        """
        trading_pair_rules = exchange_info_dict.get("symbols", [])
        retval = []
        for rule in filter(binance_utils.is_exchange_information_valid,
                           trading_pair_rules):
            try:
                trading_pair = await BinanceAPIOrderBookDataSource.trading_pair_associated_to_exchange_symbol(
                    symbol=rule.get("symbol"),
                    domain=self._domain,
                    api_factory=self._api_factory,
                    throttler=self._throttler)
                filters = rule.get("filters")
                price_filter = [
                    f for f in filters if f.get("filterType") == "PRICE_FILTER"
                ][0]
                lot_size_filter = [
                    f for f in filters if f.get("filterType") == "LOT_SIZE"
                ][0]
                min_notional_filter = [
                    f for f in filters if f.get("filterType") == "MIN_NOTIONAL"
                ][0]

                min_order_size = Decimal(lot_size_filter.get("minQty"))
                tick_size = price_filter.get("tickSize")
                step_size = Decimal(lot_size_filter.get("stepSize"))
                min_notional = Decimal(min_notional_filter.get("minNotional"))

                retval.append(
                    TradingRule(trading_pair,
                                min_order_size=min_order_size,
                                min_price_increment=Decimal(tick_size),
                                min_base_amount_increment=Decimal(step_size),
                                min_notional_size=Decimal(min_notional)))

            except Exception:
                self.logger().exception(
                    f"Error parsing the trading pair rule {rule}. Skipping.")
        return retval

    async def _user_stream_event_listener(self):
        """
        This functions runs in background continuously processing the events received from the exchange by the user
        stream data source. It keeps reading events from the queue until the task is interrupted.
        The events received are balance updates, order updates and trade events.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                event_type = event_message.get("e")
                # Refer to https://github.com/binance-exchange/binance-official-api-docs/blob/master/user-data-stream.md
                # As per the order update section in Binance the ID of the order being cancelled is under the "C" key
                if event_type == "executionReport":
                    execution_type = event_message.get("x")
                    if execution_type != "CANCELED":
                        client_order_id = event_message.get("c")
                    else:
                        client_order_id = event_message.get("C")

                    if execution_type == "TRADE":
                        tracked_order = self._order_tracker.fetch_order(
                            client_order_id=client_order_id)
                        if tracked_order is not None:
                            trade_update = TradeUpdate(
                                trade_id=str(event_message["t"]),
                                client_order_id=client_order_id,
                                exchange_order_id=str(event_message["i"]),
                                trading_pair=tracked_order.trading_pair,
                                fee_asset=event_message["N"],
                                fee_paid=Decimal(event_message["n"]),
                                fill_base_amount=Decimal(event_message["l"]),
                                fill_quote_amount=Decimal(event_message["l"]) *
                                Decimal(event_message["L"]),
                                fill_price=Decimal(event_message["L"]),
                                fill_timestamp=int(event_message["T"]),
                            )
                            self._order_tracker.process_trade_update(
                                trade_update)

                    tracked_order = self.in_flight_orders.get(client_order_id)
                    if tracked_order is not None:
                        order_update = OrderUpdate(
                            trading_pair=tracked_order.trading_pair,
                            update_timestamp=int(event_message["E"]),
                            new_state=CONSTANTS.ORDER_STATE[
                                event_message["X"]],
                            client_order_id=client_order_id,
                            exchange_order_id=str(event_message["i"]),
                        )
                        self._order_tracker.process_order_update(
                            order_update=order_update)

                elif event_type == "outboundAccountPosition":
                    balances = event_message["B"]
                    for balance_entry in balances:
                        asset_name = balance_entry["a"]
                        free_balance = Decimal(balance_entry["f"])
                        total_balance = Decimal(balance_entry["f"]) + Decimal(
                            balance_entry["l"])
                        self._account_available_balances[
                            asset_name] = free_balance
                        self._account_balances[asset_name] = total_balance

            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().error(
                    "Unexpected error in user stream listener loop.",
                    exc_info=True)
                await asyncio.sleep(5.0)

    async def _update_order_fills_from_trades(self):
        """
        This is intended to be a backup measure to get filled events with trade ID for orders,
        in case Binance's user stream events are not working.
        NOTE: It is not required to copy this functionality in other connectors.
        This is separated from _update_order_status which only updates the order status without producing filled
        events, since Binance's get order endpoint does not return trade IDs.
        The minimum poll interval for order status is 10 seconds.
        """
        small_interval_last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL
        small_interval_current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL
        long_interval_last_tick = self._last_poll_timestamp / self.LONG_POLL_INTERVAL
        long_interval_current_tick = self.current_timestamp / self.LONG_POLL_INTERVAL

        if (long_interval_current_tick > long_interval_last_tick or
            (self.in_flight_orders
             and small_interval_current_tick > small_interval_last_tick)):
            query_time = int(self._last_trades_poll_binance_timestamp * 1e3)
            self._last_trades_poll_binance_timestamp = self._binance_time_synchronizer.time(
            )
            order_by_exchange_id_map = {}
            for order in self._order_tracker.all_orders.values():
                order_by_exchange_id_map[order.exchange_order_id] = order

            tasks = []
            trading_pairs = self._order_book_tracker._trading_pairs
            for trading_pair in trading_pairs:
                params = {
                    "symbol":
                    await BinanceAPIOrderBookDataSource.
                    exchange_symbol_associated_to_pair(
                        trading_pair=trading_pair,
                        domain=self._domain,
                        api_factory=self._api_factory,
                        throttler=self._throttler)
                }
                if self._last_poll_timestamp > 0:
                    params["startTime"] = query_time
                tasks.append(
                    self._api_request(method=RESTMethod.GET,
                                      path_url=CONSTANTS.MY_TRADES_PATH_URL,
                                      params=params,
                                      is_auth_required=True))

            self.logger().debug(
                f"Polling for order fills of {len(tasks)} trading pairs.")
            results = await safe_gather(*tasks, return_exceptions=True)

            for trades, trading_pair in zip(results, trading_pairs):

                if isinstance(trades, Exception):
                    self.logger().network(
                        f"Error fetching trades update for the order {trading_pair}: {trades}.",
                        app_warning_msg=
                        f"Failed to fetch trade update for {trading_pair}.")
                    continue
                for trade in trades:
                    exchange_order_id = str(trade["orderId"])
                    if exchange_order_id in order_by_exchange_id_map:
                        # This is a fill for a tracked order
                        tracked_order = order_by_exchange_id_map[
                            exchange_order_id]
                        trade_update = TradeUpdate(
                            trade_id=str(trade["id"]),
                            client_order_id=tracked_order.client_order_id,
                            exchange_order_id=exchange_order_id,
                            trading_pair=trading_pair,
                            fee_asset=trade["commissionAsset"],
                            fee_paid=Decimal(trade["commission"]),
                            fill_base_amount=Decimal(trade["qty"]),
                            fill_quote_amount=Decimal(trade["quoteQty"]),
                            fill_price=Decimal(trade["price"]),
                            fill_timestamp=int(trade["time"]),
                        )
                        self._order_tracker.process_trade_update(trade_update)
                    elif self.is_confirmed_new_order_filled_event(
                            str(trade["id"]), exchange_order_id, trading_pair):
                        # This is a fill of an order registered in the DB but not tracked any more
                        self._current_trade_fills.add(
                            TradeFillOrderDetails(market=self.display_name,
                                                  exchange_trade_id=str(
                                                      trade["id"]),
                                                  symbol=trading_pair))
                        self.trigger_event(
                            MarketEvent.OrderFilled,
                            OrderFilledEvent(
                                timestamp=float(trade["time"]) * 1e-3,
                                order_id=self._exchange_order_ids.get(
                                    str(trade["orderId"]), None),
                                trading_pair=trading_pair,
                                trade_type=TradeType.BUY
                                if trade["isBuyer"] else TradeType.SELL,
                                order_type=OrderType.LIMIT_MAKER
                                if trade["isMaker"] else OrderType.LIMIT,
                                price=Decimal(trade["price"]),
                                amount=Decimal(trade["qty"]),
                                trade_fee=DeductedFromReturnsTradeFee(
                                    flat_fees=[
                                        TokenAmount(
                                            trade["commissionAsset"],
                                            Decimal(trade["commission"]))
                                    ]),
                                exchange_trade_id=str(trade["id"])))
                        self.logger().info(
                            f"Recreating missing trade in TradeFill: {trade}")

    async def _update_order_status(self):
        # This is intended to be a backup measure to close straggler orders, in case Binance's user stream events
        # are not working.
        # The minimum poll interval for order status is 10 seconds.
        last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL
        current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL

        tracked_orders: List[InFlightOrder] = list(
            self.in_flight_orders.values())
        if current_tick > last_tick and len(tracked_orders) > 0:

            tasks = [
                self._api_request(method=RESTMethod.GET,
                                  path_url=CONSTANTS.ORDER_PATH_URL,
                                  params={
                                      "symbol":
                                      await BinanceAPIOrderBookDataSource.
                                      exchange_symbol_associated_to_pair(
                                          trading_pair=o.trading_pair,
                                          domain=self._domain,
                                          api_factory=self._api_factory,
                                          throttler=self._throttler),
                                      "origClientOrderId":
                                      o.client_order_id
                                  },
                                  is_auth_required=True)
                for o in tracked_orders
            ]
            self.logger().debug(
                f"Polling for order status updates of {len(tasks)} orders.")
            results = await safe_gather(*tasks, return_exceptions=True)
            for order_update, tracked_order in zip(results, tracked_orders):
                client_order_id = tracked_order.client_order_id

                # If the order has already been cancelled or has failed do nothing
                if client_order_id not in self.in_flight_orders:
                    continue

                if isinstance(order_update, Exception):
                    self.logger().network(
                        f"Error fetching status update for the order {client_order_id}: {order_update}.",
                        app_warning_msg=
                        f"Failed to fetch status update for the order {client_order_id}."
                    )
                    self._order_not_found_records[client_order_id] = (
                        self._order_not_found_records.get(client_order_id, 0) +
                        1)
                    if (self._order_not_found_records[client_order_id] >= self.
                            MAX_ORDER_UPDATE_RETRIEVAL_RETRIES_WITH_FAILURES):
                        # Wait until the order not found error have repeated a few times before actually treating
                        # it as failed. See: https://github.com/CoinAlpha/hummingbot/issues/601

                        order_update: OrderUpdate = OrderUpdate(
                            client_order_id=client_order_id,
                            trading_pair=tracked_order.trading_pair,
                            update_timestamp=int(self.current_timestamp * 1e3),
                            new_state=OrderState.FAILED,
                        )
                        self._order_tracker.process_order_update(order_update)

                else:
                    # Update order execution status
                    new_state = CONSTANTS.ORDER_STATE[order_update["status"]]

                    update = OrderUpdate(
                        client_order_id=client_order_id,
                        exchange_order_id=str(order_update["orderId"]),
                        trading_pair=tracked_order.trading_pair,
                        update_timestamp=int(order_update["updateTime"]),
                        new_state=new_state,
                    )
                    self._order_tracker.process_order_update(update)

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch user events from Binance. Check API key and network connection."
                )
                await asyncio.sleep(1.0)

    async def _update_balances(self):
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()

        try:
            account_info = await self._api_request(
                method=RESTMethod.GET,
                path_url=CONSTANTS.ACCOUNTS_PATH_URL,
                is_auth_required=True)

            balances = account_info["balances"]
            for balance_entry in balances:
                asset_name = balance_entry["asset"]
                free_balance = Decimal(balance_entry["free"])
                total_balance = Decimal(balance_entry["free"]) + Decimal(
                    balance_entry["locked"])
                self._account_available_balances[asset_name] = free_balance
                self._account_balances[asset_name] = total_balance
                remote_asset_names.add(asset_name)

            asset_names_to_remove = local_asset_names.difference(
                remote_asset_names)
            for asset_name in asset_names_to_remove:
                del self._account_available_balances[asset_name]
                del self._account_balances[asset_name]
        except IOError:
            self.logger().exception(
                "Error getting account balances from server")

    async def _update_time_synchronizer(self):
        try:
            await self._binance_time_synchronizer.update_server_time_offset_with_time_provider(
                time_provider=self._get_current_server_time())
        except asyncio.CancelledError:
            raise
        except Exception:
            self.logger().exception(
                "Error requesting time from Binance server")
            raise

    async def _api_request(self,
                           method: RESTMethod,
                           path_url: str,
                           params: Optional[Dict[str, Any]] = None,
                           data: Optional[Dict[str, Any]] = None,
                           is_auth_required: bool = False) -> Dict[str, Any]:

        headers = {
            "Content-Type":
            "application/json" if method == RESTMethod.POST else
            "application/x-www-form-urlencoded"
        }
        client = await self._get_rest_assistant()

        if is_auth_required:
            url = binance_utils.private_rest_url(path_url, domain=self._domain)
        else:
            url = binance_utils.public_rest_url(path_url, domain=self._domain)

        request = RESTRequest(method=method,
                              url=url,
                              data=data,
                              params=params,
                              headers=headers,
                              is_auth_required=is_auth_required)

        async with self._throttler.execute_task(limit_id=path_url):
            response = await client.call(request)

            if response.status != 200:
                data = await response.text()
                raise IOError(
                    f"Error fetching data from {url}. HTTP status is {response.status} ({data})."
                )
            try:
                parsed_response = await response.json()
            except Exception:
                raise IOError(f"Error parsing data from {response}.")

            if "code" in parsed_response and "msg" in parsed_response:
                raise IOError(
                    f"The request to Binance failed. Error: {parsed_response}. Request: {request}"
                )

        return parsed_response

    async def _get_current_server_time(self):
        response = await self._api_request(
            method=RESTMethod.GET,
            path_url=CONSTANTS.SERVER_TIME_PATH_URL,
        )
        return response["serverTime"]

    async def _get_rest_assistant(self) -> RESTAssistant:
        if self._rest_assistant is None:
            self._rest_assistant = await self._api_factory.get_rest_assistant()
        return self._rest_assistant
Example #7
0
class NdaxExchange(ExchangeBase):
    """
    Class to onnect with NDAX exchange. Provides order book pricing, user account tracking and
    trading functionality.
    """
    SHORT_POLL_INTERVAL = 5.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    UPDATE_TRADING_RULES_INTERVAL = 60.0
    LONG_POLL_INTERVAL = 120.0
    ORDER_EXCEED_NOT_FOUND_COUNT = 2

    _logger = None

    @classmethod
    def logger(cls) -> HummingbotLogger:
        if cls._logger is None:
            cls._logger = logging.getLogger(__name__)
        return cls._logger

    def __init__(self,
                 ndax_uid: str,
                 ndax_api_key: str,
                 ndax_secret_key: str,
                 ndax_account_name: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True,
                 domain: Optional[str] = None):
        """
        :param ndax_uid: User ID of the account
        :param ndax_api_key: The API key to connect to private NDAX APIs.
        :param ndax_secret_key: The API secret.
        :param ndax_account_name: The name of the account associated to the user account.
        :param trading_pairs: The market trading pairs which to track order book data.
        :param trading_required: Whether actual trading is needed.
        """
        super().__init__()
        self._trading_required = trading_required
        self._trading_pairs = trading_pairs
        self._auth = NdaxAuth(uid=ndax_uid,
                              api_key=ndax_api_key,
                              secret_key=ndax_secret_key,
                              account_name=ndax_account_name)
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._shared_client = aiohttp.ClientSession()
        self._order_book_tracker = NdaxOrderBookTracker(
            throttler=self._throttler,
            shared_client=self._shared_client,
            trading_pairs=trading_pairs,
            domain=domain)
        self._user_stream_tracker = NdaxUserStreamTracker(
            throttler=self._throttler,
            shared_client=self._shared_client,
            auth_assistant=self._auth,
            domain=domain)
        self._domain = domain
        self._ev_loop = asyncio.get_event_loop()
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders = {}
        self._order_not_found_records = {
        }  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, TradingRule]
        self._last_poll_timestamp = 0

        self._status_polling_task = None
        self._user_stream_tracker_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None

        self._account_id = None

    @property
    def name(self) -> str:
        return CONSTANTS.EXCHANGE_NAME

    @property
    def account_id(self) -> int:
        return self._account_id

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, NdaxInFlightOrder]:
        return self._in_flight_orders

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        A dictionary of statuses of various exchange's components. Used to determine if the connector is ready
        """
        return {
            "account_id_initialized":
            self.account_id if self._trading_required else True,
            "order_books_initialized":
            self._order_book_tracker.ready,
            "account_balance":
            len(self._account_balances) > 0
            if self._trading_required else True,
            "trading_rule_initialized":
            len(self._trading_rules) > 0,
            "user_stream_initialized":
            self._user_stream_tracker.data_source.last_recv_time > 0
            if self._trading_required else True,
        }

    @property
    def ready(self) -> bool:
        """
        Determines if the connector is ready.
        :return True when all statuses pass, this might take 5-10 seconds for all the connector's components and
        services to be ready.
        """
        return all(self.status_dict.values())

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, Any]:
        """
        :return active in-flight order in JSON format. Used to save order entries into local sqlite databse.
        """
        return {
            client_oid: order.to_json()
            for client_oid, order in self._in_flight_orders.items()
            if not order.is_done
        }

    async def initialized_account_id(self) -> int:
        if not self._account_id:
            self._account_id = await self._get_account_id()
        return self._account_id

    def restore_tracking_states(self, saved_states: Dict[str, Any]):
        """
        Restore in-flight orders from the saved tracking states(from local db). This is such that the connector can pick
        up from where it left off before Hummingbot client was terminated.
        :param saved_states: The saved tracking_states.
        """
        self._in_flight_orders.update({
            client_oid: NdaxInFlightOrder.from_json(order_json)
            for client_oid, order_json in saved_states.items()
        })

    def supported_order_types(self) -> List[OrderType]:
        """
        :return: a list of OrderType supported by this connector.
        Note that Market order type is no longer required and will not be used.
        """
        return [OrderType.MARKET, OrderType.LIMIT, OrderType.LIMIT_MAKER]

    async def _get_account_id(self) -> int:
        """
        Calls REST API to retrieve Account ID
        """
        params = {
            "OMSId": 1,
            "UserId": self._auth.uid,
            "UserName": self._auth.account_name
        }

        resp: List[int] = await self._api_request(
            "GET",
            path_url=CONSTANTS.USER_ACCOUNT_INFOS_PATH_URL,
            params=params,
            is_auth_required=True,
        )

        account_info = next(
            (account_info for account_info in resp
             if account_info.get("AccountName") == self._auth.account_name),
            None)
        if account_info is None:
            self.logger().error(
                f"There is no account named {self._auth.account_name} "
                f"associated with the current NDAX user")
            acc_id = None
        else:
            acc_id = int(account_info.get("AccountId"))

        return acc_id

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(
            self._trading_rules_polling_loop())
        if self._trading_required:
            self._status_polling_task = safe_ensure_future(
                self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(
                self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(
                self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        """
        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        This function is required by NetworkIterator base class and is called periodically to check
        the network connection. Simply ping the network (or call any light weight public API).
        """
        try:
            resp = await self._api_request(
                method="GET",
                path_url=CONSTANTS.PING_PATH_URL,
                limit_id=CONSTANTS.HTTP_PING_ID,
            )
            if "msg" not in resp or resp["msg"] != "PONG":
                raise Exception()
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    async def _api_request(
            self,
            method: str,
            path_url: str,
            params: Optional[Dict[str, Any]] = None,
            data: Optional[Dict[str, Any]] = None,
            is_auth_required: bool = False,
            limit_id: Optional[str] = None
    ) -> Union[Dict[str, Any], List[Any]]:
        """
        Sends an aiohttp request and waits for a response.
        :param method: The HTTP method, e.g. get or post
        :param path_url: The path url or the API end point
        :param params: The query parameters of the API request
        :param params: The body parameters of the API request
        :param is_auth_required: Whether an authentication is required, when True the function will add encrypted
        signature to the request.
        :param limit_id: The id used for the API throttler. If not supplied, the `path_url` is used instead.
        :returns A response in json format.
        """
        url = ndax_utils.rest_api_url(self._domain) + path_url

        try:
            if is_auth_required:
                headers = self._auth.get_auth_headers()
            else:
                headers = self._auth.get_headers()

            limit_id = limit_id or path_url
            if method == "GET":
                async with self._throttler.execute_task(limit_id):
                    response = await self._shared_client.get(url,
                                                             headers=headers,
                                                             params=params)
            elif method == "POST":
                async with self._throttler.execute_task(limit_id):
                    response = await self._shared_client.post(
                        url, headers=headers, data=ujson.dumps(data))
            else:
                raise NotImplementedError(
                    f"{method} HTTP Method not implemented. ")

            data = await response.text()
            if data == CONSTANTS.API_LIMIT_REACHED_ERROR_MESSAGE:
                raise Exception(
                    f"The exchange API request limit has been reached (original error '{data}')"
                )

            parsed_response = await response.json()

        except ValueError as e:
            self.logger().error(f"{str(e)}")
            raise ValueError(
                f"Error authenticating request {method} {url}. Error: {str(e)}"
            )
        except Exception as e:
            raise IOError(f"Error parsing data from {url}. Error: {str(e)}")
        if response.status != 200 or (isinstance(parsed_response, dict) and
                                      not parsed_response.get("result", True)):
            self.logger().error(
                f"Error fetching data from {url}. HTTP status is {response.status}. "
                f"Message: {parsed_response} "
                f"Params: {params} "
                f"Data: {data}")
            raise Exception(
                f"Error fetching data from {url}. HTTP status is {response.status}. "
                f"Message: {parsed_response} "
                f"Params: {params} "
                f"Data: {data}")

        return parsed_response

    def get_order_price_quantum(self, trading_pair: str,
                                price: Decimal) -> Decimal:
        """
        Used by quantize_order_price() in _create_order()
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str,
                               order_size: Decimal) -> Decimal:
        """
        Used by quantize_order_price() in _create_order()
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    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. ")

    def trigger_order_created_event(self, order: NdaxInFlightOrder):
        event_tag = MarketEvent.BuyOrderCreated if order.trade_type is TradeType.BUY else MarketEvent.SellOrderCreated
        event_class = BuyOrderCreatedEvent if order.trade_type is TradeType.BUY else SellOrderCreatedEvent
        self.trigger_event(
            event_tag,
            event_class(self.current_timestamp,
                        order.order_type,
                        order.trading_pair,
                        order.amount,
                        order.price,
                        order.client_order_id,
                        exchange_order_id=order.exchange_order_id))

    def buy(self,
            trading_pair: str,
            amount: Decimal,
            order_type: OrderType = OrderType.MARKET,
            price: Decimal = s_decimal_NaN,
            **kwargs) -> str:
        """
        Buys an amount of base asset as specified in the trading pair. This function returns immediately.
        To see an actual order, wait for a BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-CAD) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price in which the order is to be placed at
        :returns A new client order id
        """
        order_id: str = ndax_utils.get_new_client_order_id(True, trading_pair)
        safe_ensure_future(
            self._create_order(
                trade_type=TradeType.BUY,
                trading_pair=trading_pair,
                order_id=order_id,
                amount=amount,
                price=price,
                order_type=order_type,
            ))
        return order_id

    def sell(self,
             trading_pair: str,
             amount: Decimal,
             order_type: OrderType = OrderType.MARKET,
             price: Decimal = s_decimal_NaN,
             **kwargs) -> str:
        """
        Sells an amount of base asset as specified in the trading pair. This function returns immediately.
        To see an actual order, wait for a BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-CAD) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price in which the order is to be placed at
        :returns A new client order id
        """
        order_id: str = ndax_utils.get_new_client_order_id(False, trading_pair)
        safe_ensure_future(
            self._create_order(
                trade_type=TradeType.SELL,
                trading_pair=trading_pair,
                order_id=order_id,
                amount=amount,
                price=price,
                order_type=order_type,
            ))
        return order_id

    async def _execute_cancel(self, trading_pair: str, order_id: str) -> str:
        """
        To determine if an order is successfully cancelled, we either call the
        GetOrderStatus/GetOpenOrders endpoint or wait for a OrderStateEvent/OrderTradeEvent from the WS.
        :param trading_pair: The market (e.g. BTC-CAD) the order is in.
        :param order_id: The client_order_id of the order to be cancelled.
        """
        try:
            tracked_order: Optional[
                NdaxInFlightOrder] = self._in_flight_orders.get(
                    order_id, None)
            if tracked_order is None:
                raise ValueError(
                    f"Failed to cancel order - {order_id}. Order not being tracked."
                )
            if tracked_order.is_locally_working:
                raise NdaxInFlightOrderNotCreated(
                    f"Failed to cancel order - {order_id}. Order not yet created."
                    f" This is most likely due to rate-limiting.")

            body_params = {
                "OMSId": 1,
                "AccountId": await self.initialized_account_id(),
                "OrderId": await tracked_order.get_exchange_order_id()
            }

            # The API response simply verifies that the API request have been received by the API servers.
            await self._api_request(method="POST",
                                    path_url=CONSTANTS.CANCEL_ORDER_PATH_URL,
                                    data=body_params,
                                    is_auth_required=True)

            return order_id

        except asyncio.CancelledError:
            raise
        except NdaxInFlightOrderNotCreated:
            raise
        except Exception as e:
            self.logger().error(f"Failed to cancel order {order_id}: {str(e)}")
            self.logger().network(
                f"Failed to cancel order {order_id}: {str(e)}",
                exc_info=True,
                app_warning_msg=f"Failed to cancel order {order_id} on NDAX. "
                f"Check API key and network connection.")
            if RESOURCE_NOT_FOUND_ERR in str(e):
                self._order_not_found_records[
                    order_id] = self._order_not_found_records.get(order_id,
                                                                  0) + 1
                if self._order_not_found_records[
                        order_id] >= self.ORDER_EXCEED_NOT_FOUND_COUNT:
                    self.logger().warning(
                        f"Order {order_id} does not seem to be active, will stop tracking order..."
                    )
                    self.stop_tracking_order(order_id)
                    self.trigger_event(
                        MarketEvent.OrderCancelled,
                        OrderCancelledEvent(self.current_timestamp, order_id))

    def cancel(self, trading_pair: str, order_id: str):
        """
        Cancel an order. This function returns immediately.
        An Order is only determined to be cancelled when a OrderCancelledEvent is received.
        :param trading_pair: The market (e.g. BTC-CAD) of the order.
        :param order_id: The client_order_id of the order to be cancelled.
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    async def get_open_orders(self) -> List[OpenOrder]:
        query_params = {
            "OMSId": 1,
            "AccountId": await self.initialized_account_id(),
        }
        open_orders: List[Dict[str, Any]] = await self._api_request(
            method="GET",
            path_url=CONSTANTS.GET_OPEN_ORDERS_PATH_URL,
            params=query_params,
            is_auth_required=True)

        trading_pair_id_map: Dict[
            str,
            int] = await self._order_book_tracker.data_source.get_instrument_ids(
            )
        id_trading_pair_map: Dict[int, str] = {
            instrument_id: trading_pair
            for trading_pair, instrument_id in trading_pair_id_map.items()
        }

        return [
            OpenOrder(
                client_order_id=order["ClientOrderId"],
                trading_pair=id_trading_pair_map[order["Instrument"]],
                price=Decimal(str(order["Price"])),
                amount=Decimal(str(order["Quantity"])),
                executed_amount=Decimal(str(order["QuantityExecuted"])),
                status=order["OrderState"],
                order_type=OrderType.LIMIT
                if order["OrderType"] == "Limit" else OrderType.MARKET,
                is_buy=True if order["Side"] == "Buy" else False,
                time=order["ReceiveTime"],
                exchange_order_id=order["OrderId"],
            ) for order in open_orders
        ]

    async def cancel_all(self, timeout_sec: float) -> List[CancellationResult]:
        """
        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_sec: The timeout at which the operation will be canceled.
        :returns List of CancellationResult which indicates whether each order is successfully cancelled.
        """

        # Note: NDAX's CancelOrder endpoint simply indicates if the cancel requests has been succesfully received.
        cancellation_results = []
        tracked_orders = self.in_flight_orders
        try:
            for order in tracked_orders.values():
                self.cancel(trading_pair=order.trading_pair,
                            order_id=order.client_order_id)

            open_orders = await self.get_open_orders()

            for client_oid, tracked_order in tracked_orders.items():
                matched_order = [
                    o for o in open_orders if o.client_order_id == client_oid
                ]
                if not matched_order:
                    cancellation_results.append(
                        CancellationResult(client_oid, True))
                    self.trigger_event(
                        MarketEvent.OrderCancelled,
                        OrderCancelledEvent(self.current_timestamp,
                                            client_oid))
                else:
                    cancellation_results.append(
                        CancellationResult(client_oid, False))

        except Exception as ex:
            self.logger().network(
                f"Failed to cancel all orders ({ex})",
                exc_info=True,
                app_warning_msg=
                "Failed to cancel all orders on NDAX. Check API key and network connection."
            )
        return cancellation_results

    def _format_trading_rules(
            self, instrument_info: List[Dict[str,
                                             Any]]) -> Dict[str, TradingRule]:
        """
        Converts JSON API response into a local dictionary of trading rules.
        :param instrument_info: The JSON API response.
        :returns: A dictionary of trading pair to its respective TradingRule.
        """
        result = {}
        for instrument in instrument_info:
            try:
                trading_pair = f"{instrument['Product1Symbol']}-{instrument['Product2Symbol']}"

                result[trading_pair] = TradingRule(
                    trading_pair=trading_pair,
                    min_order_size=Decimal(str(instrument["MinimumQuantity"])),
                    min_price_increment=Decimal(
                        str(instrument["PriceIncrement"])),
                    min_base_amount_increment=Decimal(
                        str(instrument["QuantityIncrement"])),
                )
            except Exception:
                self.logger().error(
                    f"Error parsing the trading pair rule: {instrument}. Skipping...",
                    exc_info=True)
        return result

    async def _update_trading_rules(self):
        params = {"OMSId": 1}
        instrument_info: List[Dict[str, Any]] = await self._api_request(
            method="GET", path_url=CONSTANTS.MARKETS_URL, params=params)
        self._trading_rules.clear()
        self._trading_rules = self._format_trading_rules(instrument_info)

    async def _trading_rules_polling_loop(self):
        """
        Periodically update trading rules.
        """
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(self.UPDATE_TRADING_RULES_INTERVAL)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().network(
                    f"Unexpected error while fetching trading rules. Error: {str(e)}",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch new trading rules from NDAX. "
                    "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances
        """
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()

        params = {"OMSId": 1, "AccountId": await self.initialized_account_id()}
        account_positions: List[Dict[str, Any]] = await self._api_request(
            method="GET",
            path_url=CONSTANTS.ACCOUNT_POSITION_PATH_URL,
            params=params,
            is_auth_required=True)
        for position in account_positions:
            asset_name = position["ProductSymbol"]
            self._account_balances[asset_name] = Decimal(
                str(position["Amount"]))
            self._account_available_balances[
                asset_name] = self._account_balances[asset_name] - Decimal(
                    str(position["Hold"]))
            remote_asset_names.add(asset_name)

        asset_names_to_remove = local_asset_names.difference(
            remote_asset_names)
        for asset_name in asset_names_to_remove:
            del self._account_available_balances[asset_name]
            del self._account_balances[asset_name]

    def start_tracking_order(self, order_id: str,
                             exchange_order_id: Optional[str],
                             trading_pair: str, trade_type: TradeType,
                             price: Decimal, amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by simply adding it into _in_flight_orders dictionary.
        """
        self._in_flight_orders[order_id] = NdaxInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount)

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order by simply removing it from _in_flight_orders dictionary.
        """
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]

    async def _update_order_status(self):
        """
        Calls REST API to get order status
        """
        # Waiting on buy and sell functionality.
        active_orders: List[NdaxInFlightOrder] = [
            o for o in self._in_flight_orders.values()
            if not o.is_locally_working
        ]
        if len(active_orders) == 0:
            return

        tasks = []
        for active_order in active_orders:
            ex_order_id: Optional[str] = None
            try:
                ex_order_id = await active_order.get_exchange_order_id()
            except asyncio.TimeoutError:
                # We assume that tracked orders without an exchange order id is an order that failed to be created.
                self._order_not_found_records[
                    active_order.
                    client_order_id] = self._order_not_found_records.get(
                        active_order.client_order_id, 0) + 1
                self.logger().debug(
                    f"Tracker order {active_order.client_order_id} does not have an exchange id."
                    f"Attempting fetch in next polling interval")
                if self._order_not_found_records[
                        active_order.
                        client_order_id] >= self.ORDER_EXCEED_NOT_FOUND_COUNT:
                    self.logger().info(
                        f"Order {active_order.client_order_id} does not seem to be active, will stop tracking order..."
                    )
                    self.stop_tracking_order(active_order.client_order_id)
                    self.trigger_event(
                        MarketEvent.OrderCancelled,
                        OrderCancelledEvent(self.current_timestamp,
                                            active_order.client_order_id))
                continue

            query_params = {
                "OMSId": 1,
                "AccountId": await self.initialized_account_id(),
                "OrderId": int(ex_order_id),
            }

            tasks.append(
                asyncio.create_task(
                    self._api_request(
                        method="GET",
                        path_url=CONSTANTS.GET_ORDER_STATUS_PATH_URL,
                        params=query_params,
                        is_auth_required=True,
                    )))
        self.logger().debug(
            f"Polling for order status updates of {len(tasks)} orders. ")

        raw_responses: List[Dict[str, Any]] = await safe_gather(
            *tasks, return_exceptions=True)

        # Initial parsing of responses. Removes Exceptions.
        parsed_status_responses: List[Dict[str, Any]] = []
        for resp in raw_responses:
            if not isinstance(resp, Exception):
                parsed_status_responses.append(resp)
            else:
                self.logger().error(
                    f"Error fetching order status. Response: {resp}")

        if len(parsed_status_responses) == 0:
            return

        min_ts: int = min([
            int(order_status["ReceiveTime"])
            for order_status in parsed_status_responses
        ])

        trade_history_tasks = []
        trading_pair_ids: Dict[
            str,
            int] = await self._order_book_tracker.data_source.get_instrument_ids(
            )

        for trading_pair in self._trading_pairs:
            body_params = {
                "OMSId": 1,
                "AccountId": await self.initialized_account_id(),
                "UserId": self._auth.uid,
                "InstrumentId": trading_pair_ids[trading_pair],
                "StartTimestamp": min_ts,
            }
            trade_history_tasks.append(
                asyncio.create_task(
                    self._api_request(
                        method="POST",
                        path_url=CONSTANTS.GET_TRADES_HISTORY_PATH_URL,
                        data=body_params,
                        is_auth_required=True)))

        raw_responses: List[Dict[str, Any]] = await safe_gather(
            *trade_history_tasks, return_exceptions=True)

        # Initial parsing of responses. Joining all the responses
        parsed_history_resps: List[Dict[str, Any]] = []
        for resp in raw_responses:
            if not isinstance(resp, Exception):
                parsed_history_resps.extend(resp)
            else:
                self.logger().error(
                    f"Error fetching trades history. Response: {resp}")

        # Trade updates must be handled before any order status updates.
        for trade in parsed_history_resps:
            self._process_trade_event_message(trade)

        for order_status in parsed_status_responses:
            self._process_order_event_message(order_status)

    def _reset_poll_notifier(self):
        self._poll_notifier = asyncio.Event()

    async def _status_polling_loop(self):
        """
        Periodically update user balances and order status via REST API. This serves as a fallback measure for web
        socket API updates.
        """
        while True:
            try:
                self._reset_poll_notifier()
                await self._poll_notifier.wait()
                start_ts = self.current_timestamp
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = start_ts
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(
                    f"Unexpected error while in status polling loop. Error: {str(e)}",
                    exc_info=True)
                self.logger().network(
                    "Unexpected error while fetching account updates.",
                    exc_info=True,
                    app_warning_msg="Could not fetch account updates from NDAX. "
                    "Check API key and network connection.")
                await asyncio.sleep(0.5)

    def tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock tick(1 second by default).
        It checks if a status polling task is due for execution.
        """
        now = time.time()
        poll_interval = (self.SHORT_POLL_INTERVAL if
                         now - self._user_stream_tracker.last_recv_time > 60.0
                         else self.LONG_POLL_INTERVAL)
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN,
                is_maker: Optional[bool] = None) -> AddedToCostTradeFee:
        """
        To get trading fee, this function is simplified by using fee override configuration. Most parameters to this
        function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for
        maker order.
        """
        is_maker = order_type is OrderType.LIMIT_MAKER
        return AddedToCostTradeFee(percent=self.estimate_fee_pct(is_maker))

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch user events from NDAX. Check API key and network connection."
                )
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        """
        Listens to message in _user_stream_tracker.user_stream queue.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                endpoint = NdaxWebSocketAdaptor.endpoint_from_message(
                    event_message)
                payload = NdaxWebSocketAdaptor.payload_from_message(
                    event_message)

                if endpoint == CONSTANTS.ACCOUNT_POSITION_EVENT_ENDPOINT_NAME:
                    self._process_account_position_event(payload)
                elif endpoint == CONSTANTS.ORDER_STATE_EVENT_ENDPOINT_NAME:
                    self._process_order_event_message(payload)
                elif endpoint == CONSTANTS.ORDER_TRADE_EVENT_ENDPOINT_NAME:
                    self._process_trade_event_message(payload)
                else:
                    self.logger().debug(
                        f"Unknown event received from the connector ({event_message})"
                    )
            except asyncio.CancelledError:
                raise
            except Exception as ex:
                self.logger().error(
                    f"Unexpected error in user stream listener loop ({ex})",
                    exc_info=True)
                await asyncio.sleep(5.0)

    def _process_account_position_event(self,
                                        account_position_event: Dict[str,
                                                                     Any]):
        token = account_position_event["ProductSymbol"]
        amount = Decimal(str(account_position_event["Amount"]))
        on_hold = Decimal(str(account_position_event["Hold"]))
        self._account_balances[token] = amount
        self._account_available_balances[token] = (amount - on_hold)

    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)

    def _process_trade_event_message(self, order_msg: Dict[str, Any]):
        """
        Updates in-flight order and trigger order filled event for trade message received. Triggers order completed
        event if the total executed amount equals to the specified order amount.
        :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]
            updated = tracked_order.update_with_trade_update(order_msg)

            if updated:
                trade_amount = Decimal(str(order_msg["Quantity"]))
                trade_price = Decimal(str(order_msg["Price"]))
                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=tracked_order.trade_type,
                    amount=trade_amount,
                    price=trade_price)
                amount_for_fee = (trade_amount
                                  if tracked_order.trade_type is TradeType.BUY
                                  else trade_amount * trade_price)
                tracked_order.fee_paid += amount_for_fee * trade_fee.percent

                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,
                                     trade_price,
                                     trade_amount,
                                     trade_fee,
                                     exchange_trade_id=str(
                                         order_msg["TradeId"])))
                if (math.isclose(tracked_order.executed_amount_base,
                                 tracked_order.amount)
                        or tracked_order.executed_amount_base >=
                        tracked_order.amount):
                    tracked_order.mark_as_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,
                                    tracked_order.fee_paid,
                                    tracked_order.order_type,
                                    tracked_order.exchange_order_id))
                    self.stop_tracking_order(tracked_order.client_order_id)
Example #8
0
class BitmartExchange(ExchangeBase):
    """
    BitmartExchange connects with BitMart exchange and provides order book pricing, user account tracking and
    trading functionality.
    """
    API_CALL_TIMEOUT = 10.0
    POLL_INTERVAL = 1.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    UPDATE_TRADE_STATUS_MIN_INTERVAL = 10.0

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global ctce_logger
        if ctce_logger is None:
            ctce_logger = logging.getLogger(__name__)
        return ctce_logger

    def __init__(self,
                 bitmart_api_key: str,
                 bitmart_secret_key: str,
                 bitmart_memo: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True,
                 ):
        """
        :param bitmart_api_key: The API key to connect to private BitMart APIs.
        :param bitmart_secret_key: The API secret.
        :param trading_pairs: The market trading pairs which to track order book data.
        :param trading_required: Whether actual trading is needed.
        """
        super().__init__()
        self._api_factory = bitmart_utils.build_api_factory()
        self._rest_assistant = None
        self._trading_required = trading_required
        self._trading_pairs = trading_pairs
        self._bitmart_auth = BitmartAuth(api_key=bitmart_api_key,
                                         secret_key=bitmart_secret_key,
                                         memo=bitmart_memo)
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._order_book_tracker = BitmartOrderBookTracker(
            throttler=self._throttler, trading_pairs=trading_pairs
        )
        self._user_stream_tracker = BitmartUserStreamTracker(
            throttler=self._throttler, bitmart_auth=self._bitmart_auth, trading_pairs=trading_pairs
        )
        self._ev_loop = asyncio.get_event_loop()
        self._shared_client = None
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders = {}  # Dict[client_order_id:str, BitmartInFlightOrder]
        self._order_not_found_records = {}  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, TradingRule]
        self._status_polling_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._last_poll_timestamp = 0
        self._real_time_balance_update = False

    @property
    def name(self) -> str:
        return "bitmart"

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, BitmartInFlightOrder]:
        return self._in_flight_orders

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        A dictionary of statuses of various connector's components.
        """
        return {
            "order_books_initialized": self._order_book_tracker.ready,
            "account_balance": len(self._account_balances) > 0 if self._trading_required else True,
            "trading_rule_initialized": len(self._trading_rules) > 0,
            "user_stream_initialized":
                self._user_stream_tracker.data_source.last_recv_time > 0 if self._trading_required else True,
        }

    @property
    def ready(self) -> bool:
        """
        :return True when all statuses pass, this might take 5-10 seconds for all the connector's components and
        services to be ready.
        """
        return all(self.status_dict.values())

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, any]:
        """
        :return active in-flight orders in json format, is used to save in sqlite db.
        """
        return {
            key: value.to_json()
            for key, value in self._in_flight_orders.items()
            if not value.is_done
        }

    def restore_tracking_states(self, saved_states: Dict[str, any]):
        """
        Restore in-flight orders from saved tracking states, this is st the connector can pick up on where it left off
        when it disconnects.
        :param saved_states: The saved tracking_states.
        """
        self._in_flight_orders.update({
            key: BitmartInFlightOrder.from_json(value)
            for key, value in saved_states.items()
        })

    def supported_order_types(self) -> List[OrderType]:
        """
        :return a list of OrderType supported by this connector.
        Note that Market order type is no longer required and will not be used.
        """
        return [OrderType.LIMIT, OrderType.LIMIT_MAKER]

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(self._trading_rules_polling_loop())
        if self._trading_required:
            self._status_polling_task = safe_ensure_future(self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        """
        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        This function is required by NetworkIterator base class and is called periodically to check
        the network connection. Simply ping the network (or call any light weight public API).
        """
        try:
            request = RESTRequest(
                method=RESTMethod.GET,
                url=f"{CONSTANTS.REST_URL}/{CONSTANTS.CHECK_NETWORK_PATH_URL}",
            )
            rest_assistant = await self._get_rest_assistant()
            response = await rest_assistant.call(request=request)
            if response.status != 200:
                raise Exception
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    async def _get_rest_assistant(self) -> RESTAssistant:
        if self._rest_assistant is None:
            self._rest_assistant = await self._api_factory.get_rest_assistant()
        return self._rest_assistant

    async def _trading_rules_polling_loop(self):
        """
        Periodically update trading rule.
        """
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(60)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().network(f"Unexpected error while fetching trading rules. Error: {str(e)}",
                                      exc_info=True,
                                      app_warning_msg="Could not fetch new trading rules from BitMart. "
                                                      "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        request = RESTRequest(
            method=RESTMethod.GET,
            url=f"{CONSTANTS.REST_URL}/{CONSTANTS.GET_TRADING_RULES_PATH_URL}",
        )
        rest_assistant = await self._get_rest_assistant()
        response = await rest_assistant.call(request=request)

        symbols_details: Dict[str, Any] = await response.json()
        self._trading_rules.clear()
        self._trading_rules = self._format_trading_rules(symbols_details)

    def _format_trading_rules(self, symbols_details: Dict[str, Any]) -> Dict[str, TradingRule]:
        """
        Converts json API response into a dictionary of trading rules.
        :param symbols_details: The json API response
        :return A dictionary of trading rules.
        Response Example:
        {
            "code": 1000,
            "trace":"886fb6ae-456b-4654-b4e0-d681ac05cea1",
            "message": "OK",
            "data": {
                "symbols": [
                    {
                        "symbol":"GXC_BTC",
                         "symbol_id":1024,
                         "base_currency":"GXC",
                         "quote_currency":"BTC",
                         "quote_increment":"1.00000000",
                         "base_min_size":"1.00000000",
                         "base_max_size":"10000000.00000000",
                         "price_min_precision":6,
                         "price_max_precision":8,
                         "expiration":"NA",
                         "min_buy_amount":"0.00010000",
                         "min_sell_amount":"0.00010000"
                    },
                    ...
                ]
            }
        }
        """
        result = {}
        for rule in symbols_details["data"]["symbols"]:
            try:
                trading_pair = bitmart_utils.convert_from_exchange_trading_pair(rule["symbol"])
                price_decimals = Decimal(str(rule["price_max_precision"]))
                # E.g. a price decimal of 2 means 0.01 incremental.
                price_step = Decimal("1") / Decimal(str(math.pow(10, price_decimals)))
                result[trading_pair] = TradingRule(trading_pair=trading_pair,
                                                   min_order_size=Decimal(str(rule["base_min_size"])),
                                                   max_order_size=Decimal(str(rule["base_max_size"])),
                                                   min_order_value=Decimal(str(rule["min_buy_amount"])),
                                                   min_base_amount_increment=Decimal(str(rule["quote_increment"])),
                                                   min_price_increment=price_step)
            except Exception:
                self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True)
        return result

    async def _api_request(self,
                           method: str,
                           path_url: str,
                           params: Optional[Dict[str, Any]] = None,
                           auth_type: str = None) -> Dict[str, Any]:
        """
        Sends an aiohttp request and waits for a response.
        :param method: The HTTP method, e.g. get or post
        :param path_url: The path url or the API end point
        :param params: Request parameters
        :param auth_type: Type of Authorization header to send in request, from {"SIGNED", "KEYED", None}
        :returns A response in json format.
        """
        params = params or {}
        async with self._throttler.execute_task(path_url):
            url = f"{CONSTANTS.REST_URL}/{path_url}"

            headers = self._bitmart_auth.get_headers(bitmart_utils.get_ms_timestamp(), params, auth_type)

            if method == "get":
                request = RESTRequest(
                    method=RESTMethod.GET,
                    url=url,
                    headers=headers,
                    params=params
                )
                rest_assistant = await self._get_rest_assistant()
                response = await rest_assistant.call(request=request)
            elif method == "post":
                post_json = json.dumps(params)
                request = RESTRequest(
                    method=RESTMethod.POST,
                    url=url,
                    headers=headers,
                    data=post_json
                )
                rest_assistant = await self._get_rest_assistant()
                response = await rest_assistant.call(request=request)
            else:
                raise NotImplementedError

            try:
                parsed_response = json.loads(await response.text())
            except Exception as e:
                raise IOError(f"Error parsing data from {url}. Error: {str(e)}")
            if response.status != 200:
                raise IOError(f"Error calling {url}. HTTP status is {response.status}. "
                              f"Message: {parsed_response['message']}")
            if int(parsed_response["code"]) != 1000:
                raise IOError(f"{url} API call failed, error message: {parsed_response['message']}")
            return parsed_response

    def get_order_price_quantum(self, trading_pair: str, price: Decimal):
        """
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str, order_size: Decimal):
        """
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def buy(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET,
            price: Decimal = s_decimal_NaN, **kwargs) -> str:
        """
        Buys an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = bitmart_utils.get_new_client_order_id(True, trading_pair)
        safe_ensure_future(self._create_order(TradeType.BUY, order_id, trading_pair, amount, order_type, price))
        return order_id

    def sell(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET,
             price: Decimal = s_decimal_NaN, **kwargs) -> str:
        """
        Sells an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for SellOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to sell from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = bitmart_utils.get_new_client_order_id(False, trading_pair)
        safe_ensure_future(self._create_order(TradeType.SELL, order_id, trading_pair, amount, order_type, price))
        return order_id

    def cancel(self, trading_pair: str, order_id: str):
        """
        Cancel an order. This function returns immediately.
        To get the cancellation result, you'll have to wait for OrderCancelledEvent.
        :param trading_pair: The market (e.g. BTC-USDT) of the order.
        :param order_id: The internal order id (also called client_order_id)
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    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))

    def start_tracking_order(self,
                             order_id: str,
                             exchange_order_id: str,
                             trading_pair: str,
                             trade_type: TradeType,
                             price: Decimal,
                             amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by simply adding it into _in_flight_orders dictionary.
        """
        self._in_flight_orders[order_id] = BitmartInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount,
            creation_timestamp=self.current_timestamp
        )

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order by simply removing it from _in_flight_orders dictionary.
        """
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]

    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 cancel 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 canceled 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 _status_polling_loop(self):
        """
        Periodically update user balances and order status via REST API. This serves as a fallback measure for web
        socket API updates.
        """
        while True:
            try:
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = self.current_timestamp
                self._poll_notifier.clear()
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(str(e), exc_info=True)
                self.logger().network("Unexpected error while fetching account updates.",
                                      exc_info=True,
                                      app_warning_msg="Could not fetch account updates from BitMart. "
                                                      "Check API key and network connection.")
                await asyncio.sleep(0.5)

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances.
        """
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()
        account_info = await self._api_request("get", CONSTANTS.GET_ACCOUNT_SUMMARY_PATH_URL, {}, "KEYED")
        for account in account_info["data"]["wallet"]:
            asset_name = account["id"]
            self._account_available_balances[asset_name] = Decimal(str(account["available"]))
            self._account_balances[asset_name] = Decimal(str(account["available"])) + Decimal(str(account["frozen"]))
            remote_asset_names.add(asset_name)

        asset_names_to_remove = local_asset_names.difference(remote_asset_names)
        for asset_name in asset_names_to_remove:
            del self._account_available_balances[asset_name]
            del self._account_balances[asset_name]

        self._in_flight_orders_snapshot = {k: copy.copy(v) for k, v in self._in_flight_orders.items()}
        self._in_flight_orders_snapshot_timestamp = self.current_timestamp

    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 _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 canceled 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)

    async def _process_trade_message_rest(self, trade_msg: Dict[str, Any]):
        """
        Updates in-flight order and trigger order filled event for trade message received from REST API. Triggers order completed
        event if the total executed amount equals to the specified order amount.
        """
        for order in self._in_flight_orders.values():
            await order.get_exchange_order_id()
        track_order = [o for o in self._in_flight_orders.values() if str(trade_msg["order_id"]) == o.exchange_order_id]
        if not track_order:
            return
        tracked_order = track_order[0]
        (delta_trade_amount, delta_trade_price, trade_id) = tracked_order.update_with_trade_update_rest(trade_msg)
        if not delta_trade_amount:
            return
        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,
                delta_trade_price,
                delta_trade_amount,
                # TradeFee(0.0, [(trade_msg["fee_coin_name"], Decimal(str(trade_msg["fees"])))]),
                estimate_fee(self.name, tracked_order.order_type in [OrderType.LIMIT, OrderType.LIMIT_MAKER]),
                exchange_trade_id=trade_id
            )
        )
        if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or tracked_order.executed_amount_base >= tracked_order.amount:
            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 trade status rest 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.executed_amount_base,
                                           tracked_order.executed_amount_quote,
                                           tracked_order.order_type))
            self.stop_tracking_order(tracked_order.client_order_id)

    async def _process_trade_message_ws(self, trade_msg: Dict[str, Any]):
        """
        Updates in-flight order and trigger order filled event for order message received from WebSocket API. Triggers order completed
        event if the total executed amount equals to the specified order amount.
        """
        for order in self._in_flight_orders.values():
            await order.get_exchange_order_id()
        track_order = [o for o in self._in_flight_orders.values() if str(trade_msg["order_id"]) == o.exchange_order_id]
        if not track_order:
            return
        tracked_order = track_order[0]
        (delta_trade_amount, delta_trade_price, trade_id) = tracked_order.update_with_order_update_ws(trade_msg)
        if not delta_trade_amount:
            return
        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,
                delta_trade_price,
                delta_trade_amount,
                estimate_fee(self.name, tracked_order.order_type in [OrderType.LIMIT, OrderType.LIMIT_MAKER]),
                exchange_trade_id=trade_id
            )
        )
        if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or tracked_order.executed_amount_base >= tracked_order.amount:
            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 trade status ws 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.executed_amount_base,
                                           tracked_order.executed_amount_quote,
                                           tracked_order.order_type))
            self.stop_tracking_order(tracked_order.client_order_id)

    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 tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock's tick (1 second by default).
        It checks if status polling task is due for execution.
        """
        last_tick = int(self._last_timestamp / self.POLL_INTERVAL)
        current_tick = int(timestamp / self.POLL_INTERVAL)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN,
                is_maker: Optional[bool] = None) -> AddedToCostTradeFee:
        """
        To get trading fee, this function is simplified by using fee override configuration. Most parameters to this
        function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for
        maker order.
        """
        is_maker = order_type is OrderType.LIMIT_MAKER
        return AddedToCostTradeFee(percent=self.estimate_fee_pct(is_maker))

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg="Could not fetch user events from Bitmart. Check API key and network connection."
                )
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        """
        Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by
        BitmartAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                if "data" not in event_message:
                    continue
                for msg in event_message["data"]:     # data is a list
                    await self._process_order_message(msg)
                    await self._process_trade_message_ws(msg)
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().error("Unexpected error in user stream listener loop.", exc_info=True)
                await asyncio.sleep(5.0)

    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
Example #9
0
class MexcExchange(ExchangeBase):
    MARKET_RECEIVED_ASSET_EVENT_TAG = MarketEvent.ReceivedAsset
    MARKET_BUY_ORDER_COMPLETED_EVENT_TAG = MarketEvent.BuyOrderCompleted
    MARKET_SELL_ORDER_COMPLETED_EVENT_TAG = MarketEvent.SellOrderCompleted
    MARKET_WITHDRAW_ASSET_EVENT_TAG = MarketEvent.WithdrawAsset
    MARKET_ORDER_CANCELLED_EVENT_TAG = MarketEvent.OrderCancelled
    MARKET_TRANSACTION_FAILURE_EVENT_TAG = MarketEvent.TransactionFailure
    MARKET_ORDER_FAILURE_EVENT_TAG = MarketEvent.OrderFailure
    MARKET_ORDER_FILLED_EVENT_TAG = MarketEvent.OrderFilled
    MARKET_BUY_ORDER_CREATED_EVENT_TAG = MarketEvent.BuyOrderCreated
    MARKET_SELL_ORDER_CREATED_EVENT_TAG = MarketEvent.SellOrderCreated
    API_CALL_TIMEOUT = 10.0
    UPDATE_ORDERS_INTERVAL = 10.0
    SHORT_POLL_INTERVAL = 5.0
    MORE_SHORT_POLL_INTERVAL = 1.0
    LONG_POLL_INTERVAL = 120.0
    ORDER_LEN_LIMIT = 20

    _logger = None

    @classmethod
    def logger(cls) -> HummingbotLogger:
        if cls._logger is None:
            cls._logger = logging.getLogger(__name__)
        return cls._logger

    def __init__(
            self,
            mexc_api_key: str,
            mexc_secret_key: str,
            poll_interval: float = 5.0,
            order_book_tracker_data_source_type:
        OrderBookTrackerDataSourceType = OrderBookTrackerDataSourceType.
        EXCHANGE_API,
            trading_pairs: Optional[List[str]] = None,
            trading_required: bool = True):

        super().__init__()
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._shared_client = aiohttp.ClientSession()
        self._async_scheduler = AsyncCallScheduler(call_interval=0.5)
        self._data_source_type = order_book_tracker_data_source_type
        self._ev_loop = asyncio.get_event_loop()
        self._mexc_auth = MexcAuth(api_key=mexc_api_key,
                                   secret_key=mexc_secret_key)
        self._in_flight_orders = {}
        self._last_poll_timestamp = 0
        self._last_timestamp = 0
        self._order_book_tracker = MexcOrderBookTracker(
            throttler=self._throttler,
            trading_pairs=trading_pairs,
            shared_client=self._shared_client)
        self._poll_notifier = asyncio.Event()
        self._poll_interval = poll_interval
        self._status_polling_task = None
        self._trading_required = trading_required
        self._trading_rules = {}
        self._trading_rules_polling_task = None
        self._user_stream_tracker = MexcUserStreamTracker(
            throttler=self._throttler,
            mexc_auth=self._mexc_auth,
            trading_pairs=trading_pairs,
            shared_client=self._shared_client)
        self._user_stream_tracker_task = None
        self._user_stream_event_listener_task = None

    @property
    def name(self) -> str:
        return "mexc"

    @property
    def order_book_tracker(self) -> MexcOrderBookTracker:
        return self._order_book_tracker

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, MexcInFlightOrder]:
        return self._in_flight_orders

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, Any]:
        return {
            client_oid: order.to_json()
            for client_oid, order in self._in_flight_orders.items()
            if not order.is_done
        }

    def restore_tracking_states(self, saved_states: Dict[str, Any]):
        self._in_flight_orders.update({
            key: MexcInFlightOrder.from_json(value)
            for key, value in saved_states.items()
        })

    @property
    def shared_client(self) -> aiohttp.ClientSession:
        return self._shared_client

    @property
    def user_stream_tracker(self) -> MexcUserStreamTracker:
        return self._user_stream_tracker

    @shared_client.setter
    def shared_client(self, client: aiohttp.ClientSession):
        self._shared_client = client

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        await self.stop_network()
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(
            self._trading_rules_polling_loop())

        if self._trading_required:
            self._status_polling_task = safe_ensure_future(
                self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(
                self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(
                self._user_stream_event_listener())
            await self._update_balances()

    async def stop_network(self):
        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        try:
            resp = await self._api_request(method="GET",
                                           path_url=CONSTANTS.MEXC_PING_URL)
            if 'code' not in resp or resp['code'] != 200:
                raise Exception()
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    def tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock's tick (1 second by default).
        It checks if status polling task is due for execution.
        """
        # now = time.time()
        poll_interval = self.MORE_SHORT_POLL_INTERVAL
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    async def _http_client(self) -> aiohttp.ClientSession:
        if self._shared_client is None:
            self._shared_client = aiohttp.ClientSession()
        return self._shared_client

    async def _api_request(self,
                           method: str,
                           path_url: str,
                           params: Optional[Dict[str, Any]] = {},
                           data={},
                           is_auth_required: bool = False,
                           limit_id: Optional[str] = None) -> Dict[str, Any]:

        headers = {"Content-Type": "application/json"}
        if path_url in CONSTANTS.MEXC_PLACE_ORDER:
            headers.update({'source': 'HUMBOT'})
        client = await self._http_client()
        text_data = ujson.dumps(data) if data else None
        limit_id = limit_id or path_url
        path_url = self._mexc_auth.add_auth_to_params(method, path_url, params,
                                                      is_auth_required)
        url = urljoin(CONSTANTS.MEXC_BASE_URL, path_url)
        async with self._throttler.execute_task(limit_id):
            response_core = await client.request(
                method=method.upper(),
                url=url,
                headers=headers,
                # params=params if params else None, #mexc`s params  is already in the url
                data=text_data,
            )

        # async with response_core as response:
        if response_core.status != 200:
            raise IOError(
                f"Error request from {url}. Response: {await response_core.json()}."
            )
        try:
            parsed_response = await response_core.json()
            return parsed_response
        except Exception as ex:
            raise IOError(f"Error parsing data from {url}." + repr(ex))

    async def _update_balances(self):
        path_url = CONSTANTS.MEXC_BALANCE_URL
        msg = await self._api_request("GET",
                                      path_url=path_url,
                                      is_auth_required=True)
        if msg['code'] == 200:
            balances = msg['data']
        else:
            raise Exception(msg)
            self.logger().info(f" _update_balances error: {msg} ")
            return

        self._account_available_balances.clear()
        self._account_balances.clear()
        for k, balance in balances.items():
            # if Decimal(balance['frozen']) + Decimal(balance['available']) > Decimal(0.0001):
            self._account_balances[k] = Decimal(balance['frozen']) + Decimal(
                balance['available'])
            self._account_available_balances[k] = Decimal(balance['available'])

    async def _update_trading_rules(self):
        try:
            last_tick = int(self._last_timestamp / 60.0)
            current_tick = int(self.current_timestamp / 60.0)
            if current_tick > last_tick or len(self._trading_rules) < 1:
                exchange_info = await self._api_request(
                    "GET", path_url=CONSTANTS.MEXC_SYMBOL_URL)
                trading_rules_list = self._format_trading_rules(
                    exchange_info['data'])
                self._trading_rules.clear()
                for trading_rule in trading_rules_list:
                    self._trading_rules[
                        trading_rule.trading_pair] = trading_rule
        except Exception as ex:
            self.logger().error("Error _update_trading_rules:" + str(ex),
                                exc_info=True)

    def _format_trading_rules(
            self, raw_trading_pair_info: List[Dict[str,
                                                   Any]]) -> List[TradingRule]:
        trading_rules = []
        for info in raw_trading_pair_info:
            try:
                trading_rules.append(
                    TradingRule(
                        trading_pair=convert_from_exchange_trading_pair(
                            info['symbol']),
                        # min_order_size=Decimal(info["min_amount"]),
                        # max_order_size=Decimal(info["max_amount"]),
                        min_price_increment=Decimal(
                            num_to_increment(info["price_scale"])),
                        min_base_amount_increment=Decimal(
                            num_to_increment(info["quantity_scale"])),
                        # min_quote_amount_increment=Decimal(info["1e-{info['value-precision']}"]),
                        # min_notional_size=Decimal(info["min-order-value"])
                        min_notional_size=Decimal(info["min_amount"]),
                        # max_notional_size=Decimal(info["max_amount"]),
                    ))
            except Exception:
                self.logger().error(
                    f"Error parsing the trading pair rule {info}. Skipping.",
                    exc_info=True)
        return trading_rules

    async def get_order_status(self, exchangge_order_id: str,
                               trading_pair: str) -> Dict[str, Any]:
        params = {"order_ids": exchangge_order_id}
        msg = await self._api_request(
            "GET",
            path_url=CONSTANTS.MEXC_ORDER_DETAILS_URL,
            params=params,
            is_auth_required=True)

        if msg["code"] == 200:
            return msg['data'][0]

    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)

    def _reset_poll_notifier(self):
        self._poll_notifier = asyncio.Event()

    async def _status_polling_loop(self):
        while True:
            try:
                self._reset_poll_notifier()
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = self.current_timestamp
            except asyncio.CancelledError:
                raise
            except Exception as ex:
                self.logger().network(
                    "Unexpected error while fetching account updates." +
                    repr(ex),
                    exc_info=True,
                    app_warning_msg="Could not fetch account updates from MEXC. "
                    "Check API key and network connection.")
                await asyncio.sleep(0.5)

    async def _trading_rules_polling_loop(self):
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(60)
            except asyncio.CancelledError:
                raise
            except Exception as ex:
                self.logger().network(
                    "Unexpected error while fetching trading rules." +
                    repr(ex),
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch new trading rules from MEXC. "
                    "Check network connection.")
                await asyncio.sleep(0.5)

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, Any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception as ex:
                self.logger().error(
                    f"Unknown error. Retrying after 1 second. {ex}",
                    exc_info=True)
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        async for stream_message in self._iter_user_event_queue():
            # self.logger().info(f"stream_message:{stream_message}")
            try:
                if 'channel' in stream_message.keys(
                ) and stream_message['channel'] == 'push.personal.account':
                    continue
                elif 'channel' in stream_message.keys(
                ) and stream_message['channel'] == 'push.personal.order':
                    await self._process_order_message(stream_message)
                else:
                    self.logger().debug(
                        f"Unknown event received from the connector ({stream_message})"
                    )
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(
                    f"Unexpected error in user stream listener lopp. {e}",
                    exc_info=True)
                await asyncio.sleep(5.0)

    async def _process_order_message(self, stream_message: Dict[str, Any]):
        client_order_id = stream_message["data"]["clientOrderId"]
        # trading_pair = convert_from_exchange_trading_pair(stream_message["symbol"])
        # 1:NEW,2:FILLED,3:PARTIALLY_FILLED,4:CANCELED,5:PARTIALLY_CANCELED
        order_status = ws_order_status_convert_to_str(
            stream_message["data"]["status"])
        tracked_order = self._in_flight_orders.get(client_order_id, None)
        if tracked_order is None:
            return
        # Update balance in time
        await self._update_balances()

        if order_status in {"FILLED", "PARTIALLY_FILLED"}:
            executed_amount = Decimal(str(
                stream_message["data"]['quantity'])) - Decimal(
                    str(stream_message["data"]['remainQuantity']))
            execute_price = Decimal(str(stream_message["data"]['price']))
            execute_amount_diff = executed_amount - tracked_order.executed_amount_base
            if execute_amount_diff > s_decimal_0:
                tracked_order.executed_amount_base = executed_amount
                tracked_order.executed_amount_quote = Decimal(
                    str(stream_message["data"]['amount'])) - Decimal(
                        str(stream_message["data"]['remainAmount']))

                current_fee = self.get_fee(tracked_order.base_asset,
                                           tracked_order.quote_asset,
                                           tracked_order.order_type,
                                           tracked_order.trade_type,
                                           execute_amount_diff, execute_price)
                self.logger().info(
                    f"Filled {execute_amount_diff} out of {tracked_order.amount} of "
                )
                self.trigger_event(
                    self.MARKET_ORDER_FILLED_EVENT_TAG,
                    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,
                        current_fee,
                        exchange_trade_id=tracked_order.exchange_order_id))
        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
            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 websocket 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 websocket 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))
            self.stop_tracking_order(tracked_order.client_order_id)
            return

        if order_status == "CANCELED" or order_status == "PARTIALLY_CANCELED":
            tracked_order.last_state = order_status
            self.logger().info(
                f"Order {tracked_order.client_order_id} has been cancelled "
                f"according to order delta websocket API.")
            self.trigger_event(
                self.MARKET_ORDER_CANCELLED_EVENT_TAG,
                OrderCancelledEvent(self.current_timestamp,
                                    tracked_order.client_order_id))
            self.stop_tracking_order(tracked_order.client_order_id)

    @property
    def status_dict(self) -> Dict[str, bool]:
        return {
            "order_books_initialized":
            self._order_book_tracker.ready,
            "acount_balance":
            len(self._account_balances) > 0
            if self._trading_required else True,
            "trading_rule_initialized":
            len(self._trading_rules) > 0
        }

    def supported_order_types(self):
        return [OrderType.LIMIT, OrderType.MARKET]

    @property
    def ready(self) -> bool:
        return all(self.status_dict.values())

    async def place_order(self, order_id: str, trading_pair: str,
                          amount: Decimal, is_buy: bool, order_type: OrderType,
                          price: Decimal) -> str:

        if order_type is OrderType.LIMIT:
            order_type_str = "LIMIT_ORDER"
        elif order_type is OrderType.LIMIT_MAKER:
            order_type_str = "POST_ONLY"

        data = {
            'client_order_id': order_id,
            'order_type': order_type_str,
            'trade_type': "BID" if is_buy else "ASK",
            'symbol': convert_to_exchange_trading_pair(trading_pair),
            'quantity': str(amount),
            'price': str(price)
        }

        exchange_order_id = await self._api_request(
            "POST",
            path_url=CONSTANTS.MEXC_PLACE_ORDER,
            params={},
            data=data,
            is_auth_required=True)

        return str(exchange_order_id.get('data'))

    async def execute_buy(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"Buy order amount {decimal_amount} is lower than the notional size "
            )
        try:
            exchange_order_id = await self.place_order(order_id, trading_pair,
                                                       decimal_amount, True,
                                                       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.BUY,
                                      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()} buy order {order_id} for {decimal_amount} {trading_pair}."
                )
            self.trigger_event(
                self.MARKET_BUY_ORDER_CREATED_EVENT_TAG,
                BuyOrderCreatedEvent(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 buy {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}." + repr(ex),
                exc_info=True,
                app_warning_msg=
                "Failed to submit buy 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))

    def buy(self,
            trading_pair: str,
            amount: Decimal,
            order_type=OrderType.MARKET,
            price: Decimal = s_decimal_NaN,
            **kwargs) -> str:
        tracking_nonce = int(get_tracking_nonce())
        order_id = str(f"buy-{trading_pair}-{tracking_nonce}")
        safe_ensure_future(
            self.execute_buy(order_id, trading_pair, amount, order_type,
                             price))
        return order_id

    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))

    def sell(self,
             trading_pair: str,
             amount: Decimal,
             order_type: OrderType = OrderType.MARKET,
             price: Decimal = s_decimal_NaN,
             **kwargs) -> str:

        tracking_nonce = int(get_tracking_nonce())
        order_id = str(f"sell-{trading_pair}-{tracking_nonce}")

        safe_ensure_future(
            self.execute_sell(order_id, trading_pair, amount, order_type,
                              price))
        return order_id

    async def execute_cancel(self, trading_pair: str, client_order_id: str):
        try:
            tracked_order = self._in_flight_orders.get(client_order_id)
            if tracked_order is None:
                # raise ValueError(f"Failed to cancel order - {client_order_id}. Order not found.")
                self.logger().network(
                    f"Failed to cancel order - {client_order_id}. Order not found."
                )
                return
            params = {
                "client_order_ids": client_order_id,
            }
            response = await self._api_request(
                "DELETE",
                path_url=CONSTANTS.MEXC_ORDER_CANCEL,
                params=params,
                is_auth_required=True)

            if not response['code'] == 200:
                raise MexcAPIError("Order could not be canceled")

        except MexcAPIError as ex:
            self.logger().network(
                f"Failed to cancel order {client_order_id} : {repr(ex)}",
                exc_info=True,
                app_warning_msg=
                f"Failed to cancel the order {client_order_id} on Mexc. "
                f"Check API key and network connection.")

    def cancel(self, trading_pair: str, order_id: str):
        safe_ensure_future(self.execute_cancel(trading_pair, order_id))
        return order_id

    async def cancel_all(self,
                         timeout_seconds: float) -> List[CancellationResult]:
        orders_by_trading_pair = {}

        for order in self._in_flight_orders.values():
            orders_by_trading_pair[
                order.trading_pair] = orders_by_trading_pair.get(
                    order.trading_pair, [])
            orders_by_trading_pair[order.trading_pair].append(order)

        if len(orders_by_trading_pair) == 0:
            return []

        for trading_pair in orders_by_trading_pair:
            cancel_order_ids = [
                o.exchange_order_id
                for o in orders_by_trading_pair[trading_pair]
            ]
            is_need_loop = True
            while is_need_loop:
                if len(cancel_order_ids) > self.ORDER_LEN_LIMIT:
                    is_need_loop = True
                    this_turn_cancel_order_ids = cancel_order_ids[:self.
                                                                  ORDER_LEN_LIMIT]
                    cancel_order_ids = cancel_order_ids[self.ORDER_LEN_LIMIT:]
                else:
                    this_turn_cancel_order_ids = cancel_order_ids
                    is_need_loop = False
                self.logger().debug(
                    f"cancel_order_ids {this_turn_cancel_order_ids} orders_by_trading_pair[trading_pair]"
                )
                params = {
                    'order_ids':
                    quote(','.join([o for o in this_turn_cancel_order_ids])),
                }

                cancellation_results = []
                try:
                    cancel_all_results = await self._api_request(
                        "DELETE",
                        path_url=CONSTANTS.MEXC_ORDER_CANCEL,
                        params=params,
                        is_auth_required=True)

                    for order_result_client_order_id, order_result_value in cancel_all_results[
                            'data'].items():
                        for o in orders_by_trading_pair[trading_pair]:
                            if o.client_order_id == order_result_client_order_id:
                                result_bool = True if order_result_value == "invalid order state" or order_result_value == "success" else False
                                cancellation_results.append(
                                    CancellationResult(o.client_order_id,
                                                       result_bool))
                                if result_bool:
                                    self.trigger_event(
                                        self.MARKET_ORDER_CANCELLED_EVENT_TAG,
                                        OrderCancelledEvent(
                                            self.current_timestamp,
                                            order_id=o.client_order_id,
                                            exchange_order_id=o.
                                            exchange_order_id))
                                    self.stop_tracking_order(o.client_order_id)

                except Exception as ex:

                    self.logger().network(
                        f"Failed to cancel all orders: {this_turn_cancel_order_ids}"
                        + repr(ex),
                        exc_info=True,
                        app_warning_msg=
                        "Failed to cancel all orders on Mexc. Check API key and network connection."
                    )
        return cancellation_results

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def start_tracking_order(self, order_id: str,
                             exchange_order_id: Optional[str],
                             trading_pair: str, trade_type: TradeType,
                             price: Decimal, amount: Decimal,
                             order_type: OrderType):
        self._in_flight_orders[order_id] = MexcInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount)

    def stop_tracking_order(self, order_id: str):
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]

    def get_order_price_quantum(self, trading_pair: str,
                                price: Decimal) -> Decimal:
        """
        Used by quantize_order_price() in _create_order()
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str,
                               order_size: Decimal) -> Decimal:
        """
        Used by quantize_order_price() in _create_order()
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def quantize_order_amount(self,
                              trading_pair: str,
                              amount: Decimal,
                              price: Decimal = s_decimal_0) -> Decimal:

        trading_rule = self._trading_rules[trading_pair]

        quantized_amount = ExchangeBase.quantize_order_amount(
            self, trading_pair, amount)
        current_price = self.get_price(trading_pair, False)

        calc_price = current_price if price == s_decimal_0 else price

        notional_size = calc_price * quantized_amount

        if notional_size < trading_rule.min_notional_size * Decimal("1"):
            return s_decimal_0

        return quantized_amount

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN,
                is_maker: Optional[bool] = None) -> AddedToCostTradeFee:
        is_maker = order_type is OrderType.LIMIT_MAKER
        return AddedToCostTradeFee(percent=self.estimate_fee_pct(is_maker))

    async def get_deal_detail_fee(self, order_id: str) -> Dict[str, Any]:
        params = {
            'order_id': order_id,
        }
        msg = await self._api_request("GET",
                                      path_url=CONSTANTS.MEXC_DEAL_DETAIL,
                                      params=params,
                                      is_auth_required=True)
        fee = s_decimal_0
        fee_currency = None
        if msg['code'] == 200:
            balances = msg['data']
        else:
            raise Exception(msg)
        for order in balances:
            fee += Decimal(order['fee'])
            fee_currency = order['fee_currency']
        return fee, fee_currency
Example #10
0
class CryptoComExchange(ExchangeBase):
    """
    CryptoComExchange connects with Crypto.com exchange and provides order book pricing, user account tracking and
    trading functionality.
    """
    API_CALL_TIMEOUT = 10.0
    SHORT_POLL_INTERVAL = 5.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    LONG_POLL_INTERVAL = 120.0

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global ctce_logger
        if ctce_logger is None:
            ctce_logger = logging.getLogger(__name__)
        return ctce_logger

    def __init__(self,
                 crypto_com_api_key: str,
                 crypto_com_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True
                 ):
        """
        :param crypto_com_api_key: The API key to connect to private Crypto.com APIs.
        :param crypto_com_secret_key: The API secret.
        :param trading_pairs: The market trading pairs which to track order book data.
        :param trading_required: Whether actual trading is needed.
        """
        super().__init__()
        self._trading_required = trading_required
        self._trading_pairs = trading_pairs
        self._crypto_com_auth = CryptoComAuth(crypto_com_api_key, crypto_com_secret_key)
        self._shared_client = aiohttp.ClientSession()
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)

        self._order_book_tracker = CryptoComOrderBookTracker(shared_client=self._shared_client,
                                                             throttler=self._throttler,
                                                             trading_pairs=trading_pairs,
                                                             )
        self._user_stream_tracker = CryptoComUserStreamTracker(crypto_com_auth=self._crypto_com_auth,
                                                               shared_client=self._shared_client,
                                                               )
        self._ev_loop = asyncio.get_event_loop()
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders = {}  # Dict[client_order_id:str, CryptoComInFlightOrder]
        self._order_not_found_records = {}  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, TradingRule]
        self._status_polling_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._last_poll_timestamp = 0

    @property
    def name(self) -> str:
        return "crypto_com"

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, CryptoComInFlightOrder]:
        return self._in_flight_orders

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        A dictionary of statuses of various connector's components.
        """
        return {
            "order_books_initialized": self._order_book_tracker.ready,
            "account_balance": len(self._account_balances) > 0 if self._trading_required else True,
            "trading_rule_initialized": len(self._trading_rules) > 0,
            "user_stream_initialized":
                self._user_stream_tracker.data_source.ready > 0 if self._trading_required else True,
        }

    @property
    def ready(self) -> bool:
        """
        :return True when all statuses pass, this might take 5-10 seconds for all the connector's components and
        services to be ready.
        """
        return all(self.status_dict.values())

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, any]:
        """
        :return active in-flight orders in json format, is used to save in sqlite db.
        """
        return {
            key: value.to_json()
            for key, value in self._in_flight_orders.items()
            if not value.is_done
        }

    def restore_tracking_states(self, saved_states: Dict[str, any]):
        """
        Restore in-flight orders from saved tracking states, this is st the connector can pick up on where it left off
        when it disconnects.
        :param saved_states: The saved tracking_states.
        """
        self._in_flight_orders.update({
            key: CryptoComInFlightOrder.from_json(value)
            for key, value in saved_states.items()
        })

    def supported_order_types(self) -> List[OrderType]:
        """
        :return a list of OrderType supported by this connector.
        Note that Market order type is no longer required and will not be used.
        """
        return [OrderType.LIMIT, OrderType.LIMIT_MAKER]

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(self._trading_rules_polling_loop())
        if self._trading_required:
            self._status_polling_task = safe_ensure_future(self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        """
        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        This function is required by NetworkIterator base class and is called periodically to check
        the network connection. Simply ping the network (or call any light weight public API).
        """
        try:
            # since there is no ping endpoint, the lowest rate call is to get BTC-USDT ticker
            await self._api_request("get", CONSTANTS.GET_TICKER_PATH_URL)
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    async def _http_client(self) -> aiohttp.ClientSession:
        """
        :returns Shared client session instance
        """
        if self._shared_client is None:
            self._shared_client = aiohttp.ClientSession()
        return self._shared_client

    async def _trading_rules_polling_loop(self):
        """
        Periodically update trading rule.
        """
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(60)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().network(f"Unexpected error while fetching trading rules. Error: {str(e)}",
                                      exc_info=True,
                                      app_warning_msg="Could not fetch new trading rules from Crypto.com. "
                                                      "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        instruments_info = await self._api_request("get", path_url=CONSTANTS.GET_TRADING_RULES_PATH_URL)
        self._trading_rules.clear()
        self._trading_rules = self._format_trading_rules(instruments_info)

    def _format_trading_rules(self, instruments_info: Dict[str, Any]) -> Dict[str, TradingRule]:
        """
        Converts json API response into a dictionary of trading rules.
        :param instruments_info: The json API response
        :return A dictionary of trading rules.
        Response Example:
        {
            "id": 11,
            "method": "public/get-instruments",
            "code": 0,
            "result": {
                "instruments": [
                      {
                        "instrument_name": "ETH_CRO",
                        "quote_currency": "CRO",
                        "base_currency": "ETH",
                        "price_decimals": 2,
                        "quantity_decimals": 2
                      },
                      {
                        "instrument_name": "CRO_BTC",
                        "quote_currency": "BTC",
                        "base_currency": "CRO",
                        "price_decimals": 8,
                        "quantity_decimals": 2
                      }
                    ]
              }
        }
        """
        result = {}
        for rule in instruments_info["result"]["instruments"]:
            try:
                trading_pair = crypto_com_utils.convert_from_exchange_trading_pair(rule["instrument_name"])
                price_decimals = Decimal(str(rule["price_decimals"]))
                quantity_decimals = Decimal(str(rule["quantity_decimals"]))
                # E.g. a price decimal of 2 means 0.01 incremental.
                price_step = Decimal("1") / Decimal(str(math.pow(10, price_decimals)))
                quantity_step = Decimal("1") / Decimal(str(math.pow(10, quantity_decimals)))
                result[trading_pair] = TradingRule(trading_pair,
                                                   min_price_increment=price_step,
                                                   min_base_amount_increment=quantity_step)
            except Exception:
                self.logger().error(f"Error parsing the trading pair rule {rule}. Skipping.", exc_info=True)
        return result

    async def _api_request(self,
                           method: str,
                           path_url: str,
                           params: Dict[str, Any] = {},
                           is_auth_required: bool = False) -> Dict[str, Any]:
        """
        Sends an aiohttp request and waits for a response.
        :param method: The HTTP method, e.g. get or post
        :param path_url: The path url or the API end point
        :param is_auth_required: Whether an authentication is required, when True the function will add encrypted
        signature to the request.
        :returns A response in json format.
        """
        async with self._throttler.execute_task(path_url):
            url = crypto_com_utils.get_rest_url(path_url)
            client = await self._http_client()
            if is_auth_required:
                request_id = crypto_com_utils.RequestId.generate_request_id()
                data = {"params": params}
                params = self._crypto_com_auth.generate_auth_dict(path_url, request_id,
                                                                  crypto_com_utils.get_ms_timestamp(), data)
                headers = self._crypto_com_auth.get_headers()
            else:
                headers = {"Content-Type": "application/json"}

            if method == "get":
                response = await client.get(url, headers=headers)
            elif method == "post":
                post_json = json.dumps(params)
                response = await client.post(url, data=post_json, headers=headers)
            else:
                raise NotImplementedError

            try:
                parsed_response = json.loads(await response.text())
            except Exception as e:
                raise IOError(f"Error parsing data from {url}. Error: {str(e)}")
            if response.status != 200:
                raise IOError(f"Error fetching data from {url}. HTTP status is {response.status}. "
                              f"Message: {parsed_response}")
            if parsed_response["code"] != 0:
                raise IOError(f"{url} API call failed, response: {parsed_response}")
            return parsed_response

    def get_order_price_quantum(self, trading_pair: str, price: Decimal):
        """
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str, order_size: Decimal):
        """
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def buy(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET,
            price: Decimal = s_decimal_NaN, **kwargs) -> str:
        """
        Buys an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = crypto_com_utils.get_new_client_order_id(True, trading_pair)
        safe_ensure_future(self._create_order(TradeType.BUY, order_id, trading_pair, amount, order_type, price))
        return order_id

    def sell(self, trading_pair: str, amount: Decimal, order_type=OrderType.MARKET,
             price: Decimal = s_decimal_NaN, **kwargs) -> str:
        """
        Sells an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for SellOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to sell from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = crypto_com_utils.get_new_client_order_id(False, trading_pair)
        safe_ensure_future(self._create_order(TradeType.SELL, order_id, trading_pair, amount, order_type, price))
        return order_id

    def cancel(self, trading_pair: str, order_id: str):
        """
        Cancel an order. This function returns immediately.
        To get the cancellation result, you'll have to wait for OrderCancelledEvent.
        :param trading_pair: The market (e.g. BTC-USDT) of the order.
        :param order_id: The internal order id (also called client_order_id)
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    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", CONSTANTS.CREATE_ORDER_PATH_URL, 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,
                                   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 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))

    def start_tracking_order(self,
                             order_id: str,
                             exchange_order_id: str,
                             trading_pair: str,
                             trade_type: TradeType,
                             price: Decimal,
                             amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by simply adding it into _in_flight_orders dictionary.
        """
        self._in_flight_orders[order_id] = CryptoComInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount,
            creation_timestamp=self.current_timestamp
        )

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order by simply removing it from _in_flight_orders dictionary.
        """
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]

    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
            await self._api_request(
                "post",
                CONSTANTS.CANCEL_ORDER_PATH_URL,
                {"instrument_name": crypto_com_utils.convert_to_exchange_trading_pair(trading_pair),
                 "order_id": ex_order_id},
                True
            )
            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 CryptoCom. "
                                f"Check API key and network connection."
            )

    async def _status_polling_loop(self):
        """
        Periodically update user balances and order status via REST API. This serves as a fallback measure for web
        socket API updates.
        """
        while True:
            try:
                self._poll_notifier = asyncio.Event()
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = self.current_timestamp
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(str(e), exc_info=True)
                self.logger().network("Unexpected error while fetching account updates.",
                                      exc_info=True,
                                      app_warning_msg="Could not fetch account updates from Crypto.com. "
                                                      "Check API key and network connection.")
                await asyncio.sleep(0.5)

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances.
        """
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()
        account_info = await self._api_request("post", CONSTANTS.GET_ACCOUNT_SUMMARY_PATH_URL, {}, True)
        for account in account_info["result"]["accounts"]:
            asset_name = account["currency"]
            self._account_available_balances[asset_name] = Decimal(str(account["available"]))
            self._account_balances[asset_name] = Decimal(str(account["balance"]))
            remote_asset_names.add(asset_name)

        asset_names_to_remove = local_asset_names.difference(remote_asset_names)
        for asset_name in asset_names_to_remove:
            del self._account_available_balances[asset_name]
            del self._account_balances[asset_name]

    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()
                tasks.append(self._api_request("post",
                                               CONSTANTS.GET_ORDER_DETAIL_PATH_URL,
                                               {"order_id": order_id},
                                               True))
            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 "result" not in response:
                    self.logger().info(f"_update_order_status result not in resp: {response}")
                    continue
                result = response["result"]
                if "trade_list" in result:
                    for trade_msg in result["trade_list"]:
                        await self._process_trade_message(trade_msg)
                self._process_order_message(result["order_info"])

    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 canceled 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 _process_trade_message(self, trade_msg: Dict[str, Any]):
        """
        Updates in-flight order and trigger order filled event for trade message received. Triggers order completed
        event if the total executed amount equals to the specified order amount.
        """
        for order in self._in_flight_orders.values():
            await order.get_exchange_order_id()
        track_order = [o for o in self._in_flight_orders.values() if trade_msg["order_id"] == o.exchange_order_id]
        if not track_order:
            return
        tracked_order = track_order[0]
        updated = tracked_order.update_with_trade_update(trade_msg)
        if not updated:
            return
        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(trade_msg["traded_price"])),
                Decimal(str(trade_msg["traded_quantity"])),
                AddedToCostTradeFee(
                    flat_fees=[TokenAmount(trade_msg["fee_currency"], Decimal(str(trade_msg["fee"])))]
                ),
                exchange_trade_id=str(trade_msg.get("trade_id", int(self._time() * 1e6)))
            )
        )
        if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or \
                tracked_order.executed_amount_base >= tracked_order.amount:
            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.executed_amount_base,
                                           tracked_order.executed_amount_quote,
                                           tracked_order.order_type))
            self.stop_tracking_order(tracked_order.client_order_id)

    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.")
        tracked_orders: Dict[str, CryptoComInFlightOrder] = self._in_flight_orders.copy().items()
        cancellation_results = []
        try:
            tasks = []

            for _, order in tracked_orders:
                api_params = {
                    "instrument_name": crypto_com_utils.convert_to_exchange_trading_pair(order.trading_pair),
                    "order_id": order.exchange_order_id,
                }
                tasks.append(self._api_request(method="post",
                                               path_url=CONSTANTS.CANCEL_ORDER_PATH_URL,
                                               params=api_params,
                                               is_auth_required=True))

            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 Crypto.com. Check API key and network connection."
            )
        return cancellation_results

    def tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock's tick (1 second by default).
        It checks if status polling task is due for execution.
        """
        now = time.time()
        poll_interval = (self.SHORT_POLL_INTERVAL
                         if now - self._user_stream_tracker.last_recv_time > 60.0
                         else self.LONG_POLL_INTERVAL)
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN,
                is_maker: Optional[bool] = None) -> AddedToCostTradeFee:
        """
        To get trading fee, this function is simplified by using fee override configuration. Most parameters to this
        function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for
        maker order.
        """
        is_maker = order_type is OrderType.LIMIT_MAKER
        return AddedToCostTradeFee(percent=self.estimate_fee_pct(is_maker))

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg="Could not fetch user events from CryptoCom. Check API key and network connection."
                )
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        """
        Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by
        CryptoComAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                if "result" not in event_message or "channel" not in event_message["result"]:
                    continue
                channel = event_message["result"]["channel"]
                if "user.trade" in channel:
                    for trade_msg in event_message["result"]["data"]:
                        await self._process_trade_message(trade_msg)
                elif "user.order" in channel:
                    for order_msg in event_message["result"]["data"]:
                        self._process_order_message(order_msg)
                elif channel == "user.balance":
                    balances = event_message["result"]["data"]
                    for balance_entry in balances:
                        asset_name = balance_entry["currency"]
                        self._account_balances[asset_name] = Decimal(str(balance_entry["balance"]))
                        self._account_available_balances[asset_name] = Decimal(str(balance_entry["available"]))
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().error("Unexpected error in user stream listener loop.", exc_info=True)
                await asyncio.sleep(5.0)

    async def get_open_orders(self) -> List[OpenOrder]:
        result = await self._api_request(
            "post",
            CONSTANTS.GET_OPEN_ORDERS_PATH_URL,
            {},
            True
        )
        ret_val = []
        for order in result["result"]["order_list"]:
            if crypto_com_utils.HBOT_BROKER_ID not in order["client_oid"]:
                continue
            if order["type"] != "LIMIT":
                raise Exception(f"Unsupported order type {order['type']}")
            ret_val.append(
                OpenOrder(
                    client_order_id=order["client_oid"],
                    trading_pair=crypto_com_utils.convert_from_exchange_trading_pair(order["instrument_name"]),
                    price=Decimal(str(order["price"])),
                    amount=Decimal(str(order["quantity"])),
                    executed_amount=Decimal(str(order["cumulative_quantity"])),
                    status=order["status"],
                    order_type=OrderType.LIMIT,
                    is_buy=True if order["side"].lower() == "buy" else False,
                    time=int(order["create_time"]),
                    exchange_order_id=order["order_id"]
                )
            )
        return ret_val
class WazirxAPIUserStreamDataSource(UserStreamTrackerDataSource):
    MESSAGE_TIMEOUT = 30.0
    PING_TIMEOUT = 10.0

    _logger: Optional[HummingbotLogger] = None

    @classmethod
    def logger(cls) -> HummingbotLogger:
        if cls._logger is None:
            cls._logger = logging.getLogger(__name__)
        return cls._logger

    def __init__(self, wazirx_auth: WazirxAuth):
        super().__init__()
        self._wazirx_auth: WazirxAuth = wazirx_auth
        self._last_recv_time: float = 0
        self._auth_successful_event = asyncio.Event()
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._shared_http_client = None

    @property
    def ready(self) -> bool:
        return self._last_recv_time > 0 and self._auth_successful_event.is_set(
        )

    @property
    def last_recv_time(self) -> float:
        return self._last_recv_time

    async def _http_client(self) -> aiohttp.ClientSession:
        """
        :returns Shared client session instance
        """
        if self._shared_http_client is None:
            self._shared_http_client = aiohttp.ClientSession()
        return self._shared_http_client

    # generate auth token for private channels
    async def _get_wss_auth_token(self):
        async with self._throttler.execute_task(
                CONSTANTS.CREATE_WSS_AUTH_TOKEN):
            client = await self._http_client()
            url = f"{CONSTANTS.WAZIRX_API_BASE}/{CONSTANTS.CREATE_WSS_AUTH_TOKEN}"
            params = {}
            params = self._wazirx_auth.get_auth(params)
            headers = self._wazirx_auth.get_headers()
            response = await client.post(url, headers=headers, data=params)
            parsed_response = json.loads(await response.text())
            self._auth_successful_event.set()
            return parsed_response["auth_key"]

    async def _create_websocket_connection(
            self) -> aiohttp.ClientWebSocketResponse:
        """
        Initialize WebSocket client for APIOrderBookDataSource
        """
        try:
            return await aiohttp.ClientSession().ws_connect(
                url=CONSTANTS.WSS_URL)
        except asyncio.CancelledError:
            raise
        except Exception as e:
            self.logger().network(
                f"Unexpected error occured when connecting to WebSocket server. "
                f"Error: {e}")
            raise

    async def _iter_messages(
            self, ws: aiohttp.ClientWebSocketResponse) -> AsyncIterable[Any]:
        try:
            while True:
                yield await ws.receive_json()
        except asyncio.CancelledError:
            raise
        except Exception as e:
            self.logger().network(
                f"Unexpected error occured when parsing websocket payload. "
                f"Error: {e}")
            raise
        finally:
            await ws.close()

    async def listen_for_user_stream(self, output: asyncio.Queue):
        """
        Subscribe to active orders via web socket
        """
        while True:
            try:
                ws = await self._create_websocket_connection()
                subscribe_request: Dict[str, Any] = {
                    "event":
                    "subscribe",
                    "streams":
                    ["outboundAccountPosition", "orderUpdate", "ownTrade"],
                    "auth_key":
                    await self._get_wss_auth_token(),
                }
                await ws.send_json(subscribe_request)

                async for json_msg in self._iter_messages(ws):
                    self._last_recv_time = time.time()
                    output.put_nowait(json_msg)

            except asyncio.CancelledError:
                raise
            except asyncio.TimeoutError:
                self.logger().warning(
                    "WebSocket ping timed out. Reconnecting after 5 seconds..."
                )
            except Exception:
                self.logger().error(
                    "Unexpected error while maintaining the user event listen key. Retrying after "
                    "5 seconds...",
                    exc_info=True)
            finally:
                self.logger().info("Closing")
                await ws.close()
                await asyncio.sleep(5)
class AscendExExchange(ExchangePyBase):
    """
    AscendExExchange connects with AscendEx exchange and provides order book pricing, user account tracking and
    trading functionality.
    """
    API_CALL_TIMEOUT = 10.0
    SHORT_POLL_INTERVAL = 5.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    LONG_POLL_INTERVAL = 10.0

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global ctce_logger
        if ctce_logger is None:
            ctce_logger = logging.getLogger(__name__)
        return ctce_logger

    def __init__(self,
                 ascend_ex_api_key: str,
                 ascend_ex_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True):
        """
        :param ascend_ex_api_key: The API key to connect to private AscendEx APIs.
        :param ascend_ex_secret_key: The API secret.
        :param trading_pairs: The market trading pairs which to track order book data.
        :param trading_required: Whether actual trading is needed.
        """
        super().__init__()
        self._trading_required = trading_required
        self._trading_pairs = trading_pairs
        self._shared_client = aiohttp.ClientSession()
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._order_book_tracker = AscendExOrderBookTracker(
            shared_client=self._shared_client,
            throttler=self._throttler,
            trading_pairs=self._trading_pairs)
        self._ascend_ex_auth = AscendExAuth(ascend_ex_api_key,
                                            ascend_ex_secret_key)
        self._user_stream_tracker = AscendExUserStreamTracker(
            shared_client=self._shared_client,
            throttler=self._throttler,
            ascend_ex_auth=self._ascend_ex_auth,
            trading_pairs=self._trading_pairs)
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders = {
        }  # Dict[client_order_id:str, AscendExInFlightOrder]
        self._order_not_found_records = {
        }  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, AscendExTradingRule]
        self._status_polling_task = None
        self._user_stream_tracker_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._last_poll_timestamp = 0
        self._account_group = None  # required in order to make post requests
        self._account_uid = None  # required in order to produce deterministic order ids
        self._throttler = AsyncThrottler(rate_limits=CONSTANTS.RATE_LIMITS)

    @property
    def name(self) -> str:
        return CONSTANTS.EXCHANGE_NAME

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, AscendExTradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, AscendExInFlightOrder]:
        return self._in_flight_orders

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        A dictionary of statuses of various connector's components.
        """
        return {
            "order_books_initialized":
            self._order_book_tracker.ready,
            "account_balance":
            len(self._account_balances) > 0
            if self._trading_required else True,
            "trading_rule_initialized":
            len(self._trading_rules) > 0,
            "user_stream_initialized":
            self._user_stream_tracker.data_source.last_recv_time > 0
            if self._trading_required else True,
            "account_data":
            self._account_group is not None and self._account_uid is not None
        }

    @property
    def ready(self) -> bool:
        """
        :return True when all statuses pass, this might take 5-10 seconds for all the connector's components and
        services to be ready.
        """
        return all(self.status_dict.values())

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, any]:
        """
        :return active in-flight orders in json format, is used to save in sqlite db.
        """
        return {
            key: value.to_json()
            for key, value in self._in_flight_orders.items()
            if not value.is_done
        }

    def restore_tracking_states(self, saved_states: Dict[str, any]):
        """
        Restore in-flight orders from saved tracking states, this is st the connector can pick up on where it left off
        when it disconnects.
        :param saved_states: The saved tracking_states.
        """
        self._in_flight_orders.update({
            key: AscendExInFlightOrder.from_json(value)
            for key, value in saved_states.items()
        })

    def supported_order_types(self) -> List[OrderType]:
        """
        :return a list of OrderType supported by this connector.
        Note that Market order type is no longer required and will not be used.
        """
        return [OrderType.LIMIT, OrderType.LIMIT_MAKER]

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        self._order_book_tracker.start()
        await self._update_account_data()

        self._trading_rules_polling_task = safe_ensure_future(
            self._trading_rules_polling_loop())
        if self._trading_required:
            self._status_polling_task = safe_ensure_future(
                self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(
                self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(
                self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        """
        # Resets timestamps for status_polling_task
        self._last_poll_timestamp = 0
        self._last_timestamp = 0

        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        This function is required by NetworkIterator base class and is called periodically to check
        the network connection. Simply ping the network (or call any light weight public API).
        """
        try:
            # since there is no ping endpoint, the lowest rate call is to get BTC-USDT ticker
            await self._api_request(method="get",
                                    path_url=CONSTANTS.TICKER_PATH_URL)
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    async def _trading_rules_polling_loop(self):
        """
        Periodically update trading rule.
        """
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(60)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().network(
                    f"Unexpected error while fetching trading rules. Error: {str(e)}",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch new trading rules from AscendEx. "
                    "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        instruments_info = await self._api_request(
            method="get", path_url=CONSTANTS.PRODUCTS_PATH_URL)
        self._trading_rules.clear()
        self._trading_rules = self._format_trading_rules(instruments_info)

    def _format_trading_rules(
            self,
            instruments_info: Dict[str,
                                   Any]) -> Dict[str, AscendExTradingRule]:
        """
        Converts json API response into a dictionary of trading rules.
        :param instruments_info: The json API response
        :return A dictionary of trading rules.
        Response Example:
        {
            "code": 0,
            "data": [
                {
                    "symbol":                "BTMX/USDT",
                    "baseAsset":             "BTMX",
                    "quoteAsset":            "USDT",
                    "status":                "Normal",
                    "minNotional":           "5",
                    "maxNotional":           "100000",
                    "marginTradable":         true,
                    "commissionType":        "Quote",
                    "commissionReserveRate": "0.001",
                    "tickSize":              "0.000001",
                    "lotSize":               "0.001"
                }
            ]
        }
        """
        trading_rules = {}
        for rule in instruments_info["data"]:
            try:
                trading_pair = ascend_ex_utils.convert_from_exchange_trading_pair(
                    rule["symbol"])
                trading_rules[trading_pair] = AscendExTradingRule(
                    trading_pair,
                    min_price_increment=Decimal(rule["tickSize"]),
                    min_base_amount_increment=Decimal(rule["lotSize"]),
                    min_notional_size=Decimal(rule["minNotional"]),
                    max_notional_size=Decimal(rule["maxNotional"]),
                    commission_type=AscendExCommissionType[
                        rule["commissionType"].upper()],
                    commission_reserve_rate=Decimal(
                        rule["commissionReserveRate"]),
                )
            except Exception:
                self.logger().error(
                    f"Error parsing the trading pair rule {rule}. Skipping.",
                    exc_info=True)
        return trading_rules

    async def _update_account_data(self):
        headers = {
            **self._ascend_ex_auth.get_headers(),
            **self._ascend_ex_auth.get_auth_headers("info"),
        }
        url = f"{CONSTANTS.REST_URL}/info"
        response = await self._shared_client.get(url, headers=headers)

        try:
            parsed_response = json.loads(await response.text())
        except Exception as e:
            raise IOError(f"Error parsing data from {url}. Error: {str(e)}")
        if response.status != 200:
            raise IOError(
                f"Error fetching data from {url}. HTTP status is {response.status}. "
                f"Message: {parsed_response}")
        if parsed_response["code"] != 0:
            raise IOError(
                f"{url} API call failed, response: {parsed_response}")

        self._account_group = parsed_response["data"]["accountGroup"]
        self._account_uid = parsed_response["data"]["userUID"]

    async def _api_request(
            self,
            method: str,
            path_url: str,
            params: Optional[Dict[str, Any]] = None,
            data: Optional[Dict[str, Any]] = None,
            is_auth_required: bool = False,
            force_auth_path_url: Optional[str] = None) -> Dict[str, Any]:
        """
        Sends an aiohttp request and waits for a response.
        :param method: The HTTP method, e.g. get or post
        :param path_url: The path url or the API end point
        :param is_auth_required: Whether an authentication is required, when True the function will add encrypted
        signature to the request.
        :returns A response in json format.
        """
        kwargs = {}
        if params:
            kwargs["params"] = params
        if data:
            kwargs["data"] = json.dumps(data)

        if is_auth_required:
            if self._account_group is None:
                await self._update_account_data()

            url = f"{ascend_ex_utils.get_rest_url_private(self._account_group)}/{path_url}"
            kwargs["headers"] = {
                **self._ascend_ex_auth.get_headers(),
                **self._ascend_ex_auth.get_auth_headers(path_url if force_auth_path_url is None else force_auth_path_url),
            }
        else:
            url = f"{CONSTANTS.REST_URL}/{path_url}"
            kwargs["headers"] = self._ascend_ex_auth.get_headers()

        if method == "get":
            async with self._throttler.execute_task(path_url):
                response = await self._shared_client.get(url, **kwargs)
        elif method == "post":
            async with self._throttler.execute_task(path_url):
                response = await self._shared_client.post(url, **kwargs)
        elif method == "delete":
            async with self._throttler.execute_task(path_url):
                response = await self._shared_client.delete(url, **kwargs)
        else:
            raise NotImplementedError

        resp_text = await response.text()
        if response.status != 200:
            raise IOError(
                f"Error calling {url}. HTTP status is {response.status}. "
                f"Message: {resp_text}")
        try:
            parsed_response = json.loads(resp_text)
        except Exception as e:
            raise IOError(f"Error calling {url}. Error: {str(e)}")
        if parsed_response["code"] != 0:
            raise IOError(
                f"{url} API call failed, response: {parsed_response}")

        return parsed_response

    def get_order_price_quantum(self, trading_pair: str, price: Decimal):
        """
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str, order_size: Decimal):
        """
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def buy(self,
            trading_pair: str,
            amount: Decimal,
            order_type=OrderType.MARKET,
            price: Decimal = s_decimal_NaN,
            **kwargs) -> str:
        """
        Buys an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        client_order_id = ascend_ex_utils.gen_client_order_id(
            True, trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.BUY, client_order_id, trading_pair,
                               amount, order_type, price))
        return client_order_id

    def sell(self,
             trading_pair: str,
             amount: Decimal,
             order_type=OrderType.MARKET,
             price: Decimal = s_decimal_NaN,
             **kwargs) -> str:
        """
        Sells an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for SellOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to sell from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        client_order_id = ascend_ex_utils.gen_client_order_id(
            False, trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.SELL, client_order_id, trading_pair,
                               amount, order_type, price))
        return client_order_id

    def cancel(self, trading_pair: str, order_id: str):
        """
        Cancel an order. This function returns immediately.
        To get the cancellation result, you'll have to wait for OrderCancelledEvent.
        :param trading_pair: The market (e.g. BTC-USDT) of the order.
        :param order_id: The internal order id (also called client_order_id)
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    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))

    def trigger_order_created_event(self, order: AscendExInFlightOrder):
        event_tag = MarketEvent.BuyOrderCreated if order.trade_type is TradeType.BUY else MarketEvent.SellOrderCreated
        event_class = BuyOrderCreatedEvent if order.trade_type is TradeType.BUY else SellOrderCreatedEvent
        self.trigger_event(
            event_tag,
            event_class(self.current_timestamp,
                        order.order_type,
                        order.trading_pair,
                        order.amount,
                        order.price,
                        order.client_order_id,
                        exchange_order_id=order.exchange_order_id))

    def start_tracking_order(self, order_id: str, exchange_order_id: str,
                             trading_pair: str, trade_type: TradeType,
                             price: Decimal, amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by simply adding it into _in_flight_orders dictionary.
        """
        self._in_flight_orders[order_id] = AscendExInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount)

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order by simply removing it from _in_flight_orders dictionary.
        """
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]

    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 _status_polling_loop(self):
        """
        Periodically update user balances and order status via REST API. This serves as a fallback measure for web
        socket API updates.
        """
        while True:
            try:
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = self.current_timestamp
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(str(e), exc_info=True)
                self.logger().network(
                    "Unexpected error while fetching account updates.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch account updates from AscendEx. "
                    "Check API key and network connection.")
                await asyncio.sleep(0.5)
            finally:
                self._poll_notifier = asyncio.Event()

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances.
        """
        response = await self._api_request(method="get",
                                           path_url=CONSTANTS.BALANCE_PATH_URL,
                                           is_auth_required=True,
                                           force_auth_path_url="balance")
        balances = list(
            map(
                lambda balance: AscendExBalance(balance["asset"], balance[
                    "availableBalance"], balance["totalBalance"]),
                response.get("data", list())))
        self._process_balances(balances)

    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[AscendExInFlightOrder] = list(
                self._in_flight_orders.values())
            for o in tracked_orders:
                await o.get_exchange_order_id()
            order_ids: str = ",".join(o.exchange_order_id
                                      for o in tracked_orders)
            params = {"orderId": order_ids}
            resp = await self._api_request(
                method="get",
                path_url=CONSTANTS.ORDER_STATUS_PATH_URL,
                params=params,
                is_auth_required=True,
                force_auth_path_url="order/status")
            self.logger().debug(
                f"Polling for order status updates of {len(order_ids)} orders."
            )
            self.logger().debug(
                f"cash/order/status?orderId={order_ids} response: {resp}")
            # The data returned from this end point can be either a list or a dict depending on number of orders
            resp_records: List = []
            if isinstance(resp["data"], dict):
                resp_records.append(resp["data"])
            elif isinstance(resp["data"], list):
                resp_records = resp["data"]
            ascend_ex_orders: List[AscendExOrder] = []
            try:
                for order_data in resp_records:
                    ascend_ex_orders.append(
                        AscendExOrder(
                            order_data["symbol"], order_data["price"],
                            order_data["orderQty"], order_data["orderType"],
                            order_data["avgPx"], order_data["cumFee"],
                            order_data["cumFilledQty"],
                            order_data["errorCode"], order_data["feeAsset"],
                            order_data["lastExecTime"], order_data["orderId"],
                            order_data["seqNum"], order_data["side"],
                            order_data["status"], order_data["stopPrice"],
                            order_data["execInst"]))
                for order in ascend_ex_orders:
                    self._process_order_message(order)
                for hbot_order in tracked_orders:
                    if hbot_order.exchange_order_id not in (
                            o.orderId for o in ascend_ex_orders):
                        self.logger().info(
                            f"{hbot_order} is missing from expected response ({resp})"
                        )
            except Exception:
                self.logger().info(
                    f"Unexpected error during processing order status. The Ascend Ex Response: {resp}",
                    exc_info=True)

    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 tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock's tick (1 second by default).
        It checks if status polling task is due for execution.
        """
        now = time.time()
        poll_interval = (self.SHORT_POLL_INTERVAL if
                         now - self._user_stream_tracker.last_recv_time > 60.0
                         else self.LONG_POLL_INTERVAL)
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN) -> TradeFee:
        """For more information: https://ascendex.github.io/ascendex-pro-api/#place-order."""
        trading_pair = f"{base_currency}-{quote_currency}"
        trading_rule = self._trading_rules[trading_pair]
        fee_percent = Decimal("0")
        if order_side == TradeType.BUY:
            if trading_rule.commission_type == AscendExCommissionType.QUOTE:
                fee_percent = trading_rule.commission_reserve_rate
        elif trading_rule.commission_type == AscendExCommissionType.BASE:
            fee_percent = trading_rule.commission_reserve_rate
        return TradeFee(percent=fee_percent)

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch user events from AscendEx. Check API key and network connection."
                )
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        """
        Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by
        AscendExAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                if event_message.get("m") == "order":
                    order_data = event_message.get("data")
                    trading_pair = order_data["s"]
                    base_asset, quote_asset = tuple(
                        asset for asset in trading_pair.split("/"))
                    self._process_order_message(
                        AscendExOrder(trading_pair, order_data["p"],
                                      order_data["q"], order_data["ot"],
                                      order_data["ap"], order_data["cf"],
                                      order_data["cfq"], order_data["err"],
                                      order_data["fa"], order_data["t"],
                                      order_data["orderId"], order_data["sn"],
                                      order_data["sd"], order_data["st"],
                                      order_data["sp"], order_data["ei"]))
                    # Handles balance updates from orders.
                    base_asset_balance = AscendExBalance(
                        base_asset, order_data["bab"], order_data["btb"])
                    quote_asset_balance = AscendExBalance(
                        quote_asset, order_data["qab"], order_data["qtb"])
                    self._process_balances(
                        [base_asset_balance, quote_asset_balance], False)
                elif event_message.get("m") == "balance":
                    # Handles balance updates from Deposits/Withdrawals, Transfers between Cash and Margin Accounts
                    balance_data = event_message.get("data")
                    balance = AscendExBalance(balance_data["a"],
                                              balance_data["ab"],
                                              balance_data["tb"])
                    self._process_balances(list(balance), False)

            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().error(
                    "Unexpected error in user stream listener loop.",
                    exc_info=True)
                await asyncio.sleep(5.0)

    async def get_open_orders(self) -> List[OpenOrder]:
        result = await self._api_request(
            method="get",
            path_url=CONSTANTS.ORDER_OPEN_PATH_URL,
            is_auth_required=True,
            force_auth_path_url="order/open")
        ret_val = []
        for order in result["data"]:
            if order["orderType"].lower() != "limit":
                self.logger().debug(
                    f"Unsupported orderType: {order['orderType']}. Order: {order}",
                    exc_info=True)
                continue

            exchange_order_id = order["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:
                self.logger().debug(
                    f"Unrecognized Order {exchange_order_id}: {order}")
                continue

            ret_val.append(
                OpenOrder(
                    client_order_id=client_order_id,
                    trading_pair=ascend_ex_utils.
                    convert_from_exchange_trading_pair(order["symbol"]),
                    price=Decimal(str(order["price"])),
                    amount=Decimal(str(order["orderQty"])),
                    executed_amount=Decimal(str(order["cumFilledQty"])),
                    status=order["status"],
                    order_type=OrderType.LIMIT,
                    is_buy=True if order["side"].lower() == "buy" else False,
                    time=int(order["lastExecTime"]),
                    exchange_order_id=exchange_order_id))
        return ret_val

    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)

    def _process_balances(self,
                          balances: List[AscendExBalance],
                          is_complete_list: bool = True):
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()

        for balance in balances:
            asset_name = balance.asset
            self._account_available_balances[asset_name] = Decimal(
                balance.availableBalance)
            self._account_balances[asset_name] = Decimal(balance.totalBalance)
            remote_asset_names.add(asset_name)
        if not is_complete_list:
            return
        asset_names_to_remove = local_asset_names.difference(
            remote_asset_names)
        for asset_name in asset_names_to_remove:
            del self._account_available_balances[asset_name]
            del self._account_balances[asset_name]

    def quantize_order_amount(self,
                              trading_pair: str,
                              amount: Decimal,
                              price: Decimal = s_decimal_0) -> Decimal:
        trading_rule: AscendExTradingRule = self._trading_rules[trading_pair]
        quantized_amount: Decimal = super().quantize_order_amount(
            trading_pair, amount)

        # Check against min_order_size and min_notional_size. If not passing either check, return 0.
        if quantized_amount < trading_rule.min_order_size:
            return s_decimal_0

        if price == s_decimal_0:
            current_price: Decimal = self.get_price(trading_pair, False)
            notional_size = current_price * quantized_amount
        else:
            notional_size = price * quantized_amount

        # Add 1% as a safety factor in case the prices changed while making the order.
        if notional_size < trading_rule.min_notional_size * Decimal("1.01") or \
                notional_size > trading_rule.max_notional_size:
            return s_decimal_0

        return quantized_amount
Example #13
0
class WazirxExchange(ExchangeBase):
    """
    WazirxExchange connects with Wazirx exchange and provides order book pricing, user account tracking and
    trading functionality.
    """
    API_CALL_TIMEOUT = 10.0
    SHORT_POLL_INTERVAL = 5.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    LONG_POLL_INTERVAL = 120.0

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global ctce_logger
        if ctce_logger is None:
            ctce_logger = logging.getLogger(__name__)
        return ctce_logger

    def __init__(self,
                 wazirx_api_key: str,
                 wazirx_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True):
        """
        :param wazirx_api_key: The API key to connect to private Wazirx APIs.
        :param wazirx_secret_key: The API secret.
        :param trading_pairs: The market trading pairs which to track order book data.
        :param trading_required: Whether actual trading is needed.
        """
        super().__init__()
        self._trading_required = trading_required
        self._trading_pairs = trading_pairs
        self._wazirx_auth = WazirxAuth(wazirx_api_key, wazirx_secret_key)
        self._order_book_tracker = WazirxOrderBookTracker(
            trading_pairs=trading_pairs)
        self._user_stream_tracker = WazirxUserStreamTracker(
            self._wazirx_auth, trading_pairs)
        self._ev_loop = asyncio.get_event_loop()
        self._shared_client = None
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders = {
        }  # Dict[client_order_id:str, WazirxInFlightOrder]
        self._order_not_found_records = {
        }  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, TradingRule]
        self._status_polling_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._last_poll_timestamp = 0
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)

    @property
    def name(self) -> str:
        return CONSTANTS.EXCHANGE_NAME

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, WazirxInFlightOrder]:
        return self._in_flight_orders

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        A dictionary of statuses of various connector's components.
        """
        return {
            "order_books_initialized":
            self._order_book_tracker.ready,
            "account_balance":
            len(self._account_balances) > 0
            if self._trading_required else True,
            "trading_rule_initialized":
            len(self._trading_rules) > 0,
            "user_stream_initialized":
            self._user_stream_tracker.data_source.ready > 0
            if self._trading_required else True,
        }

    @property
    def ready(self) -> bool:
        """
        :return True when all statuses pass, this might take 5-10 seconds for all the connector's components and
        services to be ready.
        """
        return all(self.status_dict.values())

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, any]:
        """
        :return active in-flight orders in json format, is used to save in sqlite db.
        """
        return {
            key: value.to_json()
            for key, value in self._in_flight_orders.items()
            if not value.is_done
        }

    def restore_tracking_states(self, saved_states: Dict[str, any]):
        """
        Restore in-flight orders from saved tracking states, this is st the connector can pick up on where it left off
        when it disconnects.
        :param saved_states: The saved tracking_states.
        """
        self._in_flight_orders.update({
            key: WazirxInFlightOrder.from_json(value)
            for key, value in saved_states.items()
        })

    def supported_order_types(self) -> List[OrderType]:
        """
        :return a list of OrderType supported by this connector.
        Note that Market order type is no longer required and will not be used.
        """
        return [OrderType.LIMIT, OrderType.LIMIT_MAKER]

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(
            self._trading_rules_polling_loop())

        if self._trading_required:
            self._status_polling_task = safe_ensure_future(
                self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(
                self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(
                self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        """
        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        This function is required by NetworkIterator base class and is called periodically to check
        the network connection. Simply ping the network (or call any light weight public API).
        """
        try:
            await self._api_request("get", CONSTANTS.CHECK_NETWORK_PATH_URL)
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    async def _http_client(self) -> aiohttp.ClientSession:
        """
        :returns Shared client session instance
        """
        if self._shared_client is None:
            self._shared_client = aiohttp.ClientSession()
        return self._shared_client

    async def _trading_rules_polling_loop(self):
        """
        Periodically update trading rule.
        """
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(60)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().network(
                    f"Unexpected error while fetching trading rules. Error: {str(e)}",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch new trading rules from Wazirx. "
                    "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        instruments_info = await self._api_request(
            "get", path_url=CONSTANTS.GET_TRADING_RULES_PATH_URL)
        self._trading_rules.clear()
        self._trading_rules = self._format_trading_rules(instruments_info)

    def _format_trading_rules(
            self, instruments_info: Dict[str, Any]) -> Dict[str, TradingRule]:
        """
        Converts json API response into a dictionary of trading rules.
        :param instruments_info: The json API response
        :return A dictionary of trading rules.
        Response Example:
        {
            "timezone": "UTC",
            "serverTime": 1631531599247,
            "symbols": [
                {
                    "symbol": "btcinr",
                    "status": "trading",
                    "baseAsset": "btc",
                    "quoteAsset": "inr",
                    "baseAssetPrecision": 5,
                    "quoteAssetPrecision": 0,
                    "orderTypes": [
                        "limit",
                        "stop_limit"
                    ],
                    "isSpotTradingAllowed": true,
                    "filters": [
                        {
                            "filterType": "PRICE_FILTER",
                            "minPrice": "1",
                            "tickSize": "1"
                        }
                    ]
                }
            ]
        }
        """
        result = {}
        if "symbols" in instruments_info:
            for rule in instruments_info["symbols"]:
                if rule["isSpotTradingAllowed"] is True:
                    try:
                        trading_pair = wazirx_utils.convert_from_exchange_trading_pair(
                            rule["symbol"])
                        price_decimals = Decimal(
                            str(rule["quoteAssetPrecision"]))
                        quantity_decimals = Decimal(
                            str(rule["baseAssetPrecision"]))
                        # E.g. a price decimal of 2 means 0.01 incremental.
                        price_step = Decimal("1") / Decimal(
                            str(math.pow(10, price_decimals)))
                        quantity_step = Decimal("1") / Decimal(
                            str(math.pow(10, quantity_decimals)))
                        min_order_value = Decimal(
                            wazirx_utils.get_min_order_value(trading_pair))
                        result[trading_pair] = TradingRule(
                            trading_pair,
                            min_price_increment=price_step,
                            min_base_amount_increment=quantity_step,
                            min_order_value=min_order_value,
                        )
                    except Exception:
                        self.logger().error(
                            f"Error parsing the trading pair rule {rule}. Skipping.",
                            exc_info=True)

        return result

    async def _api_request(self,
                           method: str,
                           path_url: str,
                           params: Dict[str, Any] = {},
                           is_auth_required: bool = False) -> Dict[str, Any]:
        """
        Sends an aiohttp request and waits for a response.
        :param method: The HTTP method, e.g. get or post
        :param path_url: The path url or the API end point
        :param is_auth_required: Whether an authentication is required, when True the function will add encrypted
        signature to the request.
        :returns A response in json format.
        """
        async with self._throttler.execute_task(path_url):
            url = f"{CONSTANTS.WAZIRX_API_BASE}/{path_url}"
            client = await self._http_client()
            if is_auth_required:
                params = self._wazirx_auth.get_auth(params)
                headers = self._wazirx_auth.get_headers()
            else:
                headers = {"Content-Type": "application/json"}

            if method == "get":
                response = await client.get(url, headers=headers, data=params)
            elif method == "post":
                response = await client.post(url, headers=headers, data=params)
            elif method == "delete":
                response = await client.delete(url,
                                               headers=headers,
                                               data=params)
            else:
                raise NotImplementedError

            try:
                parsed_response = json.loads(await response.text())
            except Exception as e:
                raise IOError(
                    f"Error parsing data from {url}. Error: {str(e)}")
            if response.status != 200 and response.status != 201:
                raise IOError(
                    f"Error fetching data from {url}. HTTP status is {response.status}. "
                    f"Message: {parsed_response}, {params}")
            return parsed_response

    def get_order_price_quantum(self, trading_pair: str, price: Decimal):
        """
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str, order_size: Decimal):
        """
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def buy(self,
            trading_pair: str,
            amount: Decimal,
            order_type=OrderType.MARKET,
            price: Decimal = s_decimal_NaN,
            **kwargs) -> str:
        """
        Buys an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = wazirx_utils.get_new_client_order_id(
            True, trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.BUY, order_id, trading_pair, amount,
                               order_type, price))
        return order_id

    def sell(self,
             trading_pair: str,
             amount: Decimal,
             order_type=OrderType.MARKET,
             price: Decimal = s_decimal_NaN,
             **kwargs) -> str:
        """
        Sells an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for SellOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to sell from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = wazirx_utils.get_new_client_order_id(
            False, trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.SELL, order_id, trading_pair, amount,
                               order_type, price))
        return order_id

    def cancel(self, trading_pair: str, order_id: str):
        """
        Cancel an order. This function returns immediately.
        To get the cancellation result, you'll have to wait for OrderCancelledEvent.
        :param trading_pair: The market (e.g. BTC-USDT) of the order.
        :param order_id: The internal order id (also called client_order_id)
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    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:
            self.trigger_event(
                MarketEvent.OrderFailure,
                MarketOrderFailureEvent(self.current_timestamp, order_id,
                                        order_type))
            raise ValueError(
                f"Buy order amount {amount} is lower than the minimum order size "
                f"{trading_rule.min_order_size}.")

        order_value: Decimal = amount * price
        if order_value < trading_rule.min_order_value:
            self.trigger_event(
                MarketEvent.OrderFailure,
                MarketOrderFailureEvent(self.current_timestamp, order_id,
                                        order_type))
            raise ValueError(
                f"{trade_type.name} order value {order_value} is lower than the minimum order value "
                f"{trading_rule.min_order_value}.")

        api_params = {
            "symbol":
            wazirx_utils.convert_to_exchange_trading_pair(trading_pair),
            "side": trade_type.name.lower(),
            "type": "limit",
            "price": f"{price:f}",
            "quantity": f"{amount:f}",
            # "client_oid": f"{order_id}"
        }
        self.start_tracking_order(order_id, None, trading_pair, trade_type,
                                  price, amount, order_type)
        try:
            order_result = await self._api_request("post",
                                                   CONSTANTS.ORDER_PATH_URL,
                                                   api_params, True)
            self.logger().info(order_result)
            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)

            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 Wazirx 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))

    def start_tracking_order(self, order_id: str, exchange_order_id: str,
                             trading_pair: str, trade_type: TradeType,
                             price: Decimal, amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by simply adding it into _in_flight_orders dictionary.
        """
        self._in_flight_orders[order_id] = WazirxInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount,
            creation_timestamp=self.current_timestamp)

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order by simply removing it from _in_flight_orders dictionary.
        """
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]

    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":
                wazirx_utils.convert_to_exchange_trading_pair(trading_pair),
                "orderId":
                ex_order_id
            }
            result = await self._api_request("delete",
                                             CONSTANTS.ORDER_PATH_URL,
                                             api_params, True)

            if result["status"] == "wait":
                await wait_til(lambda: tracked_order.is_cancelled)
                return order_id
            else:
                tracked_order.last_state = result["status"]
                if tracked_order.is_cancelled:
                    self._process_order_message(result)
                    return order_id
                elif tracked_order.is_done:
                    api_params = {
                        "limit":
                        100,
                        "symbol":
                        wazirx_utils.convert_to_exchange_trading_pair(
                            trading_pair),
                        "orderId":
                        ex_order_id,
                    }
                    order_trades = await self._api_request(
                        "get", CONSTANTS.MY_TRADES_PATH_URL, api_params, True)
                    for order_trade in order_trades:
                        trade_msg = {
                            "order_id": ex_order_id,
                            "trade_id": str(order_trade["id"]),
                            "traded_price": order_trade["price"],
                            "traded_quantity": order_trade["qty"],
                            "quote_quantity": order_trade["quoteQty"],
                            "fee": order_trade["fee"],
                            "fee_currency": order_trade["feeCurrency"].upper(),
                        }
                        await self._process_trade_message(trade_msg)
                    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 Wazirx. "
                f"Check API key and network connection.")

    async def _status_polling_loop(self):
        """
        Periodically update user balances and order status via REST API. This serves as a fallback measure for web
        socket API updates.
        """
        while True:
            try:
                self._poll_notifier = asyncio.Event()
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = self.current_timestamp
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(str(e), exc_info=True)
                self.logger().network(
                    "Unexpected error while fetching account updates.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch account updates from Wazirx. "
                    "Check API key and network connection.")
                await asyncio.sleep(0.5)

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances.
        """
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()
        account_info = await self._api_request("get",
                                               CONSTANTS.FUND_DETAILS_PATH_URL,
                                               {}, True)
        for account in account_info:
            asset_name = account["asset"].upper()
            self._account_available_balances[asset_name] = Decimal(
                str(account["free"]))
            self._account_balances[
                asset_name] = self._account_available_balances[
                    asset_name] + Decimal(str(account["locked"]))
            remote_asset_names.add(asset_name)
        asset_names_to_remove = local_asset_names.difference(
            remote_asset_names)
        for asset_name in asset_names_to_remove:
            del self._account_available_balances[asset_name]
            del self._account_balances[asset_name]

    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()
                api_params = {"orderId": order_id}
                tasks.append(
                    self._api_request("get", CONSTANTS.ORDER_PATH_URL,
                                      api_params, True))
            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
                elif "status" not in response:
                    self.logger().info(
                        f"_update_order_status result not in resp: {response}")
                    continue
                else:
                    api_params = {
                        "limit": 100,
                        "symbol": response["symbol"],
                        "orderId": response["id"],
                    }
                    order_trades = await self._api_request(
                        "get", CONSTANTS.MY_TRADES_PATH_URL, api_params, True)
                    for order_trade in order_trades:
                        trade_msg = {
                            "order_id": str(response["id"]),
                            "trade_id": str(order_trade["id"]),
                            "traded_price": order_trade["price"],
                            "traded_quantity": order_trade["qty"],
                            "quote_quantity": order_trade["quoteQty"],
                        }
                        await self._process_trade_message(trade_msg)
                    self._process_order_message(response)

    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)

    async def _process_trade_message(self, trade_msg: Dict[str, Any]):
        """
        Updates in-flight order and trigger order filled event for trade message received. Triggers order completed
        event if the total executed amount equals to the specified order amount.
        """
        for order in self._in_flight_orders.values():
            await order.get_exchange_order_id()
        track_order = [
            o for o in self._in_flight_orders.values()
            if trade_msg["order_id"] == o.exchange_order_id
        ]
        if not track_order:
            return
        tracked_order = track_order[0]

        updated = tracked_order.update_with_trade_update(trade_msg)
        if not updated:
            return
        self.logger().info("_process_trade_message")
        self.logger().info(trade_msg)
        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(trade_msg["traded_price"])),
                             Decimal(str(trade_msg["traded_quantity"])),
                             AddedToCostTradeFee(flat_fees=[
                                 TokenAmount(trade_msg["fee_currency"],
                                             Decimal(str(trade_msg["fee"])))
                             ]),
                             exchange_trade_id=trade_msg["trade_id"]))
        if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or \
                tracked_order.executed_amount_base >= tracked_order.amount:
            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,
                            tracked_order.fee_paid, tracked_order.order_type))
            self.stop_tracking_order(tracked_order.client_order_id)

    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.
        """
        incomplete_orders = [
            o for o in self._in_flight_orders.values() if not o.is_done
        ]
        tasks = [
            self._execute_cancel(o.trading_pair, o.client_order_id)
            for o in incomplete_orders
        ]
        order_id_set = set([o.client_order_id for o in incomplete_orders])
        successful_cancellations = []
        try:
            self.logger().info("Start Cancel ALL ................")
            async with timeout(timeout_seconds):
                results = await safe_gather(*tasks, return_exceptions=True)
                for result in results:
                    if result is not None and not isinstance(
                            result, Exception):
                        order_id_set.remove(result)
                        successful_cancellations.append(
                            CancellationResult(result, True))
        except Exception:
            self.logger().error("Cancel all failed.", exc_info=True)
            self.logger().network(
                "Unexpected error cancelling orders.",
                exc_info=True,
                app_warning_msg=
                "Failed to cancel order on Wazirx. Check API key and network connection."
            )

        failed_cancellations = [
            CancellationResult(oid, False) for oid in order_id_set
        ]
        return successful_cancellations + failed_cancellations

    def tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock's tick (1 second by default).
        It checks if status polling task is due for execution.
        """
        now = time.time()
        poll_interval = (self.SHORT_POLL_INTERVAL if
                         now - self._user_stream_tracker.last_recv_time > 60.0
                         else self.LONG_POLL_INTERVAL)
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN,
                is_maker: Optional[bool] = None) -> AddedToCostTradeFee:
        """
        To get trading fee, this function is simplified by using fee override configuration. Most parameters to this
        function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for
        maker order.
        """
        is_maker = order_type is OrderType.LIMIT_MAKER
        return AddedToCostTradeFee(percent=self.estimate_fee_pct(is_maker))

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg=
                    "Could not fetch user events from Wazirx. Check API key and network connection."
                )
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        """
        Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by
        WazirxAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                self.logger().info(event_message)
                if "data" not in event_message or "stream" not in event_message:
                    continue
                stream = event_message["stream"]
                if "ownTrade" in stream:
                    trade_evt = event_message["data"]
                    trade_msg = {
                        "trade_id": str(trade_evt["t"]),
                        "order_id": str(trade_evt["o"]),
                        "traded_price": trade_evt["p"],
                        "traded_quantity": trade_evt["q"],
                        "fee_currency": trade_evt["U"].upper(),
                        "fee": trade_evt["f"]
                    }
                    await self._process_trade_message(trade_msg)
                elif "orderUpdate" in stream:
                    order_evt = event_message["data"]
                    order_msg = {
                        "id": order_evt["i"],
                        "status": order_evt["X"],
                    }
                    self._process_order_message(order_msg)
                elif "outboundAccountPosition" in stream:
                    balances = event_message["data"]["B"]
                    for balance_entry in balances:
                        asset_name = balance_entry["a"].upper()
                        free_balance = Decimal(str(balance_entry["b"]))
                        locked_balance = Decimal(str(balance_entry["l"]))
                        self._account_balances[
                            asset_name] = free_balance + locked_balance
                        self._account_available_balances[
                            asset_name] = free_balance
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().error(
                    "Unexpected error in user stream listener loop.",
                    exc_info=True)
                await asyncio.sleep(5.0)
 async def execute_requests(self, no_request: int, limit_id: str,
                            throttler: AsyncThrottler):
     for _ in range(no_request):
         async with throttler.execute_task(limit_id=limit_id):
             self._req_counters[limit_id] += 1
 def test_within_capacity_returns_true_for_throttler_without_configured_limits(
         self):
     throttler = AsyncThrottler(rate_limits=[])
     context = throttler.execute_task(limit_id="test_limit_id")
     self.assertTrue(context.within_capacity())
class GateIoExchange(ExchangeBase):
    """
    GateIoExchange connects with Gate.io exchange and provides order book pricing, user account tracking and
    trading functionality.
    """
    ORDER_NOT_EXIST_CONFIRMATION_COUNT = 3
    ORDER_NOT_EXIST_CANCEL_COUNT = 2

    @classmethod
    def logger(cls) -> HummingbotLogger:
        global ctce_logger
        if ctce_logger is None:
            ctce_logger = logging.getLogger(__name__)
        return ctce_logger

    def __init__(self,
                 gate_io_api_key: str,
                 gate_io_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True):
        """
        :param gate_io_api_key: The API key to connect to private Gate.io APIs.
        :param gate_io_secret_key: The API secret.
        :param trading_pairs: The market trading pairs which to track order book data.
        :param trading_required: Whether actual trading is needed.
        """
        super().__init__()
        self._trading_required = trading_required
        self._trading_pairs = trading_pairs
        self._gate_io_auth = GateIoAuth(gate_io_api_key, gate_io_secret_key)
        self._throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
        self._order_book_tracker = GateIoOrderBookTracker(
            self._throttler, trading_pairs=trading_pairs)
        self._user_stream_tracker = GateIoUserStreamTracker(
            self._gate_io_auth, trading_pairs)
        self._ev_loop = asyncio.get_event_loop()
        self._shared_client = None
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders = {
        }  # Dict[client_order_id:str, GateIoInFlightOrder]
        self._order_not_found_records = {
        }  # Dict[client_order_id:str, count:int]
        self._trading_rules = {}  # Dict[trading_pair:str, TradingRule]
        self._status_polling_task = None
        self._user_stream_event_listener_task = None
        self._trading_rules_polling_task = None
        self._last_poll_timestamp = 0
        self._real_time_balance_update = False
        self._update_balances_fetching = False
        self._update_balances_queued = False
        self._update_balances_finished = asyncio.Event()

    @property
    def name(self) -> str:
        return "gate_io"

    @property
    def order_books(self) -> Dict[str, OrderBook]:
        return self._order_book_tracker.order_books

    @property
    def trading_rules(self) -> Dict[str, TradingRule]:
        return self._trading_rules

    @property
    def in_flight_orders(self) -> Dict[str, GateIoInFlightOrder]:
        return self._in_flight_orders

    @property
    def status_dict(self) -> Dict[str, bool]:
        """
        A dictionary of statuses of various connector's components.
        """
        return {
            "order_books_initialized":
            self._order_book_tracker.ready,
            "account_balance":
            len(self._account_balances) > 0
            if self._trading_required else True,
            "trading_rule_initialized":
            len(self._trading_rules) > 0,
            "user_stream_initialized":
            self._user_stream_tracker.data_source.last_recv_time > 0
            if self._trading_required else True,
        }

    @property
    def ready(self) -> bool:
        """
        :return True when all statuses pass, this might take 5-10 seconds for all the connector's components and
        services to be ready.
        """
        return all(self.status_dict.values())

    @property
    def limit_orders(self) -> List[LimitOrder]:
        return [
            in_flight_order.to_limit_order()
            for in_flight_order in self._in_flight_orders.values()
        ]

    @property
    def tracking_states(self) -> Dict[str, any]:
        """
        :return active in-flight orders in json format, is used to save in sqlite db.
        """
        return {
            key: value.to_json()
            for key, value in self._in_flight_orders.items()
            if not value.is_done
        }

    def restore_tracking_states(self, saved_states: Dict[str, any]):
        """
        Restore in-flight orders from saved tracking states, this is st the connector can pick up on where it left off
        when it disconnects.
        :param saved_states: The saved tracking_states.
        """
        self._in_flight_orders.update({
            key: GateIoInFlightOrder.from_json(value)
            for key, value in saved_states.items()
        })

    def supported_order_types(self) -> List[OrderType]:
        """
        :return a list of OrderType supported by this connector.
        Note that Market order type is no longer required and will not be used.
        """
        return [OrderType.LIMIT]

    def get_taker_order_type(self):
        return OrderType.LIMIT

    def start(self, clock: Clock, timestamp: float):
        """
        This function is called automatically by the clock.
        """
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        """
        This function is called automatically by the clock.
        """
        super().stop(clock)

    async def start_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        It starts tracking order book, polling trading rules,
        updating statuses and tracking user data.
        """
        self._order_book_tracker.start()
        self._trading_rules_polling_task = safe_ensure_future(
            self._trading_rules_polling_loop())
        if self._trading_required:
            self._status_polling_task = safe_ensure_future(
                self._status_polling_loop())
            self._user_stream_tracker_task = safe_ensure_future(
                self._user_stream_tracker.start())
            self._user_stream_event_listener_task = safe_ensure_future(
                self._user_stream_event_listener())

    async def stop_network(self):
        """
        This function is required by NetworkIterator base class and is called automatically.
        """
        # Resets timestamps for status_polling_task
        self._last_poll_timestamp = 0
        self._last_timestamp = 0
        self._poll_notifier = asyncio.Event()
        # Reset balance queue
        self._update_balances_fetching = False
        self._update_balances_queued = False
        self._update_balances_finished = asyncio.Event()

        self._order_book_tracker.stop()
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._trading_rules_polling_task is not None:
            self._trading_rules_polling_task.cancel()
            self._trading_rules_polling_task = None
        if self._status_polling_task is not None:
            self._status_polling_task.cancel()
            self._status_polling_task = None
        if self._user_stream_tracker_task is not None:
            self._user_stream_tracker_task.cancel()
            self._user_stream_tracker_task = None
        if self._user_stream_event_listener_task is not None:
            self._user_stream_event_listener_task.cancel()
            self._user_stream_event_listener_task = None

    async def check_network(self) -> NetworkStatus:
        """
        This function is required by NetworkIterator base class and is called periodically to check
        the network connection. Simply ping the network (or call any light weight public API).
        """
        try:
            # since there is no ping endpoint, the lowest rate call is to get BTC-USD symbol
            await self._api_request("GET", CONSTANTS.NETWORK_CHECK_PATH_URL)
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    async def _http_client(self) -> aiohttp.ClientSession:
        """
        :returns Shared client session instance
        """
        if self._shared_client is None:
            self._shared_client = aiohttp.ClientSession()
        return self._shared_client

    async def _trading_rules_polling_loop(self):
        """
        Periodically update trading rule.
        """
        while True:
            try:
                await self._update_trading_rules()
                await asyncio.sleep(CONSTANTS.INTERVAL_TRADING_RULES)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().network(
                    f"Unexpected error while fetching trading rules. Error: {str(e)}",
                    exc_info=True,
                    app_warning_msg=(
                        "Could not fetch new trading rules from "
                        f"{CONSTANTS.EXCHANGE_NAME}. Check network connection."
                    ))
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        symbols_info = await self._api_request(
            "GET", endpoint=CONSTANTS.SYMBOL_PATH_URL)
        self._trading_rules.clear()
        self._trading_rules = self._format_trading_rules(symbols_info)

    def _format_trading_rules(
            self, symbols_info: Dict[str, Any]) -> Dict[str, TradingRule]:
        """
        Converts json API response into a dictionary of trading rules.
        :param symbols_info: The json API response
        :return A dictionary of trading rules.
        Response Example:
        [
            {
                "id": "ETH_USDT",
                "base": "ETH",
                "quote": "USDT",
                "fee": "0.2",
                "min_base_amount": "0.001",
                "min_quote_amount": "1.0",
                "amount_precision": 3,
                "precision": 6,
                "trade_status": "tradable",
                "sell_start": 1516378650,
                "buy_start": 1516378650
            }
        ]
        """
        result = {}
        for rule in symbols_info:
            try:
                trading_pair = convert_from_exchange_trading_pair(rule["id"])
                min_amount_inc = Decimal(f"1e-{rule['amount_precision']}")
                min_price_inc = Decimal(f"1e-{rule['precision']}")
                min_amount = Decimal(
                    str(rule.get("min_base_amount", min_amount_inc)))
                min_notional = Decimal(
                    str(rule.get("min_quote_amount", min_price_inc)))
                result[trading_pair] = TradingRule(
                    trading_pair,
                    min_order_size=min_amount,
                    min_price_increment=min_price_inc,
                    min_base_amount_increment=min_amount_inc,
                    min_notional_size=min_notional,
                    min_order_value=min_notional,
                )
            except Exception:
                self.logger().error(
                    f"Error parsing the trading pair rule {rule}. Skipping.",
                    exc_info=True)
        return result

    async def _api_request(self,
                           method: str,
                           endpoint: str,
                           params: Optional[Dict[str, Any]] = None,
                           is_auth_required: bool = False,
                           try_count: int = 0,
                           limit_id: Optional[str] = None) -> Dict[str, Any]:
        """
        Sends an aiohttp request and waits for a response.
        :param method: The HTTP method, e.g. get or post
        :param endpoint: The path url or the API end point
        :param params: Additional get/post parameters
        :param is_auth_required: Whether an authentication is required, when True the function will add encrypted
        signature to the request.
        :returns A response in json format.
        """
        url = f"{CONSTANTS.REST_URL}/{endpoint}"
        limit_id = limit_id or endpoint
        shared_client = await self._http_client()
        # Turn `params` into either GET params or POST body data
        qs_params: dict = params if method.upper() != "POST" else None
        req_params = ujson.dumps(params) if method.upper(
        ) == "POST" and params is not None else None
        # Generate auth headers if needed.
        headers: dict = {"Content-Type": "application/json"}
        async with self._throttler.execute_task(limit_id=limit_id):
            if is_auth_required:
                headers: dict = self._gate_io_auth.get_headers(
                    method, f"{CONSTANTS.REST_URL_AUTH}/{endpoint}",
                    req_params if req_params is not None else params)
            # Build request coro
            response_coro = shared_client.request(
                method=method.upper(),
                url=url,
                headers=headers,
                params=qs_params,
                data=req_params,
                timeout=CONSTANTS.API_CALL_TIMEOUT)
            http_status, parsed_response, request_errors = await aiohttp_response_with_errors(
                response_coro)
        if request_errors or parsed_response is None:
            if try_count < CONSTANTS.API_MAX_RETRIES:
                try_count += 1
                time_sleep = retry_sleep_time(try_count)
                self.logger().info(
                    f"Error fetching data from {url}. HTTP status is {http_status}. "
                    f"Retrying in {time_sleep:.0f}s.")
                await asyncio.sleep(time_sleep)
                return await self._api_request(
                    method=method,
                    endpoint=endpoint,
                    params=params,
                    is_auth_required=is_auth_required,
                    try_count=try_count,
                    limit_id=limit_id)
            else:
                raise GateIoAPIError({
                    "label": "HTTP_ERROR",
                    "message": parsed_response,
                    "status": http_status
                })
        if "message" in parsed_response:
            raise GateIoAPIError(parsed_response)
        return parsed_response

    def get_order_price_quantum(self, trading_pair: str, price: Decimal):
        """
        Returns a price step, a minimum price increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str, order_size: Decimal):
        """
        Returns an order amount step, a minimum amount increment for a given trading pair.
        """
        trading_rule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    def get_order_book(self, trading_pair: str) -> OrderBook:
        if trading_pair not in self._order_book_tracker.order_books:
            raise ValueError(f"No order book exists for '{trading_pair}'.")
        return self._order_book_tracker.order_books[trading_pair]

    def buy(self,
            trading_pair: str,
            amount: Decimal,
            order_type=OrderType.LIMIT,
            price: Decimal = s_decimal_NaN,
            **kwargs) -> str:
        """
        Buys an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for BuyOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to buy from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = get_new_client_order_id(True, trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.BUY, order_id, trading_pair, amount,
                               order_type, price))
        return order_id

    def sell(self,
             trading_pair: str,
             amount: Decimal,
             order_type=OrderType.LIMIT,
             price: Decimal = s_decimal_NaN,
             **kwargs) -> str:
        """
        Sells an amount of base asset (of the given trading pair). This function returns immediately.
        To see an actual order, you'll have to wait for SellOrderCreatedEvent.
        :param trading_pair: The market (e.g. BTC-USDT) to sell from
        :param amount: The amount in base token value
        :param order_type: The order type
        :param price: The price (note: this is no longer optional)
        :returns A new internal order id
        """
        order_id: str = get_new_client_order_id(False, trading_pair)
        safe_ensure_future(
            self._create_order(TradeType.SELL, order_id, trading_pair, amount,
                               order_type, price))
        return order_id

    def cancel(self, trading_pair: str, order_id: str):
        """
        Cancel an order. This function returns immediately.
        To get the cancellation result, you'll have to wait for OrderCancelledEvent.
        :param trading_pair: The market (e.g. BTC-USDT) of the order.
        :param order_id: The internal order id (also called client_order_id)
        """
        safe_ensure_future(self._execute_cancel(trading_pair, order_id))
        return order_id

    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:
            self.logger().warning(
                f"{trade_type.name.title()} order amount {amount} is lower than the minimum order size "
                f"{trading_rule.min_order_size}.")
        else:
            order_type_str = order_type.name.lower().split("_")[0]
            api_params = {
                "text": order_id,
                "currency_pair":
                convert_to_exchange_trading_pair(trading_pair),
                "side": trade_type.name.lower(),
                "type": order_type_str,
                "price": f"{price:f}",
                "amount": f"{amount:f}",
            }
            self.start_tracking_order(order_id, None, trading_pair, trade_type,
                                      price, amount, order_type)
            try:
                order_result = await self._api_request(
                    "POST", CONSTANTS.ORDER_CREATE_PATH_URL, api_params, True)
                if order_result.get('status') in {
                        "cancelled", "expired", "failed"
                }:
                    raise GateIoAPIError({
                        'label': 'ORDER_REJECTED',
                        'message': 'Order rejected.'
                    })
                else:
                    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,
                                      exchange_order_id))
            except asyncio.CancelledError:
                raise
            except GateIoAPIError as e:
                error_reason = e.error_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))

    def start_tracking_order(self, order_id: str, exchange_order_id: str,
                             trading_pair: str, trade_type: TradeType,
                             price: Decimal, amount: Decimal,
                             order_type: OrderType):
        """
        Starts tracking an order by simply adding it into _in_flight_orders dictionary.
        """
        self._in_flight_orders[order_id] = GateIoInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trade_type,
            price=price,
            amount=amount)

    def stop_tracking_order(self, order_id: str):
        """
        Stops tracking an order by simply removing it from _in_flight_orders dictionary.
        """
        if order_id in self._in_flight_orders:
            del self._in_flight_orders[order_id]
        if order_id in self._order_not_found_records:
            del self._order_not_found_records[order_id]

    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 (Unused during cancel on Gate.io)
        :param order_id: The internal order id
        order.last_state to change to CANCELED
        """
        order_was_cancelled = False
        err_msg = None
        try:
            tracked_order = self._in_flight_orders.get(order_id)
            if tracked_order is None:
                self.logger().warning(
                    f"Failed to cancel order {order_id}. Order not found in inflight orders."
                )
            else:
                if tracked_order.exchange_order_id is None:
                    await tracked_order.get_exchange_order_id()
                ex_order_id = tracked_order.exchange_order_id
                await self._api_request(
                    "DELETE",
                    CONSTANTS.ORDER_DELETE_PATH_URL.format(id=ex_order_id),
                    params={
                        'currency_pair':
                        convert_to_exchange_trading_pair(trading_pair)
                    },
                    is_auth_required=True,
                    limit_id=CONSTANTS.ORDER_DELETE_LIMIT_ID)
                order_was_cancelled = True
        except asyncio.CancelledError:
            raise
        except (asyncio.TimeoutError, GateIoAPIError) as e:
            if isinstance(e, asyncio.TimeoutError):
                err_msg = 'Order not tracked.'
                err_lbl = 'ORDER_NOT_FOUND'
            else:
                err_msg = e.error_message
                err_lbl = e.error_label
            self._order_not_found_records[
                order_id] = self._order_not_found_records.get(order_id, 0) + 1
            if err_lbl == 'ORDER_NOT_FOUND' and \
                    self._order_not_found_records[order_id] >= self.ORDER_NOT_EXIST_CANCEL_COUNT:
                order_was_cancelled = True
        if order_was_cancelled:
            self.logger().info(
                f"Successfully cancelled order {order_id} on {CONSTANTS.EXCHANGE_NAME}."
            )
            self.stop_tracking_order(order_id)
            self.trigger_event(
                MarketEvent.OrderCancelled,
                OrderCancelledEvent(self.current_timestamp, order_id))
            tracked_order.cancelled_event.set()
            return CancellationResult(order_id, True)
        else:
            err_msg = err_msg or "(no details available)"
            self.logger().network(
                f"Failed to cancel order {order_id}: {err_msg}",
                exc_info=True,
                app_warning_msg=
                f"Failed to cancel the order {order_id} on {CONSTANTS.EXCHANGE_NAME}. "
                f"Check API key and network connection.")
            return CancellationResult(order_id, False)

    async def _status_polling_loop(self):
        """
        Periodically update user balances and order status via REST API. This serves as a fallback measure for web
        socket API updates.
        """
        while True:
            try:
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_order_status(),
                )
                self._last_poll_timestamp = (time.time() if math.isnan(
                    self.current_timestamp) else self.current_timestamp)
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(str(e), exc_info=True)
                warn_msg = (
                    f"Could not fetch account updates from {CONSTANTS.EXCHANGE_NAME}. "
                    "Check API key and network connection.")
                self.logger().network(
                    "Unexpected error while fetching account updates.",
                    exc_info=True,
                    app_warning_msg=warn_msg)
                await asyncio.sleep(0.5)
            finally:
                self._poll_notifier = asyncio.Event()

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances.
        """
        try:
            # Check for in progress balance updates, queue if fetching and none already waiting, otherwise skip.
            if self._update_balances_fetching:
                if not self._update_balances_queued:
                    self._update_balances_queued = True
                    await self._update_balances_finished.wait()
                    self._update_balances_queued = False
                    self._update_balances_finished = asyncio.Event()
                else:
                    return
            self._update_balances_fetching = True
            account_info = await self._api_request(
                "GET", CONSTANTS.USER_BALANCES_PATH_URL, is_auth_required=True)
            self._process_balance_message(account_info)
            self._update_balances_fetching = False
            # Set balance update finished event if there's one waiting.
            if self._update_balances_queued and not self._update_balances_finished.is_set(
            ):
                self._update_balances_finished.set()
        except Exception as e:
            if self._update_balances_queued:
                if self._update_balances_finished.is_set():
                    self._update_balances_finished = asyncio.Event()
                else:
                    self._update_balances_finished.set()
                self._update_balances_queued = False
            if self._update_balances_fetching:
                self._update_balances_fetching = False
            warn_msg = (
                f"Could not fetch balance update from {CONSTANTS.EXCHANGE_NAME}"
            )
            self.logger().network(
                f"Unexpected error while fetching balance update - {str(e)}",
                exc_info=True,
                app_warning_msg=warn_msg)

    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 _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": "52109248977",
            "text": "3",
            "create_time": "1622638707",
            "update_time": "1622638807",
            "currency_pair": "BTC_USDT",
            "type": "limit",
            "account": "spot",
            "side": "buy",
            "amount": "0.001",
            "price": "1999.8",
            "time_in_force": "gtc",
            "left": "0.001",
            "filled_total": "0",
            "fee": "0",
            "fee_currency": "BTC",
            "point_fee": "0",
            "gt_fee": "0",
            "gt_discount": true,
            "rebated_fee": "0",
            "rebated_fee_currency": "BTC",
            "create_time_ms": "1622638707326",
            "update_time_ms": "1622638807635",
            ... optional params
            "status": "open",
            "event": "finish"
            "iceberg": "0",
            "fill_price": "0",
            "user": 5660412,
        }
        """

        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 track_order:
            tracked_order = track_order[0]

            updated = tracked_order.update_with_order_update(order_msg)

            if updated:
                safe_ensure_future(
                    self._trigger_order_fill(tracked_order, order_msg))
            elif 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 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 _process_trade_message(self, trade_msg: Dict[str, Any]):
        """
        Updates in-flight order and trigger order filled event for trade message received. Triggers order completed
        event if the total executed amount equals to the specified order amount.
        Example Trade:
        {
            "id": 5736713,
            "user_id": 1000001,
            "order_id": "30784428",
            "currency_pair": "BTC_USDT",
            "create_time": 1605176741,
            "create_time_ms": "1605176741123.456",
            "side": "sell",
            "amount": "1.00000000",
            "role": "taker",
            "price": "10000.00000000",
            "fee": "0.00200000000000",
            "point_fee": "0",
            "gt_fee": "0",
            "text": "user_defined_text",
        }
        """
        client_order_id = str(trade_msg["text"])
        tracked_order = self.in_flight_orders.get(client_order_id, None)
        if tracked_order:
            updated = tracked_order.update_with_trade_update(trade_msg)
            if updated:
                safe_ensure_future(
                    self._trigger_order_fill(tracked_order, trade_msg))

    async def _trigger_order_fill(self, tracked_order: GateIoInFlightOrder,
                                  update_msg: Dict[str, Any]):
        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(
                        update_msg.get("fill_price",
                                       update_msg.get("price", "0")))),
                tracked_order.executed_amount_base,
                TradeFee(0.0,
                         [(tracked_order.fee_asset, tracked_order.fee_paid)]),
                str(update_msg.get("update_time_ms", update_msg.get("id")))))
        if math.isclose(tracked_order.executed_amount_base, tracked_order.amount) or \
                tracked_order.executed_amount_base >= tracked_order.amount or \
                tracked_order.is_done:
            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
            await asyncio.sleep(0.1)
            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,
                            tracked_order.fee_paid, tracked_order.order_type,
                            tracked_order.exchange_order_id))
            self.stop_tracking_order(tracked_order.client_order_id)

    def _process_balance_message(self, balance_update):
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()
        for account in balance_update:
            asset_name = account["currency"]
            self._account_available_balances[asset_name] = Decimal(
                str(account["available"]))
            self._account_balances[asset_name] = Decimal(str(
                account["locked"])) + Decimal(str(account["available"]))
            remote_asset_names.add(asset_name)

        asset_names_to_remove = local_asset_names.difference(
            remote_asset_names)
        for asset_name in asset_names_to_remove:
            del self._account_available_balances[asset_name]
            del self._account_balances[asset_name]

        self._in_flight_orders_snapshot = {
            k: copy.copy(v)
            for k, v in self._in_flight_orders.items()
        }
        self._in_flight_orders_snapshot_timestamp = self.current_timestamp

    def _process_balance_message_ws(self, balance_update):
        for account in balance_update:
            asset_name = account["currency"]
            self._account_available_balances[asset_name] = Decimal(
                str(account["available"]))
            self._account_balances[asset_name] = Decimal(str(account["total"]))

        self._in_flight_orders_snapshot = {
            k: copy.copy(v)
            for k, v in self._in_flight_orders.items()
        }
        self._in_flight_orders_snapshot_timestamp = self.current_timestamp

    async def cancel_all(self,
                         timeout_seconds: float) -> List[CancellationResult]:
        """
        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."
            )
        open_orders = [
            o for o in self._in_flight_orders.values() if not o.is_done
        ]
        if len(open_orders) == 0:
            return []
        tasks = [
            self._execute_cancel(o.trading_pair, o.client_order_id)
            for o in open_orders
        ]
        cancellation_results = []
        cancel_timeout = timeout_seconds * len(open_orders) if len(
            open_orders) else timeout_seconds
        try:
            async with timeout(cancel_timeout):
                cancellation_results = await safe_gather(
                    *tasks, return_exceptions=False)
        except Exception:
            self.logger().network(
                "Unexpected error cancelling orders.",
                exc_info=True,
                app_warning_msg=
                (f"Failed to cancel all orders on {CONSTANTS.EXCHANGE_NAME}. "
                 "Check API key and network connection."))
        return cancellation_results

    def tick(self, timestamp: float):
        """
        Is called automatically by the clock for each clock's tick (1 second by default).
        It checks if status polling task is due for execution.
        """
        now = time.time()
        # Using 120 seconds here as Gate.io websocket is quiet
        poll_interval = (CONSTANTS.SHORT_POLL_INTERVAL if
                         now - self._user_stream_tracker.last_recv_time > 120.0
                         else CONSTANTS.LONG_POLL_INTERVAL)
        last_tick = int(self._last_timestamp / poll_interval)
        current_tick = int(timestamp / poll_interval)
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    def get_fee(self,
                base_currency: str,
                quote_currency: str,
                order_type: OrderType,
                order_side: TradeType,
                amount: Decimal,
                price: Decimal = s_decimal_NaN) -> TradeFee:
        """
        To get trading fee, this function is simplified by using fee override configuration. Most parameters to this
        function are ignore except order_type. Use OrderType.LIMIT_MAKER to specify you want trading fee for
        maker order.
        """
        is_maker = order_type is OrderType.LIMIT_MAKER
        return TradeFee(percent=self.estimate_fee_pct(is_maker))

    async def _iter_user_event_queue(self) -> AsyncIterable[Dict[str, any]]:
        while True:
            try:
                yield await self._user_stream_tracker.user_stream.get()
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().network(
                    "Unknown error. Retrying after 1 seconds.",
                    exc_info=True,
                    app_warning_msg=
                    (f"Could not fetch user events from {CONSTANTS.EXCHANGE_NAME}. "
                     "Check API key and network connection."))
                await asyncio.sleep(1.0)

    async def _user_stream_event_listener(self):
        """
        Listens to message in _user_stream_tracker.user_stream queue. The messages are put in by
        GateIoAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                user_channels = [
                    CONSTANTS.USER_TRADES_ENDPOINT_NAME,
                    CONSTANTS.USER_ORDERS_ENDPOINT_NAME,
                    CONSTANTS.USER_BALANCE_ENDPOINT_NAME,
                ]

                channel: str = event_message.get("channel", None)
                params: str = event_message.get("result", None)

                if channel not in user_channels:
                    self.logger().error(
                        f"Unexpected message in user stream: {event_message}.",
                        exc_info=True)
                    continue
                if channel == CONSTANTS.USER_TRADES_ENDPOINT_NAME:
                    for trade_msg in params:
                        await self._process_trade_message(trade_msg)
                elif channel == CONSTANTS.USER_ORDERS_ENDPOINT_NAME:
                    for order_msg in params:
                        self._process_order_message(order_msg)
                elif channel == CONSTANTS.USER_BALANCE_ENDPOINT_NAME:
                    self._process_balance_message_ws(params)
            except asyncio.CancelledError:
                raise
            except Exception:
                self.logger().error(
                    "Unexpected error in user stream listener loop.",
                    exc_info=True)
                await asyncio.sleep(5.0)

    # This is currently unused, but looks like a future addition.
    async def get_open_orders(self) -> List[OpenOrder]:
        result = await self._api_request("GET",
                                         CONSTANTS.USER_ORDERS_PATH_URL,
                                         is_auth_required=True)
        ret_val = []
        for pair_orders in result:
            for order in pair_orders["orders"]:
                if CONSTANTS.HBOT_ORDER_ID not in order["text"]:
                    continue
                if order["type"] != OrderType.LIMIT.name.lower():
                    self.logger().info(
                        f"Unsupported order type found: {order['type']}")
                    continue
                ret_val.append(
                    OpenOrder(client_order_id=order["text"],
                              trading_pair=convert_from_exchange_trading_pair(
                                  order["currency_pair"]),
                              price=Decimal(str(order["price"])),
                              amount=Decimal(str(order["amount"])),
                              executed_amount=Decimal(
                                  str(order["filled_total"])),
                              status=order["status"],
                              order_type=OrderType.LIMIT,
                              is_buy=True if order["side"].lower()
                              == TradeType.BUY.name.lower() else False,
                              time=int(order["create_time"]),
                              exchange_order_id=order["id"]))
        return ret_val