Ejemplo n.º 1
0
 def __init__(self,
              digifinex_api_key: str,
              digifinex_secret_key: str,
              trading_pairs: Optional[List[str]] = None,
              trading_required: bool = True):
     """
     :param key: The API key to connect to private digifinex.com APIs.
     :param secret: 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._global = DigifinexGlobal(digifinex_api_key, digifinex_secret_key)
     # self._rest_api = DigifinexRestApi(self._digifinex_auth, self._http_client)
     self._order_book_tracker = DigifinexOrderBookTracker(
         trading_pairs=trading_pairs)
     self._user_stream_tracker = DigifinexUserStreamTracker(
         self._global, trading_pairs)
     self._ev_loop = asyncio.get_event_loop()
     self._poll_notifier = asyncio.Event()
     self._last_timestamp = 0
     self._in_flight_orders: Dict[str, DigifinexInFlightOrder] = {
     }  # Dict[client_order_id:str, DigifinexInFlightOrder]
     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
Ejemplo n.º 2
0
 def setUpClass(cls):
     cls.ev_loop: asyncio.BaseEventLoop = asyncio.get_event_loop()
     cls._global = DigifinexGlobal(cls.api_key, cls.api_secret)
     cls.trading_pairs = ["BTC-USDT"]
     cls.user_stream_tracker: DigifinexUserStreamTracker = DigifinexUserStreamTracker(
         cls._global, trading_pairs=cls.trading_pairs)
     cls.user_stream_tracker_task: asyncio.Task = safe_ensure_future(cls.user_stream_tracker.start())
Ejemplo n.º 3
0
class DigifinexExchange(ExchangeBase):
    """
    DigifinexExchange connects with digifinex.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,
                 digifinex_api_key: str,
                 digifinex_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True):
        """
        :param key: The API key to connect to private digifinex.com APIs.
        :param secret: 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._global = DigifinexGlobal(digifinex_api_key, digifinex_secret_key)
        # self._rest_api = DigifinexRestApi(self._digifinex_auth, self._http_client)
        self._order_book_tracker = DigifinexOrderBookTracker(
            trading_pairs=trading_pairs)
        self._user_stream_tracker = DigifinexUserStreamTracker(
            self._global, trading_pairs)
        self._ev_loop = asyncio.get_event_loop()
        self._poll_notifier = asyncio.Event()
        self._last_timestamp = 0
        self._in_flight_orders: Dict[str, DigifinexInFlightOrder] = {
        }  # Dict[client_order_id:str, DigifinexInFlightOrder]
        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 "digifinex"

    @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, DigifinexInFlightOrder]:
        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: DigifinexInFlightOrder.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._global.rest_api.request("get", "ping")
        except asyncio.CancelledError:
            raise
        except Exception as e:
            _ = e
            self.logger().exception('check_network', stack_info=True)
            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 digifinex.com. "
                    "Check network connection.")
                await asyncio.sleep(0.5)

    async def _update_trading_rules(self):
        instruments_info = await self._global.rest_api.request(
            "get", path_url="markets")
        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:
        {
            "data": [{
                "volume_precision": 4,
                "price_precision": 2,
                "market": "btc_usdt",
                "min_amount": 2,
                "min_volume": 0.0001
            }],
            "date": 1589873858,
            "code": 0
        }
        """
        result = {}
        for rule in instruments_info["data"]:
            try:
                trading_pair = digifinex_utils.convert_from_exchange_trading_pair(
                    rule["market"])
                price_decimals = Decimal(str(rule["price_precision"]))
                quantity_decimals = Decimal(str(rule["volume_precision"]))
                # 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

    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 = digifinex_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 = digifinex_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)
        """
        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:
            self.ev_loop.run_until_complete(
                tracked_order.get_exchange_order_id())
        safe_ensure_future(self._execute_cancel(tracked_order))
        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}.")
        symbol = digifinex_utils.convert_to_exchange_trading_pair(trading_pair)
        api_params = {
            "symbol": symbol,
            "type": trade_type.name.lower(),
            "price": f"{price:f}",
            "amount": f"{amount:f}",
            # "client_oid": order_id
        }
        if order_type is OrderType.LIMIT_MAKER:
            api_params["post_only"] = 1
        self.start_tracking_order(order_id, None, trading_pair, trade_type,
                                  price, amount, order_type)
        try:
            order_result = await self._global.rest_api.request(
                "post", "spot/order/new", api_params, True)
            exchange_order_id = str(order_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 Digifinex 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] = DigifinexInFlightOrder(
            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, o: DigifinexInFlightOrder) -> 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:
            await self._global.rest_api.request(
                "post", "spot/order/cancel", {"order_id": o.exchange_order_id},
                True)
            if o.client_order_id in self._in_flight_orders:
                self.trigger_event(
                    MarketEvent.OrderCancelled,
                    OrderCancelledEvent(self.current_timestamp,
                                        o.client_order_id))
                del self._in_flight_orders[o.client_order_id]
            return o.exchange_order_id
        except asyncio.CancelledError:
            raise
        except Exception as e:
            self.logger().network(
                f"Failed to cancel order {o.exchange_order_id}: {str(e)}",
                exc_info=True,
                app_warning_msg=
                f"Failed to cancel the order {o.exchange_order_id} on Digifinex. "
                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 Digifinex. "
                    "Check API key and network connection.")
                await asyncio.sleep(0.5)

    async def _update_balances(self):
        local_asset_names = set(self._account_balances.keys())
        remote_asset_names = set()
        account_info = await self._global.rest_api.get_balance()
        for account in account_info["list"]:
            asset_name = account["currency"]
            self._account_available_balances[asset_name] = Decimal(
                str(account["free"]))
            self._account_balances[asset_name] = Decimal(str(account["total"]))
            remote_asset_names.add(asset_name)

        try:
            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 Exception as e:
            self.logger().error(e)

    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._global.rest_api.request("get", "spot/order/detail",
                                                  {"order_id": order_id},
                                                  True))
            self.logger().debug(
                f"Polling for order status updates of {len(tasks)} orders.")
            update_results = await safe_gather(*tasks, return_exceptions=True)
            for update_result in update_results:
                if isinstance(update_result, Exception):
                    raise update_result
                if "data" not in update_result:
                    self.logger().info(
                        f"_update_order_status result not in resp: {update_result}"
                    )
                    continue
                order_data = update_result["data"]
                self._process_rest_trade_details(order_data)
                self._process_order_status(order_data.get('order_id'),
                                           order_data.get('status'))

    def _process_order_status(self, exchange_order_id: str, status: int):
        """
        Updates in-flight order and triggers cancellation or failure event if needed.
        """
        tracked_order = self.find_exchange_order(exchange_order_id)
        if tracked_order is None:
            return
        client_order_id = tracked_order.client_order_id
        # Update order execution status
        tracked_order.last_state = str(status)
        if tracked_order.is_cancelled:
            self.logger().info(
                f"Successfully cancelled order {client_order_id}.")
            self.trigger_event(
                MarketEvent.OrderCancelled,
                OrderCancelledEvent(self.current_timestamp, client_order_id))
            tracked_order.cancelled_event.set()
            self.stop_tracking_order(client_order_id)
        # elif tracked_order.is_failure:
        #     self.logger().info(f"The market order {client_order_id} has failed according to order status API. "
        #                        f"Reason: {digifinex_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)

    def _process_rest_trade_details(self, order_detail_msg: Any):
        for trade_msg in order_detail_msg['detail']:
            """
            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()
            tracked_order = self.find_exchange_order(trade_msg['order_id'])
            if tracked_order is None:
                return

            updated = tracked_order.update_with_rest_order_detail(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["executed_price"])),
                    Decimal(str(trade_msg["executed_amount"])),
                    estimate_fee(
                        self.name, tracked_order.order_type
                        in [OrderType.LIMIT, OrderType.LIMIT_MAKER]),
                    # TradeFee(0.0, [(trade_msg["fee_currency"], Decimal(str(trade_msg["fee"])))]),
                    exchange_trade_id=trade_msg["tid"]))
            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)

    def find_exchange_order(self, exchange_order_id: str):
        for o in self._in_flight_orders.values():
            if o.exchange_order_id == exchange_order_id:
                return o

    def _process_order_message_traded(self, order_msg):
        tracked_order: DigifinexInFlightOrder = self.find_exchange_order(
            order_msg['id'])
        if tracked_order is None:
            return

        (delta_trade_amount,
         delta_trade_price) = tracked_order.update_with_order_update(order_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]),
                # TradeFee(0.0, [(trade_msg["fee_currency"], Decimal(str(trade_msg["fee"])))]),
                exchange_trade_id=str(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 = "2"
            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."
            )
        cancellation_results = []
        try:
            # for trading_pair in self._trading_pairs:
            #     await self._global.rest_api.request(
            #         "post",
            #         "private/cancel-all-orders",
            #         {"instrument_name": digifinex_utils.convert_to_exchange_trading_pair(trading_pair)},
            #         True
            #     )

            open_orders = list(self._in_flight_orders.values())
            for o in open_orders:
                await self._execute_cancel(o)

            for cl_order_id, tracked_order in self._in_flight_orders.items():
                open_order = [
                    o for o in open_orders
                    if o.exchange_order_id == tracked_order.exchange_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))
                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 Digifinex. 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 Digifinex. 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
        DigifinexAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                if "method" not in event_message:
                    continue
                channel = event_message["method"]
                # if "user.trade" in channel:
                #     for trade_msg in event_message["result"]["data"]:
                #         await self._process_trade_message(trade_msg)
                if "order.update" in channel:
                    for order_msg in event_message["params"]:
                        self._process_order_status(order_msg['id'],
                                                   order_msg['status'])
                        self._process_order_message_traded(order_msg)
                elif channel == "balance.update":
                    balances = event_message["params"]
                    for balance_entry in balances:
                        asset_name = balance_entry["currency"]
                        self._account_balances[asset_name] = Decimal(
                            str(balance_entry["total"]))
                        self._account_available_balances[asset_name] = Decimal(
                            str(balance_entry["free"]))
            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._global.rest_api.request("get",
                                                     "spot/order/current", {},
                                                     True)
        ret_val = []
        for order in result["data"]:
            # if digifinex_utils.HBOT_BROKER_ID not in order["client_oid"]:
            #     continue
            if order["type"] not in ["buy", "sell"]:
                raise Exception(f"Unsupported order type {order['type']}")
            ret_val.append(
                OpenOrder(client_order_id=None,
                          trading_pair=digifinex_utils.
                          convert_from_exchange_trading_pair(order["symbol"]),
                          price=Decimal(str(order["price"])),
                          amount=Decimal(str(order["amount"])),
                          executed_amount=Decimal(str(
                              order["executed_amount"])),
                          status=order["status"],
                          order_type=OrderType.LIMIT,
                          is_buy=True if order["type"] == "buy" else False,
                          time=int(order["created_date"]),
                          exchange_order_id=order["order_id"]))
        return ret_val