コード例 #1
0
 def __init__(self,
              altmarkets_api_key: str,
              altmarkets_secret_key: str,
              trading_pairs: Optional[List[str]] = None,
              trading_required: bool = True
              ):
     """
     :param altmarkets_api_key: The API key to connect to private AltMarkets.io APIs.
     :param altmarkets_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._altmarkets_auth = AltmarketsAuth(altmarkets_api_key, altmarkets_secret_key)
     self._order_book_tracker = AltmarketsOrderBookTracker(trading_pairs=trading_pairs)
     self._user_stream_tracker = AltmarketsUserStreamTracker(self._altmarkets_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, AltmarketsInFlightOrder]
     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 = Throttler(rate_limit = (10.0, 6.5))
コード例 #2
0
    def __init__(self,
                 binance_perpetual_api_key: str = None,
                 binance_perpetual_api_secret: str = None,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True,
                 **domain):
        self._testnet = True if len(domain) > 0 else False
        super().__init__()
        self._api_key = binance_perpetual_api_key
        self._api_secret = binance_perpetual_api_secret
        self._trading_required = trading_required
        # self._account_balances = {}
        # self._account_available_balances = {}

        self._base_url = PERPETUAL_BASE_URL if self._testnet is False else TESTNET_BASE_URL
        self._stream_url = DIFF_STREAM_URL if self._testnet is False else TESTNET_STREAM_URL
        self._user_stream_tracker = BinancePerpetualUserStreamTracker(base_url=self._base_url, stream_url=self._stream_url, api_key=self._api_key)
        self._order_book_tracker = BinancePerpetualOrderBookTracker(trading_pairs=trading_pairs, **domain)
        self._ev_loop = asyncio.get_event_loop()
        self._poll_notifier = asyncio.Event()
        self._in_flight_orders = {}
        self._order_not_found_records = {}
        self._last_timestamp = 0
        self._trading_rules = {}
        # self._trade_fees = {}
        # 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._throttler = Throttler((10.0, 1.0))
        self._funding_rate = 0
        self._account_positions = {}
        self._position_mode = None
コード例 #3
0
class AsyncioThrottleTest(unittest.TestCase):
    def setUp(self):
        self.throttler = Throttler(rate_limit=(20, 1),
                                   period_safety_margin=0,
                                   retry_interval=0)
        self.loop = asyncio.get_event_loop()

    async def task(self, task_id, weight):
        async with self.throttler.weighted_task(weight):
            print(int(time.time()), f"Cat {task_id}: Meow {weight}")

    def test_tasks_complete_without_delay_when_throttle_below_rate_limit(self):
        tasks = [
            self.task(1, 5),
            self.task(3, 5),
            self.task(3, 5),
            self.task(4, 4)
        ]
        with patch('hummingbot.core.utils.asyncio_throttle.asyncio.sleep'
                   ) as sleep_patch:
            self.loop.run_until_complete(asyncio.gather(*tasks))
        self.assertEqual(sleep_patch.call_count, 0)

    def test_tasks_complete_with_delay_when_throttle_above_rate_limit(self):
        tasks = [
            self.task(1, 5),
            self.task(3, 5),
            self.task(3, 5),
            self.task(4, 6)
        ]
        with patch('hummingbot.core.utils.asyncio_throttle.asyncio.sleep'
                   ) as sleep_patch:
            self.loop.run_until_complete(asyncio.gather(*tasks))
        self.assertGreater(sleep_patch.call_count, 0)
コード例 #4
0
class AltmarketsExchange(ExchangeBase):
    """
    AltmarketsExchange connects with AltMarkets.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,
                 altmarkets_api_key: str,
                 altmarkets_secret_key: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True
                 ):
        """
        :param altmarkets_api_key: The API key to connect to private AltMarkets.io APIs.
        :param altmarkets_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._altmarkets_auth = AltmarketsAuth(altmarkets_api_key, altmarkets_secret_key)
        self._order_book_tracker = AltmarketsOrderBookTracker(trading_pairs=trading_pairs)
        self._user_stream_tracker = AltmarketsUserStreamTracker(self._altmarkets_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, AltmarketsInFlightOrder]
        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 = Throttler(rate_limit = (10.0, 6.5))

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

    @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, AltmarketsInFlightOrder]:
        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: AltmarketsInFlightOrder.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.MARKET]

    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(method="GET", endpoint=Constants.ENDPOINT['TIMESTAMP'])
        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.ENDPOINT['SYMBOL'])
        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: "btcusdt",
                name: "BTC/USDT",
                base_unit: "btc",
                quote_unit: "usdt",
                min_price: "0.01",
                max_price: "200000.0",
                min_amount: "0.00000001",
                amount_precision: 8,
                price_precision: 2,
                state: "enabled"
            }
        ]
        """
        result = {}
        for rule in symbols_info:
            try:
                trading_pair = convert_from_exchange_trading_pair(rule["id"])
                min_amount = Decimal(rule["min_amount"])
                min_notional = min(Decimal(rule["min_price"]) * min_amount, Decimal("0.00000001"))
                result[trading_pair] = TradingRule(trading_pair,
                                                   min_order_size=min_amount,
                                                   min_base_amount_increment=Decimal(f"1e-{rule['amount_precision']}"),
                                                   min_notional_size=min_notional,
                                                   min_price_increment=Decimal(f"1e-{rule['price_precision']}"))
            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) -> 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.
        """
        async with self._throttler.weighted_task(request_weight=1):
            url = f"{Constants.REST_URL}/{endpoint}"
            shared_client = await self._http_client()
            # Turn `params` into either GET params or POST body data
            qs_params: dict = params if method.upper() == "GET" 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"}
            if is_auth_required:
                headers: dict = self._altmarkets_auth.get_headers()
            # 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)
                else:
                    raise AltmarketsAPIError({"error": parsed_response, "status": http_status})
            if "error" in parsed_response:
                raise AltmarketsAPIError(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 = 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 = 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}.")
        order_type_str = order_type.name.lower().split("_")[0]
        api_params = {"market": convert_to_exchange_trading_pair(trading_pair),
                      "side": trade_type.name.lower(),
                      "ord_type": order_type_str,
                      "price": f"{price:f}",
                      "volume": f"{amount:f}",
                      }
        # if order_type is OrderType.LIMIT_MAKER:
        #     api_params["postOnly"] = "true"
        self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type)
        try:
            order_result = await self._api_request("POST", Constants.ENDPOINT["ORDER_CREATE"], api_params, True)
            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))
        except asyncio.CancelledError:
            raise
        except AltmarketsAPIError as e:
            error_reason = e.error_payload.get('error', {}).get('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] = AltmarketsInFlightOrder(
            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 AltMarkets.io)
        :param order_id: The internal order id
        order.last_state to change to CANCELED
        """
        order_state, errors_found = None, {}
        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()
            exchange_order_id = tracked_order.exchange_order_id
            response = await self._api_request("POST",
                                               Constants.ENDPOINT["ORDER_DELETE"].format(id=exchange_order_id),
                                               is_auth_required=True)
            if isinstance(response, dict):
                order_state = response.get("state", None)
        except asyncio.CancelledError:
            raise
        except AltmarketsAPIError as e:
            errors_found = e.error_payload.get('error', e.error_payload)
            order_state = errors_found.get("state", None)
            if order_state is None:
                self._order_not_found_records[order_id] = self._order_not_found_records.get(order_id, 0) + 1
        if order_state in Constants.ORDER_STATES['CANCEL_WAIT'] or \
                self._order_not_found_records.get(order_id, 0) >= self.ORDER_NOT_EXIST_CANCEL_COUNT:
            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:
            self.logger().network(
                f"Failed to cancel order {order_id}: {errors_found.get('message', errors_found)}",
                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:
                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)
                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)

    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.ENDPOINT["USER_BALANCES"], is_auth_required=True)
        for account in account_info:
            asset_name = account["currency"].upper()
            self._account_available_balances[asset_name] = Decimal(str(account["balance"]))
            self._account_balances[asset_name] = Decimal(str(account["locked"])) + 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 / Constants.UPDATE_ORDER_STATUS_INTERVAL)
        current_tick = 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:
                exchange_order_id = await tracked_order.get_exchange_order_id()
                tasks.append(self._api_request("GET",
                                               Constants.ENDPOINT["ORDER_STATUS"].format(id=exchange_order_id),
                                               is_auth_required=True))
            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, AltmarketsAPIError):
                    err = response.error_payload.get('error', response.error_payload).get('errors', None)
                    if "record.not_found" in err:
                        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 order 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": 9401,
            "market": "rogerbtc",
            "kind": "ask",
            "side": "sell",
            "ord_type": "limit",
            "price": "0.00000099",
            "avg_price": "0.00000099",
            "state": "wait",
            "origin_volume": "7000.0",
            "remaining_volume": "2810.1",
            "executed_volume": "4189.9",
            "at": 1596481983,
            "created_at": 1596481983,
            "updated_at": 1596553643,
            "trades_count": 272
        }
        """
        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 not track_order:
            return
        tracked_order = track_order[0]
        # Estimate fee
        order_msg["trade_fee"] = self.estimate_fee_pct(tracked_order.order_type is OrderType.LIMIT_MAKER)
        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 market 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.
        """
        exchange_order_id = str(trade_msg["order_id"])

        tracked_orders = list(self._in_flight_orders.values())
        for order in tracked_orders:
            await order.get_exchange_order_id()
        track_order = [o for o in tracked_orders if exchange_order_id == o.exchange_order_id]

        if not track_order:
            return
        tracked_order = track_order[0]

        # Estimate fee
        trade_msg["trade_fee"] = self.estimate_fee_pct(tracked_order.order_type is OrderType.LIMIT_MAKER)
        updated = tracked_order.update_with_trade_update(trade_msg)

        if not updated:
            return

        await self._trigger_order_fill(tracked_order, trade_msg)

    async def _trigger_order_fill(self,
                                  tracked_order: AltmarketsInFlightOrder,
                                  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("price", "0"))),
                tracked_order.executed_amount_base,
                TradeFee(percent=update_msg["trade_fee"]),
            )
        )
        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))
            self.stop_tracking_order(tracked_order.client_order_id)

    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 = []
        try:
            async with timeout(timeout_seconds):
                cancellation_results = await safe_gather(*tasks, return_exceptions=False)
        except Exception:
            print("cancel all error")
            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()
        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
        AltmarketsAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                event_methods = [
                    Constants.WS_METHODS["USER_ORDERS"],
                    Constants.WS_METHODS["USER_TRADES"],
                ]

                for method in list(event_message.keys()):
                    params: dict = event_message.get(method, None)

                    if params is None or method not in event_methods:
                        continue
                    if method == Constants.WS_METHODS["USER_TRADES"]:
                        await self._process_trade_message(params)
                    elif method == Constants.WS_METHODS["USER_ORDERS"]:
                        self._process_order_message(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]:
        tracked_orders = list(self._in_flight_orders.values())
        # for order in tracked_orders:
        #     await order.get_exchange_order_id()
        result = await self._api_request("GET", Constants.ENDPOINT["USER_ORDERS"], is_auth_required=True)
        ret_val = []
        for order in result:
            if order["state"] in Constants.ORDER_STATES['DONE']:
                # Skip done orders
                continue
            exchange_order_id = str(order["id"])
            # AltMarkets doesn't support client order ids yet so we must find it from the tracked orders.
            track_order = [o for o in tracked_orders if exchange_order_id == o.exchange_order_id]
            if not track_order or len(track_order) < 1:
                # Skip untracked orders
                continue
            client_order_id = track_order[0].client_order_id
            if order["ord_type"] != OrderType.LIMIT.name.lower():
                self.logger().info(f"Unsupported order type found: {order['type']}")
                # Skip and report non-limit orders
                continue
            ret_val.append(
                OpenOrder(
                    client_order_id=client_order_id,
                    trading_pair=convert_from_exchange_trading_pair(order["market"]),
                    price=Decimal(str(order["price"])),
                    amount=Decimal(str(order["origin_volume"])),
                    executed_amount=Decimal(str(order["executed_volume"])),
                    status=order["state"],
                    order_type=OrderType.LIMIT,
                    is_buy=True if order["side"].lower() == TradeType.BUY.name.lower() else False,
                    time=str_date_to_ts(order["created_at"]),
                    exchange_order_id=exchange_order_id
                )
            )
        return ret_val
