# ruff: noqa: D200 # ruff: noqa: D204 # ruff: noqa: D205 # ruff: noqa: D404 # ruff: noqa: W291 # ruff: noqa: D400 # ruff: noqa: E501 import logging import time from datetime import datetime from decimal import Decimal from typing import Any, get_args from .grvt_ccxt_env import GrvtEnv from .grvt_ccxt_types import ( CandlestickInterval, CandlestickType, GrvtInvalidOrder, GrvtOrderSide, GrvtOrderType, Num, ccxt_interval_to_grvt_candlestick_interval, ) from .grvt_ccxt_utils import get_kuq_from_symbol, sign_derisk_mm_ratio_request # COOKIE_REFRESH_INTERVAL_SECS = 60 * 60 # 30 minutes class GrvtCcxtBase: """ GrvtCcxtBase is an abstract class for other Grvt Rest and WebSocket connectivity classes. Args: env: GrvtCcxtBase (DEV, TESTNET, PROD) logger (logging.Logger, optional). Defaults to None. parameters: (dict, optional). Dict with trading_account_id, private_key, api_key etc defaults to empty. """ def __init__( self, env: GrvtEnv, logger: logging.Logger | None = None, parameters: dict = {}, order_book_ccxt_format: bool = False, ): """Initialize the GrvtCcxtBase part.""" self.name: str = "GRVT" self.logger = logger or logging.getLogger(__name__) self.env: GrvtEnv = env self._trading_account_id: str | None = parameters.get("trading_account_id") self._private_key: str = str(parameters.get("private_key", "")) self._api_key: str = str(parameters.get("api_key", "")) self._order_book_ccxt_format: bool = order_book_ccxt_format self._path_return_value_map: dict = {} self._cookie: dict | None = None self.markets: dict = {} self._clsname: str = type(self).__name__ self.logger.info(f"GrvtCcxtBase: {self.env=}, {self._trading_account_id=}") def describe(self) -> list[str]: """Returns the description of the class methods.""" return [ "create_order", "create_limit_order", "cancel_all_orders", "cancel_order", "fetch_balance", "fetch_open_orders", "fetch_order", "fetch_order_history", "get_account_summary", "fetch_account_history", "fetch_positions", "fetch_my_trades", "load_markets", "fetch_markets", "fetch_all_markets", "fetch_market", "fetch_ticker", "fetch_mini_ticker", "fetch_order_book", "fetch_recent_trades", "fetch_trades", "fetch_funding_rate_history", "fetch_ohlcv", ] def get_trading_account_id(self) -> str: """Returns the trading account id.""" return self._trading_account_id or "" def is_order_book_ccxt_format(self) -> bool: """Returns True if order book should be returned in CCXT format.""" return self._order_book_ccxt_format def should_refresh_cookie(self) -> bool: """ Retuns: True if this object has API key and the session cookie should be refreshed. False - otherwise. """ if not self._api_key: return False time_till_expiration = None if self._cookie and "expires" in self._cookie: time_till_expiration = self._cookie["expires"] - time.time() is_cookie_fresh = time_till_expiration is not None and time_till_expiration > 5 if not is_cookie_fresh: self.logger.info( f"cookie should be refreshed {self._cookie=} now={time.time()}" f" {time_till_expiration=} secs" ) return not is_cookie_fresh def get_path_return_value_map(self) -> dict: """Returns the path return value map.""" return self._path_return_value_map def get_endpoint_return_value(self, endpoint: str) -> dict: """Returns the return value for the endpoint.""" return self._path_return_value_map.get(endpoint, {}) def was_path_called(self, path: str) -> bool: """Returns True if the path was called.""" return path in self._path_return_value_map # PRIVATE API CALLS def _check_order_arguments( self, order_type: GrvtOrderType, side: GrvtOrderSide, amount: Num, price: Num ) -> None: FN = f"{self._clsname} _check_order_arguments" if order_type not in get_args(GrvtOrderType): raise GrvtInvalidOrder(f"{FN}: order_type should be one of {get_args(GrvtOrderType)}") if side not in get_args(GrvtOrderSide): raise GrvtInvalidOrder(f"{FN}: side should be one of {get_args(GrvtOrderSide)}") if order_type == "limit": if price is None or Decimal(price) <= Decimal("0"): raise GrvtInvalidOrder(f"{FN}: requires a price argument for a limit order") elif order_type == "market": if price: raise GrvtInvalidOrder( f"{FN}: should not have a positive price argument for a market order" ) if not amount or Decimal(amount) < Decimal("0"): raise GrvtInvalidOrder(f"{FN}: amount should be above 0") def _check_account_auth(self) -> bool: if not self.get_trading_account_id(): raise GrvtInvalidOrder(f"{self._clsname}: this action requires a trading_account_id") return True def _check_valid_symbol(self, symbol: str) -> bool: if not self.markets: raise GrvtInvalidOrder(f"{self._clsname}: markets not loaded") market = self.markets.get(symbol) if not market: raise GrvtInvalidOrder(f"{self._clsname}: {symbol=} not found") return True def _get_payload_cancel_all_orders( self, params: dict = {}, ) -> dict: """ Prepares payload for fetch_order_history() method.
. Args: params: (dict) with possible keys as:.
`kind`: (str) - The kind filter to apply. Defaults to all kinds.
`base`: (str) - The base currency filter. Defaults to all base currencies.
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.
Returns: a dictionary with a payload for Rest API call to cancel all orders.
""" payload: dict[str, str | int | bool | list] = { "sub_account_id": str(self.get_trading_account_id()) } if "kind" in params: payload["kind"] = [params["kind"]] if "base" in params: payload["base"] = [params["base"]] if "quote" in params: payload["quote"] = [params["quote"]] return payload def _get_payload_fetch_markets(self, params: dict) -> dict: payload: dict[str, str | int | bool | list] = {} if params.get("kind"): payload["kind"] = [params.get("kind")] if params.get("base"): payload["base"] = [params.get("base")] if params.get("quote"): payload["quote"] = [params.get("quote")] payload["limit"] = int(params.get("limit", 1_000)) payload["is_active"] = bool(params.get("is_active", True)) return payload def _get_payload_fetch_my_trades( self, symbol: str | None = None, since: int | None = None, limit: int | None = None, params: dict = {}, ) -> dict: """ Prepares payload for fetch_my_trades() method.
. Args: symbol: get trades for this symbol only.
since: fetch trades since this timestamp in nanoseconds.
limit: maximum number of trades to fetch.
params: dictionary with parameters. Valid keys:
`cursor` (str): cursor for the pagination. If cursor is present then we ignore other filters.
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.
`base` (str): base currency. If missing/empty then fetch orders for all base currencies.
`quote` (str): quote currency. Default: 'USDT'.
`end_time` (int): fetch trades until this timestamp in nanoseconds.
Returns: a dictionary with a payload for Rest API call to fetch trades.
""" payload: dict[str, str | int | list] = { "sub_account_id": str(self.get_trading_account_id()) } if params.get("cursor"): payload["cursor"] = params["cursor"] else: if symbol: payload["instrument"] = symbol else: if "kind" in params: payload["kind"] = [params["kind"]] if "base" in params: payload["base"] = [params["base"]] if "quote" in params: payload["quote"] = [params["quote"]] if since: payload["start_time"] = str(since) if params.get("end_time"): payload["end_time"] = str(params["end_time"]) if limit: payload["limit"] = int(limit) return payload def _get_payload_fetch_trades( self, symbol: str, since: int | None = None, limit: int = 1_000, params: dict = {}, ) -> dict: """ Prepares payload for fetch_trades() method.
. Args: symbol: get trades for this symbol only.
since: fetch trades since this timestamp in nanoseconds.
limit: maximum number of trades to fetch.
params: dictionary with parameters. Valid keys:
`cursor` (str): cursor for the pagination. If cursor is present then we ignore other filters.
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.
`base` (str): base currency. If missing/empty then fetch orders for all base currencies.
`quote` (str): quote currency. Default: 'USDT'.
Returns: a dictionary with a payload for Rest API call to fetch trades.
""" payload: dict[str, str | int] = { "sub_account_id": str(self.get_trading_account_id()), "instrument": symbol, } if params.get("cursor"): payload["cursor"] = params["cursor"] else: if since: payload["start_time"] = str(since) if params.get("end_time"): payload["end_time"] = str(params["end_time"]) payload["limit"] = limit return payload def _get_payload_fetch_account_history( self, # since: int | None = None, limit: int = 500, params: dict = {}, ) -> dict: """ Prepares payload for fetch_account_history() method.
. Args: limit: maximum number of trades to fetch.
params: dictionary with parameters. Valid keys:
`start_time` (int): fetch orders since this timestamp in nanoseconds.
`end_time` (int): fetch orders until this timestamp in nanoseconds.
`cursor` (int):cursor for the pagination. If cursor is present then we ignore `start_time` and `end_time`.
Returns: a dictionary with a payload for Rest API call to fetch account history.
""" payload: dict[str, str | int] = {"sub_account_id": str(self.get_trading_account_id())} if params.get("cursor"): payload["cursor"] = params["cursor"] else: start_time = params.get("start_time") end_time = params.get("end_time") if start_time: payload["start_time"] = str(start_time) if end_time: payload["end_time"] = str(end_time) payload["limit"] = limit | 500 return payload def _get_payload_fetch_positions(self, symbols: list[str] = [], params={}) -> dict: """ Prepares payload for fetch_positions() method.
. Args: symbols: list(str) get positions for these symbols only.
Returns: a dictionary with a payload for Rest API call to fetch positions.
""" payload: dict[str, str | int | bool | list] = { "sub_account_id": str(self.get_trading_account_id()) } if symbols: ks, us, qs = [], [], [] for symbol in symbols: try: k, u, q = get_kuq_from_symbol(symbol) ks.append(k) us.append(u) qs.append(q) except Exception as e: raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_positions {e}") payload["kind"] = list(set(ks)) payload["base"] = list(set(us)) payload["quote"] = list(set(qs)) else: if "kind" in params: payload["kind"] = [params["kind"]] if "base" in params: payload["base"] = [params["base"]] if "quote" in params: payload["quote"] = [params["quote"]] return payload def _get_payload_fetch_order_history( self, params: dict, ) -> dict: """ Prepares payload for fetch_order_history() method.
. Args: params: (dict) with possible keys as:.
`kind`: (str) - The kind filter to apply. Defaults to all kinds.
`base`: (str) - The base currency filter. Defaults to all base currencies.
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.
Returns: a dictionary with a payload for Rest API call to fetch order history.
""" payload: dict[str, str | int | bool | list] = { "sub_account_id": str(self.get_trading_account_id()) } if "limit" in params: payload["limit"] = params["limit"] if params.get("cursor"): payload["cursor"] = params["cursor"] else: if "kind" in params: payload["kind"] = [params["kind"]] if "base" in params: payload["base"] = [params["base"]] if "quote" in params: payload["quote"] = [params["quote"]] if "expiration" in params: payload["expiration"] = [params["expiration"]] if "strike_price" in params: payload["strike_price"] = [params["strike_price"]] return payload def _get_payload_fetch_open_orders( self, symbol: str | None = None, params: dict = {}, ) -> dict: """ Prepares payload for fetch_order_history() method.
. Args: params: (dict) with possible keys as:.
`kind`: (str) - The kind filter to apply. Defaults to all kinds.
`base`: (str) - The base currency filter. Defaults to all base currencies.
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.
Returns: a dictionary with a payload for Rest API call to fetch order history.
""" payload: dict[str, str | int | bool | list] = { "sub_account_id": str(self.get_trading_account_id()) } if symbol: try: k, u, q = get_kuq_from_symbol(symbol) payload["kind"] = [k] payload["base"] = [u] payload["quote"] = [q] except Exception as e: raise GrvtInvalidOrder(f"Invalid symbol {symbol} in fetch_open_orders {e}") else: if "kind" in params: payload["kind"] = [params["kind"]] if "base" in params: payload["base"] = [params["base"]] if "quote" in params: payload["quote"] = [params["quote"]] return payload def _get_payload_fetch_ohlcv( self, symbol: str, timeframe: str, since: int, limit: int, params={}, ) -> dict: """ Prepares payload for fetch_ohlcv() method.
. Args: symbol: The instrument name.
timeframe: The timeframe of the ohlc. See `ccxt_interval_to_grvt_candlestick_interval`.
since: fetch ohlc since this timestamp in nanoseconds.
limit: maximum number of ohlc to fetch.
params: dictionary with parameters. Valid keys:
`cursor` (str): cursor for the pagination. If cursor is present then we ignore other filters.
`end_time` (int): end time in nanoseconds.
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.
Returns: a dictionary with a payload for Rest API call to fetch_ohlcv.
See [Candlestick] (https://api-docs.grvt.io/market_data_api/#candlestick_1) for more details.
""" if timeframe not in ccxt_interval_to_grvt_candlestick_interval: raise ValueError(f"Invalid timeframe {timeframe}") interval: CandlestickInterval = ccxt_interval_to_grvt_candlestick_interval[timeframe] payload: dict[str, str | int | bool | list] = {"instrument": symbol} if params.get("cursor"): payload["cursor"] = params["cursor"] else: if interval: payload["interval"] = interval.value candle_type = CandlestickType.TRADE if "candle_type" in params: candle_type = CandlestickType[params["candle_type"]] payload["type"] = candle_type.value if since: payload["start_time"] = str(since) if "end_time" in params: payload["end_time"] = str(params["end_time"]) if limit: payload["limit"] = int(limit) return payload def _get_balances_from_account_summary(self, account_summary: dict) -> dict: balances: dict = {} balances["info"] = account_summary.get("spot_balances", []) balances["timestamp"] = int(int(account_summary.get("event_time", 0)) / 1_000_000) balances["datetime"] = ( datetime.fromtimestamp(balances["timestamp"] / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[ :-3 ] + "Z" ) balances["total"] = {} balances["free"] = {} balances["used"] = {} for currency_balance in account_summary.get("spot_balances", []): if not currency_balance or not isinstance(currency_balance, dict): continue currency: str = currency_balance.get("currency", "") if not currency: continue balances[currency] = {"total": currency_balance.get("balance", "0.0")} balances["total"][currency] = balances[currency]["total"] if currency == "USDT": balances[currency]["free"] = account_summary.get("available_balance", "0.0") balances[currency]["used"] = str( Decimal(balances[currency]["total"]) - Decimal(balances[currency]["free"]) ) else: balances[currency]["free"] = balances[currency]["total"] balances[currency]["used"] = "0.0" balances["free"][currency] = balances[currency]["free"] balances["used"][currency] = balances[currency]["used"] return balances def _get_set_derisk_mm_ratio_payload( self, ratio: str, ) -> dict[str, str | dict]: """ Returns a payload for setting the derisking market making ratio. """ payload: dict[str, str | dict] = { "sub_account_id": self.get_trading_account_id(), "ratio": str(ratio), } signature: dict = sign_derisk_mm_ratio_request( self.env, int(self.get_trading_account_id()), str(ratio), self._private_key ) payload["signature"] = signature return payload def convert_grvt_ob_to_ccxt(self, order_book: dict) -> dict: """ Converts GRVT-specific order book format to CCXT format. """ ob_time_ms: int = int(order_book["event_time"]) // 1_000_000 ccxt_ob = { "symbol": order_book["instrument"], "bids": [], "asks": [], "timestamp": ob_time_ms, "datetime": datetime.fromtimestamp(ob_time_ms / 1_000).strftime("%Y-%m-%dT%H:%M:%S.%f")[ :-3 ] + "Z", "nonce": int(order_book["event_time"]), } ccxt_ob["bids"] = [[bid["price"], bid["size"]] for bid in order_book["bids"]] ccxt_ob["asks"] = [[ask["price"], ask["size"]] for ask in order_book["asks"]] return ccxt_ob # Vault Management APIs def _get_fetch_vault_manager_investor_history_payload( self, vault_id: str, only_own_investments: bool = False, ) -> dict: """ Prepares payload for fetch_vault_manager_investor_history() method.
. Args: vault_id: The vault id to fetch history for.
only_own_investments: If True, fetch only investments by the manager.
Returns: A dictionary with a payload for Rest API call to fetch vault investor history. """ payload: dict[str, str | bool] = { "vault_id": vault_id, "only_own_investments": only_own_investments, } return payload def _get_fetch_vault_redemption_queue_payload( self, vault_id: str, ) -> dict: """ Prepares payload for fetch_vault_redemption_queue() method.
. Args: vault_id: The vault id to fetch redemption queue for.
Returns: A dictionary with a payload for Rest API call to fetch vault redemption queue. """ return {"vault_id": vault_id}