def setUpClass(cls):
     cls.ev_loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
     cls.throttler = AsyncThrottler(CONSTANTS.RATE_LIMITS)
     cls.order_book_tracker: GateIoOrderBookTracker = GateIoOrderBookTracker(
         cls.throttler, cls.trading_pairs)
     cls.order_book_tracker.start()
     cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())
Esempio n. 2
0
 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._api_factory = build_gate_io_api_factory()
     self._rest_assistant: Optional[RESTAssistant] = None
     self._order_book_tracker = GateIoOrderBookTracker(
         self._throttler, trading_pairs, self._api_factory)
     self._user_stream_tracker = GateIoUserStreamTracker(
         self._gate_io_auth, trading_pairs, self._api_factory)
     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, 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()
Esempio n. 3
0
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
 def setUpClass(cls):
     cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
     cls.order_book_tracker: GateIoOrderBookTracker = GateIoOrderBookTracker(
         cls.trading_pairs)
     cls.order_book_tracker.start()
     cls.ev_loop.run_until_complete(cls.wait_til_tracker_ready())