コード例 #5
0
class BinancePerpetualDerivative(DerivativeBase):
    MARKET_RECEIVED_ASSET_EVENT_TAG = MarketEvent.ReceivedAsset
    MARKET_BUY_ORDER_COMPLETED_EVENT_TAG = MarketEvent.BuyOrderCompleted
    MARKET_SELL_ORDER_COMPLETED_EVENT_TAG = MarketEvent.SellOrderCompleted
    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
    SHORT_POLL_INTERVAL = 5.0
    UPDATE_ORDER_STATUS_MIN_INTERVAL = 10.0
    LONG_POLL_INTERVAL = 120.0
    ORDER_NOT_EXIST_CONFIRMATION_COUNT = 3

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

    def __init__(self,
                 binance_perpetual_api_key: str = None,
                 binance_perpetual_api_secret: str = None,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True,
                 **domain):
        self._testnet = True if len(domain) > 0 else False
        super().__init__()
        self._api_key = binance_perpetual_api_key
        self._api_secret = binance_perpetual_api_secret
        self._trading_required = trading_required
        # self._account_balances = {}
        # self._account_available_balances = {}

        self._base_url = PERPETUAL_BASE_URL if self._testnet is False else TESTNET_BASE_URL
        self._stream_url = DIFF_STREAM_URL if self._testnet is False else TESTNET_STREAM_URL
        self._user_stream_tracker = BinancePerpetualUserStreamTracker(base_url=self._base_url, stream_url=self._stream_url, api_key=self._api_key)
        self._order_book_tracker = BinancePerpetualOrderBookTracker(trading_pairs=trading_pairs, **domain)
        self._ev_loop = asyncio.get_event_loop()
        self._poll_notifier = asyncio.Event()
        self._in_flight_orders = {}
        self._order_not_found_records = {}
        self._last_timestamp = 0
        self._trading_rules = {}
        # self._trade_fees = {}
        # 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._throttler = Throttler((10.0, 1.0))
        self._funding_rate = 0
        self._account_positions = {}
        self._position_mode = None

    @property
    def name(self) -> str:
        return "binance_perpetual_testnet" if self._testnet else "binance_perpetual"

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

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

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

    @property
    def status_dict(self):
        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,

            # TODO: Uncomment when figured out trade fees
            # "trade_fees_initialized": len(self._trade_fees) > 0
        }

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

    def start(self, clock: Clock, timestamp: float):
        super().start(clock, timestamp)

    def stop(self, clock: Clock):
        super().stop(clock)

    async def start_network(self):
        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())

    def _stop_network(self):
        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 stop_network(self):
        self._stop_network()

    async def check_network(self) -> NetworkStatus:
        try:
            await self.request("/fapi/v1/ping")
        except asyncio.CancelledError:
            raise
        except Exception:
            return NetworkStatus.NOT_CONNECTED
        return NetworkStatus.CONNECTED

    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.MARKET]

    # ORDER PLACE AND CANCEL EXECUTIONS ---
    async def create_order(self,
                           trade_type: TradeType,
                           order_id: str,
                           trading_pair: str,
                           amount: Decimal,
                           order_type: OrderType,
                           position_action: PositionAction,
                           price: Optional[Decimal] = Decimal("NaN")):

        trading_rule: TradingRule = self._trading_rules[trading_pair]
        if position_action not in [PositionAction.OPEN, PositionAction.CLOSE]:
            raise ValueError("Specify either OPEN_POSITION or CLOSE_POSITION position_action.")

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

        order_result = None
        api_params = {"symbol": convert_to_exchange_trading_pair(trading_pair),
                      "side": "BUY" if trade_type is TradeType.BUY else "SELL",
                      "type": "LIMIT" if order_type is OrderType.LIMIT else "MARKET",
                      "quantity": f"{amount}",
                      "newClientOrderId": order_id
                      }
        if order_type == OrderType.LIMIT:
            api_params["price"] = f"{price}"
            api_params["timeInForce"] = "GTC"

        if self._position_mode == PositionMode.HEDGE:
            if position_action == PositionAction.OPEN:
                api_params["positionSide"] = "LONG" if trade_type is TradeType.BUY else "SHORT"
            else:
                api_params["positionSide"] = "SHORT" if trade_type is TradeType.BUY else "LONG"

        self.start_tracking_order(order_id, "", trading_pair, trade_type, price, amount, order_type)

        try:
            order_result = await self.request(path="/fapi/v1/order",
                                              params=api_params,
                                              method=MethodType.POST,
                                              add_timestamp = True,
                                              is_signed=True)
            exchange_order_id = str(order_result["orderId"])
            tracked_order = self._in_flight_orders.get(order_id)
            if tracked_order is not None:
                self.logger().info(f"Created {order_type.name.lower()} {trade_type.name.lower()} order {order_id} for "
                                   f"{amount} {trading_pair}.")
                tracked_order.exchange_order_id = exchange_order_id

            event_tag = self.MARKET_BUY_ORDER_CREATED_EVENT_TAG if trade_type is TradeType.BUY \
                else self.MARKET_SELL_ORDER_CREATED_EVENT_TAG
            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))
            return order_result
        except asyncio.CancelledError:
            raise
        except Exception as e:
            self.stop_tracking_order(order_id)
            self.logger().network(
                f"Error submitting order to Binance Perpetuals for {amount} {trading_pair} "
                f"{'' if order_type is OrderType.MARKET else price}.",
                exc_info=True,
                app_warning_msg=str(e)
            )
            self.trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG,
                               MarketOrderFailureEvent(self.current_timestamp, order_id, order_type))

    async def execute_buy(self,
                          order_id: str,
                          trading_pair: str,
                          amount: Decimal,
                          order_type: OrderType,
                          position_action: PositionAction,
                          price: Optional[Decimal] = s_decimal_NaN):
        return await self.create_order(TradeType.BUY, order_id, trading_pair, amount, order_type, position_action, price)

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

        t_pair: str = trading_pair
        order_id: str = get_client_order_id("sell", t_pair)
        safe_ensure_future(self.execute_buy(order_id, trading_pair, amount, order_type, kwargs["position_action"], price))
        return order_id

    async def execute_sell(self,
                           order_id: str,
                           trading_pair: str,
                           amount: Decimal,
                           order_type: OrderType,
                           position_action: PositionAction,
                           price: Optional[Decimal] = s_decimal_NaN):
        return await self.create_order(TradeType.SELL, order_id, trading_pair, amount, order_type, position_action, price)

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

        t_pair: str = trading_pair
        order_id: str = get_client_order_id("sell", t_pair)
        safe_ensure_future(self.execute_sell(order_id, trading_pair, amount, order_type, kwargs["position_action"], price))
        return order_id

    async def cancel_all(self, timeout_seconds: float):
        incomplete_orders = [order for order in self._in_flight_orders.values() if not order.is_done]
        tasks = [self.execute_cancel(order.trading_pair, order.client_order_id) for order in incomplete_orders]
        order_id_set = set([order.client_order_id for order in incomplete_orders])
        successful_cancellations = []

        try:
            async with timeout(timeout_seconds):
                cancellation_results = await safe_gather(*tasks, return_exceptions=True)
                for cancel_result in cancellation_results:
                    # TODO: QUESTION --- SHOULD I CHECK FOR THE BinanceAPIException CONSIDERING WE ARE MOVING AWAY FROM BINANCE-CLIENT?
                    if isinstance(cancel_result, dict) and "clientOrderId" in cancel_result:
                        client_order_id = cancel_result.get("clientOrderId")
                        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 Perpetual. Check API key and network connection."
            )
        failed_cancellations = [CancellationResult(order_id, False) for order_id in order_id_set]
        return successful_cancellations + failed_cancellations

    async def cancel_all_account_orders(self, trading_pair: str):
        try:
            params = {
                "symbol": trading_pair
            }
            response = await self.request(
                path="/fapi/v1/allOpenOrders",
                params=params,
                method=MethodType.DELETE,
                add_timestamp=True,
                is_signed=True
            )
            if response.get("code") == 200:
                for order_id in list(self._in_flight_orders.keys()):
                    self.stop_tracking_order(order_id)
            else:
                raise IOError(f"Error cancelling all account orders. Server Response: {response}")
        except Exception as e:
            self.logger().error("Could not cancel all account orders.")
            raise e

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

    async def execute_cancel(self, trading_pair: str, client_order_id: str):
        try:
            params = {
                "origClientOrderId": client_order_id,
                "symbol": convert_to_exchange_trading_pair(trading_pair)
            }
            response = await self.request(
                path="/fapi/v1/order",
                params=params,
                method=MethodType.DELETE,
                is_signed=True,
                add_timestamp = True,
                return_err=True
            )
            if response.get("code") == -2011 or "Unknown order sent" in response.get("msg", ""):
                self.logger().debug(f"The order {client_order_id} does not exist on Binance Perpetuals. "
                                    f"No cancellation needed.")
                self.stop_tracking_order(client_order_id)
                self.trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG,
                                   OrderCancelledEvent(self.current_timestamp, client_order_id))
                return {
                    "origClientOrderId": client_order_id
                }
        except Exception as e:
            self.logger().error(f"Could not cancel order {client_order_id} (on Binance Perp. {trading_pair})")
            raise e
        if response.get("status", None) == "CANCELED":
            self.logger().info(f"Successfully canceled order {client_order_id}")
            self.stop_tracking_order(client_order_id)
            self.trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG,
                               OrderCancelledEvent(self.current_timestamp, client_order_id))
        return response

    # TODO: Implement
    async def close_position(self, trading_pair: str):
        pass

    def quantize_order_amount(self, trading_pair: str, amount: object, price: object = Decimal(0)):
        trading_rule: TradingRule = self._trading_rules[trading_pair]
        # current_price: object = self.get_price(trading_pair, False)
        notional_size: object
        quantized_amount = DerivativeBase.quantize_order_amount(self, trading_pair, amount)
        if quantized_amount < trading_rule.min_order_size:
            return Decimal(0)
        """
        if price == Decimal(0):
            notional_size = current_price * quantized_amount
        else:
            notional_size = price * quantized_amount
        """

        # TODO: NOTIONAL MIN SIZE DOES NOT EXIST
        # if notional_size < trading_rule.min_notional_size * Decimal("1.01"):
        #     return Decimal(0)

        return quantized_amount

    def get_order_price_quantum(self, trading_pair: str, price: object):
        trading_rule: TradingRule = self._trading_rules[trading_pair]
        return trading_rule.min_price_increment

    def get_order_size_quantum(self, trading_pair: str, order_size: object):
        trading_rule: TradingRule = self._trading_rules[trading_pair]
        return Decimal(trading_rule.min_base_amount_increment)

    # ORDER TRACKING ---
    def start_tracking_order(self, order_id: str, exchange_order_id: str, trading_pair: str, trading_type: object,
                             price: object, amount: object, order_type: object):
        self._in_flight_orders[order_id] = BinancePerpetualsInFlightOrder(
            client_order_id=order_id,
            exchange_order_id=exchange_order_id,
            trading_pair=trading_pair,
            order_type=order_type,
            trade_type=trading_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]
        if order_id in self._order_not_found_records:
            del self._order_not_found_records[order_id]

    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 _user_stream_event_listener(self):
        async for event_message in self._iter_user_event_queue():
            try:
                event_type = event_message.get("e")
                if event_type == "ORDER_TRADE_UPDATE":
                    order_message = event_message.get("o")
                    client_order_id = order_message.get("c")

                    # If the order has already been cancelled
                    if client_order_id not in self._in_flight_orders:
                        continue

                    tracked_order = self._in_flight_orders.get(client_order_id)
                    tracked_order.update_with_execution_report(event_message)

                    # Execution Type: Trade => Filled
                    trade_type = TradeType.BUY if order_message.get("S") == "BUY" else TradeType.SELL
                    if order_message.get("X") in ["PARTIALLY_FILLED", "FILLED"]:
                        order_filled_event = OrderFilledEvent(
                            timestamp=event_message.get("E") * 1e-3,
                            order_id=client_order_id,
                            trading_pair=convert_from_exchange_trading_pair(order_message.get("s")),
                            trade_type=trade_type,
                            order_type=OrderType.LIMIT if order_message.get("o") == "LIMIT" else OrderType.MARKET,
                            price=Decimal(order_message.get("L")),
                            amount=Decimal(order_message.get("l")),
                            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=trade_type,
                                amount=Decimal(order_message.get("q")),
                                price=Decimal(order_message.get("p"))
                            ),
                            exchange_trade_id=order_message.get("t")
                        )
                        self.trigger_event(self.MARKET_ORDER_FILLED_EVENT_TAG, order_filled_event)

                    if tracked_order.is_done:
                        if not tracked_order.is_failure:
                            event_tag = None
                            event_class = None
                            if trade_type is TradeType.BUY:
                                event_tag = self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG
                                event_class = BuyOrderCompletedEvent
                            else:
                                event_tag = self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG
                                event_class = SellOrderCompletedEvent
                            self.logger().info(f"The {tracked_order.order_type.name.lower()} {trade_type} order {client_order_id} has completed "
                                               f"according to websocket delta.")
                            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 or tracked_order.quote_asset),
                                                           tracked_order.executed_amount_base,
                                                           tracked_order.executed_amount_quote,
                                                           tracked_order.fee_paid,
                                                           tracked_order.order_type))
                        else:
                            if tracked_order.is_cancelled:
                                if tracked_order.client_order_id in self._in_flight_orders:
                                    self.logger().info(f"Successfully cancelled order {tracked_order.client_order_id} according to websocket delta.")
                                    self.trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG,
                                                       OrderCancelledEvent(self.current_timestamp,
                                                                           tracked_order.client_order_id))
                                else:
                                    self.logger().info(f"The {tracked_order.order_type.name.lower()} order {tracked_order.client_order_id} has failed "
                                                       f"according to websocket delta.")
                                    self.trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG,
                                                       MarketOrderFailureEvent(self.current_timestamp,
                                                                               tracked_order.client_order_id,
                                                                               tracked_order.order_type))
                        self.stop_tracking_order(tracked_order.client_order_id)
                elif event_type == "ACCOUNT_UPDATE":
                    update_data = event_message.get("a", {})
                    # update balances
                    for asset in update_data.get("B", []):
                        asset_name = asset["a"]
                        self._account_balances[asset_name] = Decimal(asset["wb"])
                        self._account_available_balances[asset_name] = Decimal(asset["cw"])

                    # update position
                    for asset in update_data.get("P", []):
                        position = self._account_positions.get(f"{asset['s']}{asset['ps']}", None)
                        if position is not None:
                            position.update_position(position_side=PositionSide[asset["ps"]],
                                                     unrealized_pnl = Decimal(asset["up"]),
                                                     entry_price = Decimal(asset["ep"]),
                                                     amount = Decimal(asset["pa"]))
                        else:
                            await self._update_positions()
                elif event_type == "MARGIN_CALL":
                    positions = event_message.get("p", [])
                    total_maint_margin_required = 0
                    # total_pnl = 0
                    negative_pnls_msg = ""
                    for position in positions:
                        existing_position = self._account_positions.get(f"{asset['s']}{asset['ps']}", None)
                        if existing_position is not None:
                            existing_position.update_position(position_side=PositionSide[asset["ps"]],
                                                              unrealized_pnl = Decimal(asset["up"]),
                                                              amount = Decimal(asset["pa"]))
                        total_maint_margin_required += position.get("mm", 0)
                        if position.get("up", 0) < 1:
                            negative_pnls_msg += f"{position.get('s')}: {position.get('up')}, "
                    self.logger().warning("Margin Call: Your position risk is too high, and you are at risk of "
                                          "liquidation. Close your positions or add additional margin to your wallet.")
                    self.logger().info(f"Margin Required: {total_maint_margin_required}. Total Unrealized PnL: "
                                       f"{negative_pnls_msg}. Negative PnL assets: {negative_pnls_msg}.")
            except asyncio.CancelledError:
                raise
            except Exception as e:
                self.logger().error(f"Unexpected error in user stream listener loop: {e}", exc_info=True)
                await asyncio.sleep(5.0)

    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 = self._last_timestamp / poll_interval
        current_tick = timestamp / poll_interval
        if current_tick > last_tick:
            if not self._poll_notifier.is_set():
                self._poll_notifier.set()
        self._last_timestamp = timestamp

    # MARKET AND ACCOUNT INFO ---
    def get_fee(self, base_currency: str, quote_currency: str, order_type: object, order_side: object,
                amount: object, price: object):
        is_maker = order_type is OrderType.LIMIT
        return estimate_fee("binance_perpetual", is_maker)

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

    async def _update_trading_rules(self):
        last_tick = self._last_timestamp / 60.0
        current_tick = self.current_timestamp / 60.0
        if current_tick > last_tick or len(self._trading_rules) < 1:
            exchange_info = await self.request(path="/fapi/v1/exchangeInfo", method=MethodType.GET, is_signed=False)
            trading_rules_list = 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

    def _format_trading_rules(self, exchange_info_dict: Dict[str, Any]) -> List[TradingRule]:
        rules: list = exchange_info_dict.get("symbols", [])
        return_val: list = []
        for rule in rules:
            try:
                trading_pair = convert_from_exchange_trading_pair(rule["symbol"])
                filters = rule["filters"]
                filt_dict = {fil["filterType"]: fil for fil in filters}

                min_order_size = Decimal(filt_dict.get("LOT_SIZE").get("minQty"))
                step_size = Decimal(filt_dict.get("LOT_SIZE").get("stepSize"))
                tick_size = Decimal(filt_dict.get("PRICE_FILTER").get("tickSize"))

                # TODO: BINANCE PERPETUALS DOES NOT HAVE A MIN NOTIONAL VALUE, NEED TO CREATE NEW DERIVATIVES INFRASTRUCTURE
                # min_notional = 0

                return_val.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 as e:
                self.logger().error(f"Error parsing the trading pair rule {rule}. Error: {e}. Skipping...",
                                    exc_info=True)
        return return_val

    async def _trading_rules_polling_loop(self):
        while True:
            try:
                await safe_gather(
                    self._update_trading_rules()

                    # TODO: Uncomment when implemented
                    # self._update_trade_fees()
                )
                await asyncio.sleep(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 Perpetuals. "
                                                      "Check network connection.")
                await asyncio.sleep(0.5)

    async def _status_polling_loop(self):
        while True:
            try:
                self._poll_notifier = asyncio.Event()
                await self._poll_notifier.wait()
                await safe_gather(
                    self._update_balances(),
                    self._update_positions(),
                    self._update_order_fills_from_trades(),
                    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 Perpetuals. "
                                                      "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.request(path="/fapi/v2/account", is_signed=True, add_timestamp=True)
        assets = account_info.get("assets")
        for asset in assets:
            asset_name = asset.get("asset")
            available_balance = Decimal(asset.get("availableBalance"))
            wallet_balance = Decimal(asset.get("walletBalance"))
            self._account_available_balances[asset_name] = available_balance
            self._account_balances[asset_name] = wallet_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]

    # TODO: Note --- Data Structure Assumes One-way Position Mode [not hedge position mode] (see Binance Futures Docs)
    # Note --- Hedge Mode allows for Both Long and Short Positions on a trading pair
    async def _update_positions(self):
        # local_position_names = set(self._account_positions.keys())
        # remote_position_names = set()
        positions = await self.request(path="/fapi/v2/positionRisk", add_timestamp=True, is_signed=True)
        for position in positions:
            trading_pair = position.get("symbol")
            position_side = PositionSide[position.get("positionSide")]
            unrealized_pnl = Decimal(position.get("unRealizedProfit"))
            entry_price = Decimal(position.get("entryPrice"))
            amount = Decimal(position.get("positionAmt"))
            leverage = Decimal(position.get("leverage"))
            if amount != 0:
                self._account_positions[trading_pair + position_side.name] = Position(
                    trading_pair=convert_from_exchange_trading_pair(trading_pair),
                    position_side=position_side,
                    unrealized_pnl=unrealized_pnl,
                    entry_price=entry_price,
                    amount=amount,
                    leverage=leverage
                )
            else:
                if (trading_pair + position_side.name) in self._account_positions:
                    del self._account_positions[trading_pair + position_side.name]

    async def _update_order_fills_from_trades(self):
        last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL
        current_tick = self.current_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL
        if current_tick > last_tick and len(self._in_flight_orders) > 0:
            trading_pairs_to_order_map = defaultdict(lambda: {})
            for order in self._in_flight_orders.values():
                trading_pairs_to_order_map[order.trading_pair][order.exchange_order_id] = order
            trading_pairs = list(trading_pairs_to_order_map.keys())
            tasks = [
                self.request(
                    path="/fapi/v1/userTrades",
                    params={
                        "symbol": convert_to_exchange_trading_pair(trading_pair)
                    },
                    is_signed=True,
                    add_timestamp=True
                ) for trading_pair in trading_pairs]
            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):
                order_map = trading_pairs_to_order_map.get(trading_pair)
                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:
                    order_id = str(trade.get("orderId"))
                    if order_id in order_map:
                        tracked_order = order_map.get(order_id)
                        order_type = tracked_order.order_type
                        applied_trade = tracked_order.update_with_trade_updates(trade)
                        if applied_trade:
                            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,
                                    order_type,
                                    Decimal(trade.get("price")),
                                    Decimal(trade.get("qty")),
                                    self.get_fee(
                                        tracked_order.base_asset,
                                        tracked_order.quote_asset,
                                        order_type,
                                        tracked_order.trade_type,
                                        Decimal(trade["price"]),
                                        Decimal(trade["qty"])),
                                    exchange_trade_id=trade["id"]
                                )
                            )

    async def _update_order_status(self):
        last_tick = self._last_poll_timestamp / self.UPDATE_ORDER_STATUS_MIN_INTERVAL
        current_tick = 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 = [self.request(path="/fapi/v1/order",
                                  params={
                                      "symbol": convert_to_exchange_trading_pair(order.trading_pair),
                                      "origClientOrderId": order.client_order_id
                                  },
                                  method=MethodType.GET,
                                  add_timestamp=True,
                                  is_signed=True,
                                  return_err=True)
                     for order 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 client_order_id not in self._in_flight_orders:
                    continue
                if isinstance(order_update, Exception):
                    # NO_SUCH_ORDER code
                    if order_update["code"] == -2013 or order_update["msg"] == "Order does not exist.":
                        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:
                            continue
                        self.trigger_event(
                            self.MARKET_ORDER_FAILURE_EVENT_TAG,
                            MarketOrderFailureEvent(self.current_timestamp, client_order_id, tracked_order.order_type)
                        )
                        self.stop_tracking_order(client_order_id)
                    else:
                        self.logger().network(f"Error fetching status update for the order {client_order_id}: "
                                              f"{order_update}.")
                    continue

                tracked_order.last_state = order_update.get("status")
                order_type = OrderType.LIMIT if order_update.get("type") == "LIMIT" else OrderType.MARKET
                executed_amount_base = Decimal(order_update.get("executedQty", "0"))
                executed_amount_quote = Decimal(order_update.get("cumQuote", "0"))

                if tracked_order.is_done:
                    if not tracked_order.is_failure:
                        event_tag = None
                        event_class = None
                        if tracked_order.trade_type is TradeType.BUY:
                            event_tag = self.MARKET_BUY_ORDER_COMPLETED_EVENT_TAG
                            event_class = BuyOrderCompletedEvent
                        else:

                            event_tag = self.MARKET_SELL_ORDER_COMPLETED_EVENT_TAG
                            event_class = SellOrderCompletedEvent
                        self.logger().info(f"The {order_type.name.lower()} {tracked_order.trade_type.name.lower()} order {client_order_id} has "
                                           f"completed according to order status API.")
                        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 or tracked_order.base_asset),
                                                       executed_amount_base,
                                                       executed_amount_quote,
                                                       tracked_order.fee_paid,
                                                       order_type))
                    else:
                        if tracked_order.is_cancelled:
                            self.logger().info(f"Successfully cancelled order {client_order_id} according to order status API.")
                            self.trigger_event(self.MARKET_ORDER_CANCELLED_EVENT_TAG,
                                               OrderCancelledEvent(self.current_timestamp,
                                                                   client_order_id))
                        else:
                            self.logger().info(f"The {order_type.name.lower()} order {client_order_id} has failed according to "
                                               f"order status API.")
                            self.trigger_event(self.MARKET_ORDER_FAILURE_EVENT_TAG,
                                               MarketOrderFailureEvent(self.current_timestamp,
                                                                       client_order_id,
                                                                       order_type))
                    self.stop_tracking_order(client_order_id)

    async def _set_margin(self, trading_pair: str, leverage: int = 1):
        params = {
            "symbol": convert_to_exchange_trading_pair(trading_pair),
            "leverage": leverage
        }
        set_leverage = await self.request(
            path="/fapi/v1/leverage",
            params=params,
            method=MethodType.POST,
            add_timestamp=True,
            is_signed=True
        )
        if set_leverage["leverage"] == leverage:
            self.logger().info(f"Leverage Successfully set to {leverage}.")
        else:
            self.logger().error("Unable to set leverage.")
        return leverage

    def set_margin(self, trading_pair: str, leverage: int = 1):
        safe_ensure_future(self._set_margin(trading_pair, leverage))

    """
    async def get_position_pnl(self, trading_pair: str):
        await self._update_positions()
        return self._account_positions.get(trading_pair)
    """

    async def _get_funding_rate(self, trading_pair):
        # TODO: Note --- the "premiumIndex" endpoint can get markPrice, indexPrice, and nextFundingTime as well
        prem_index = await self.request("/fapi/v1/premiumIndex", params={"symbol": convert_to_exchange_trading_pair(trading_pair)})
        self._funding_rate = Decimal(prem_index.get("lastFundingRate", "0"))

    def get_funding_rate(self, trading_pair):
        safe_ensure_future(self._get_funding_rate(trading_pair))
        return self._funding_rate

    async def _set_position_mode(self, position_mode: PositionMode):
        initial_mode = await self._get_position_mode()
        if initial_mode != position_mode:
            params = {
                "dualSidePosition": position_mode.value
            }
            mode = await self.request(
                path="/fapi/v1/positionSide/dual",
                params=params,
                method=MethodType.POST,
                add_timestamp=True,
                is_signed=True
            )
            if mode["msg"] == "success" and mode["code"] == 200:
                self.logger().info(f"Using {position_mode.name} position mode.")
            else:
                self.logger().error(f"Unable to set postion mode to {position_mode.name}.")
        else:
            self.logger().info(f"Using {position_mode.name} position mode.")

    async def _get_position_mode(self):
        if self._position_mode is None:
            mode = await self.request(
                path="/fapi/v1/positionSide/dual",
                method=MethodType.GET,
                add_timestamp=True,
                is_signed=True
            )
            self._position_mode = PositionMode.HEDGE if mode["dualSidePosition"] else PositionMode.ONEWAY

        return self._position_mode

    def set_position_mode(self, position_mode: PositionMode):
        safe_ensure_future(self._set_position_mode(position_mode))

    async def request(self, path: str, params: Dict[str, Any] = {}, method: MethodType = MethodType.GET,
                      add_timestamp: bool = False, is_signed: bool = False, request_weight: int = 1, return_err: bool = False):
        async with self._throttler.weighted_task(request_weight):
            try:
                # TODO: QUESTION --- SHOULD I ADD AN ASYNC TIMEOUT? (aync with timeout(API_CALL_TIMEOUT)
                # async with aiohttp.ClientSession() as client:
                if add_timestamp:
                    params["timestamp"] = f"{int(time.time()) * 1000}"
                    params["recvWindow"] = f"{20000}"
                query = urlencode(sorted(params.items()))
                if is_signed:
                    secret = bytes(self._api_secret.encode("utf-8"))
                    signature = hmac.new(secret, query.encode("utf-8"), hashlib.sha256).hexdigest()
                    query += f"&signature={signature}"

                async with aiohttp.request(
                        method=method.value,
                        url=self._base_url + path + "?" + query,
                        headers={"X-MBX-APIKEY": self._api_key}) as response:
                    if response.status != 200:
                        error_response = await response.json()
                        if return_err:
                            return error_response
                        else:
                            raise IOError(f"Error fetching data from {path}. HTTP status is {response.status}. "
                                          f"Request Error: {error_response}")
                    return await response.json()
            except Exception as e:
                self.logger().error(f"Error fetching {path}", exc_info=True)
                self.logger().warning(f"{e}")
                raise e
コード例 #6
0
class CoinzoomExchange(ExchangeBase):
    """
    CoinzoomExchange connects with CoinZoom 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,
                 coinzoom_api_key: str,
                 coinzoom_secret_key: str,
                 coinzoom_username: str,
                 trading_pairs: Optional[List[str]] = None,
                 trading_required: bool = True
                 ):
        """
        :param coinzoom_api_key: The API key to connect to private CoinZoom APIs.
        :param coinzoom_secret_key: The API secret.
        :param coinzoom_username: The ZoomMe Username.
        :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._coinzoom_auth = CoinzoomAuth(coinzoom_api_key, coinzoom_secret_key, coinzoom_username)
        self._order_book_tracker = CoinzoomOrderBookTracker(trading_pairs=trading_pairs)
        self._user_stream_tracker = CoinzoomUserStreamTracker(self._coinzoom_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, CoinzoomInFlightOrder]
        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 = Throttler(rate_limit = (8.0, 6))

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

    @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, CoinzoomInFlightOrder]:
        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: CoinzoomInFlightOrder.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-USD symbol
            await self._api_request("GET", Constants.ENDPOINT['SYMBOL'])
        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.ENDPOINT['SYMBOL'])
        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:
        [
            {
                "symbol" : "BTC/USD",
                "baseCurrencyCode" : "BTC",
                "termCurrencyCode" : "USD",
                "minTradeAmt" : 0.0001,
                "maxTradeAmt" : 10,
                "maxPricePrecision" : 2,
                "maxQuantityPrecision" : 6,
                "issueOnly" : false
            }
        ]
        """
        result = {}
        for rule in symbols_info:
            try:
                trading_pair = convert_from_exchange_trading_pair(rule["symbol"])
                min_amount = Decimal(str(rule["minTradeAmt"]))
                min_price = Decimal(f"1e-{rule['maxPricePrecision']}")
                result[trading_pair] = TradingRule(trading_pair,
                                                   min_order_size=min_amount,
                                                   max_order_size=Decimal(str(rule["maxTradeAmt"])),
                                                   min_price_increment=min_price,
                                                   min_base_amount_increment=min_amount,
                                                   min_notional_size=min(min_price * min_amount, Decimal("0.00000001")),
                                                   max_price_significant_digits=Decimal(str(rule["maxPricePrecision"])),
                                                   )
            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) -> 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.
        """
        async with self._throttler.weighted_task(request_weight=1):
            url = f"{Constants.REST_URL}/{endpoint}"
            shared_client = await self._http_client()
            # Turn `params` into either GET params or POST body data
            qs_params: dict = params if method.upper() == "GET" 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", "User-Agent": "hummingbot"}
            if is_auth_required:
                headers: dict = self._coinzoom_auth.get_headers()
            # 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)
                else:
                    raise CoinzoomAPIError({"error": parsed_response, "status": http_status})
            if "error" in parsed_response:
                raise CoinzoomAPIError(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 = 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 = 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}.")
        order_type_str = order_type.name.upper().split("_")[0]
        api_params = {"symbol": convert_to_exchange_trading_pair(trading_pair),
                      "orderType": order_type_str,
                      "orderSide": trade_type.name.upper(),
                      "quantity": f"{amount:f}",
                      "price": f"{price:f}",
                      "originType": Constants.HBOT_BROKER_ID,
                      # CoinZoom doesn't support client order id yet
                      # "clientOrderId": order_id,
                      "payFeesWithZoomToken": "true",
                      }
        self.start_tracking_order(order_id, None, trading_pair, trade_type, price, amount, order_type)
        try:
            order_result = await self._api_request("POST", Constants.ENDPOINT["ORDER_CREATE"], api_params, True)
            exchange_order_id = str(order_result)
            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))
        except asyncio.CancelledError:
            raise
        except CoinzoomAPIError as e:
            error_reason = e.error_payload.get('error', {}).get('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] = CoinzoomInFlightOrder(
            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 CoinZoom)
        :param order_id: The internal order id
        order.last_state to change to CANCELED
        """
        order_was_cancelled = False
        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 = {
                "orderId": ex_order_id,
                "symbol": convert_to_exchange_trading_pair(trading_pair)
            }
            await self._api_request("POST",
                                    Constants.ENDPOINT["ORDER_DELETE"],
                                    api_params,
                                    is_auth_required=True)
            order_was_cancelled = True
        except asyncio.CancelledError:
            raise
        except CoinzoomAPIError as e:
            err = e.error_payload.get('error', e.error_payload)
            self.logger().error(f"Order Cancel API Error: {err}")
            # CoinZoom doesn't report any error if the order wasn't found so we can only handle API failures here.
            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_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:
            self.logger().network(
                f"Failed to cancel order {order_id}: {err.get('message', str(err))}",
                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:
                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)
                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)

    async def _update_balances(self):
        """
        Calls REST API to update total and available balances.
        """
        account_info = await self._api_request("GET", Constants.ENDPOINT["USER_BALANCES"], is_auth_required=True)
        self._process_balance_message(account_info)

    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 = 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())
            api_params = {
                'symbol': None,
                'orderSide': None,
                'orderStatuses': ["NEW", "PARTIALLY_FILLED"],
                'size': 500,
                'bookmarkOrderId': None
            }
            self.logger().debug(f"Polling for order status updates of {len(tracked_orders)} orders.")
            open_orders = await self._api_request("POST",
                                                  Constants.ENDPOINT["ORDER_STATUS"],
                                                  api_params,
                                                  is_auth_required=True)

            open_orders_dict = {o['id']: o for o in open_orders}
            found_ex_order_ids = list(open_orders_dict.keys())

            for tracked_order in tracked_orders:
                client_order_id = tracked_order.client_order_id
                ex_order_id = tracked_order.exchange_order_id
                if ex_order_id not in found_ex_order_ids:
                    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 is not found a few times before actually treating it as failed.
                        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:
                    self._process_order_message(open_orders_dict[ex_order_id])

    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 Orders:
            REST request
            {
                "id" : "977f82aa-23dc-4c8b-982c-2ee7d2002882",
                "clientOrderId" : null,
                "symbol" : "BTC/USD",
                "orderType" : "LIMIT",
                "orderSide" : "BUY",
                "quantity" : 0.1,
                "price" : 54570,
                "payFeesWithZoomToken" : false,
                "orderStatus" : "PARTIALLY_FILLED",
                "timestamp" : "2021-03-24T04:07:26.260253Z",
                "executions" :
                [
                    {
                        "id" : "38761582-2b37-4e27-a561-434981d21a96",
                        "executionType" : "PARTIAL_FILL",
                        "orderStatus" : "PARTIALLY_FILLED",
                        "lastPrice" : 54570,
                        "averagePrice" : 54570,
                        "lastQuantity" : 0.01,
                        "leavesQuantity" : 0.09,
                        "cumulativeQuantity" : 0.01,
                        "rejectReason" : null,
                        "timestamp" : "2021-03-24T04:07:44.503222Z"
                    }
                ]
            }
            WS request
            {
                'orderId': '962a2a54-fbcf-4d89-8f37-a8854020a823',
                'symbol': 'BTC/USD', 'orderType': 'LIMIT',
                'orderSide': 'BUY',
                'price': 5000,
                'quantity': 0.001,
                'executionType': 'CANCEL',
                'orderStatus': 'CANCELLED',
                'lastQuantity': 0,
                'leavesQuantity': 0,
                'cumulativeQuantity': 0,
                'transactTime': '2021-03-23T19:06:51.155520Z'

                ... Optional fields

                'id': '4eb3f26c-91bd-4bd2-bacb-15b2f432c452',
                "orderType": "LIMIT",
                "lastPrice": 56518.7,
                "averagePrice": 56518.7,
            }
        """
        # Looks like CoinZoom might support clientOrderId eventually so leaving this here for now.
        # if order_msg.get('clientOrderId') is not None:
        #     client_order_id = order_msg["clientOrderId"]
        #     if client_order_id not in self._in_flight_orders:
        #         return
        #     tracked_order = self._in_flight_orders[client_order_id]
        # else:
        if "orderId" not in order_msg:
            exchange_order_id = str(order_msg["id"])
        else:
            exchange_order_id = str(order_msg["orderId"])
        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 not track_order:
            return
        tracked_order = track_order[0]

        # Estimate fee
        order_msg["trade_fee"] = self.estimate_fee_pct(tracked_order.order_type is OrderType.LIMIT_MAKER)
        updated = tracked_order.update_with_order_update(order_msg)
        # Call Update balances on every message to catch order create, fill and cancel.
        safe_ensure_future(self._update_balances())

        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 _trigger_order_fill(self,
                                  tracked_order: CoinzoomInFlightOrder,
                                  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("averagePrice", update_msg.get("price", "0")))),
                tracked_order.executed_amount_base,
                TradeFee(percent=update_msg["trade_fee"]),
                update_msg.get("exchange_trade_id", update_msg.get("id", update_msg.get("orderId")))
            )
        )
        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))
            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"]
            total_bal = Decimal(str(account["totalBalance"]))
            self._account_available_balances[asset_name] = total_bal + Decimal(str(account["reservedBalance"]))
            self._account_balances[asset_name] = total_bal
            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 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 = []
        try:
            async with timeout(timeout_seconds):
                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()
        poll_interval = (Constants.SHORT_POLL_INTERVAL
                         if now - self._user_stream_tracker.last_recv_time > 60.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
        CoinzoomAPIUserStreamDataSource.
        """
        async for event_message in self._iter_user_event_queue():
            try:
                event_methods = [
                    Constants.WS_METHODS["USER_ORDERS"],
                    Constants.WS_METHODS["USER_ORDERS_CANCEL"],
                ]

                msg_keys = list(event_message.keys()) if event_message is not None else []

                method_key = [key for key in msg_keys if key in event_methods]

                if len(method_key) != 1:
                    continue

                method: str = method_key[0]

                if method == Constants.WS_METHODS["USER_ORDERS"]:
                    self._process_order_message(event_message[method])
                elif method == Constants.WS_METHODS["USER_ORDERS_CANCEL"]:
                    self._process_order_message(event_message[method])
            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]:
        tracked_orders = list(self._in_flight_orders.values())
        api_params = {
            'symbol': None,
            'orderSide': None,
            'orderStatuses': ["NEW", "PARTIALLY_FILLED"],
            'size': 500,
            'bookmarkOrderId': None
        }
        result = await self._api_request("POST", Constants.ENDPOINT["USER_ORDERS"], api_params, is_auth_required=True)
        ret_val = []
        for order in result:
            exchange_order_id = str(order["id"])
            # CoinZoom doesn't support client order ids yet so we must find it from the tracked orders.
            track_order = [o for o in tracked_orders if exchange_order_id == o.exchange_order_id]
            if not track_order or len(track_order) < 1:
                # Skip untracked orders
                continue
            client_order_id = track_order[0].client_order_id
            # if Constants.HBOT_BROKER_ID not in order["clientOrderId"]:
            #     continue
            if order["orderType"] != OrderType.LIMIT.name.upper():
                self.logger().info(f"Unsupported order type found: {order['type']}")
                # Skip and report non-limit orders
                continue
            ret_val.append(
                OpenOrder(
                    client_order_id=client_order_id,
                    trading_pair=convert_from_exchange_trading_pair(order["symbol"]),
                    price=Decimal(str(order["price"])),
                    amount=Decimal(str(order["quantity"])),
                    executed_amount=Decimal(str(order["cumQuantity"])),
                    status=order["orderStatus"],
                    order_type=OrderType.LIMIT,
                    is_buy=True if order["orderSide"].lower() == TradeType.BUY.name.lower() else False,
                    time=str_date_to_ts(order["timestamp"]),
                    exchange_order_id=order["id"]
                )
            )
        return ret_val
コード例 #7
0
 def setUp(self):
     self.throttler = Throttler(rate_limit=(20, 1),
                                period_safety_margin=0,
                                retry_interval=0)
     self.loop = asyncio.get_event_loop()
コード例 #8
0
ファイル: gate_io_utils.py プロジェクト: zanglang/hummingbot
    Tuple,
)

from hummingbot.core.utils.asyncio_throttle import Throttler
from hummingbot.core.utils.tracking_nonce import get_tracking_nonce
from hummingbot.client.config.config_var import ConfigVar
from hummingbot.client.config.config_methods import using_exchange
from .gate_io_constants import Constants

CENTRALIZED = True

EXAMPLE_PAIR = "BTC-USDT"

DEFAULT_FEES = [0.2, 0.2]

REQUEST_THROTTLER = Throttler(rate_limit=(18.0, 8.0))


class GateIoAPIError(IOError):
    def __init__(self, error_payload: Dict[str, Any]):
        super().__init__(str(error_payload))
        self.error_payload = error_payload
        self.http_status = error_payload.get('status')
        if isinstance(error_payload, dict):
            self.error_message = error_payload.get('error', error_payload).get(
                'message', error_payload)
            self.error_label = error_payload.get('error', error_payload).get(
                'label', error_payload)
        else:
            self.error_message = error_payload
            self.error_label = error_payload