mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 01:08:07 +00:00
更新环境配置示例,添加 GRVT 相关的 API 凭证和选项,增强文档以支持新的交易所适配器,确保用户能够正确配置和使用 GRVT 交易功能。
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Literal
|
||||
|
||||
import requests
|
||||
|
||||
from .grvt_ccxt_base import GrvtCcxtBase
|
||||
from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint
|
||||
from .grvt_ccxt_types import (
|
||||
Amount,
|
||||
GrvtInstrumentKind,
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
from .grvt_ccxt_utils import (
|
||||
EnumEncoder,
|
||||
GrvtOrder,
|
||||
get_cookie_with_expiration,
|
||||
get_grvt_order,
|
||||
get_order_payload,
|
||||
)
|
||||
|
||||
|
||||
class GrvtCcxt(GrvtCcxtBase):
|
||||
"""
|
||||
GrvtCcxt class to interact with Grvt Rest API in synchronous mode.
|
||||
|
||||
Args:
|
||||
env: GrvtEnv (DEV, TESTNET, PROD)
|
||||
logger: logging.Logger
|
||||
parameters: dict with trading_account_id, private_key, api_key etc
|
||||
|
||||
Examples:
|
||||
>>> from grvt_api import GrvtCcxt
|
||||
>>> from grvt_env import GrvtEnv
|
||||
>>> grvt = GrvtCcxt(env=GrvtEnv.TESTNET)
|
||||
>>> grvt.fetch_markets()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
order_book_ccxt_format: bool = False,
|
||||
):
|
||||
"""Initialize the GrvtCcxt instance."""
|
||||
super().__init__(env, logger, parameters, order_book_ccxt_format)
|
||||
self._clsname: str = type(self).__name__
|
||||
self._session: requests.Session = requests.Session()
|
||||
self._session.headers.update({"Content-Type": "application/json"})
|
||||
self.refresh_cookie()
|
||||
# Assign markets here
|
||||
self.markets: dict[str, dict] = self.load_markets()
|
||||
|
||||
def refresh_cookie(self) -> dict | None:
|
||||
"""Refresh the session cookie."""
|
||||
if not self.should_refresh_cookie():
|
||||
return self._cookie
|
||||
path = get_grvt_endpoint(self.env, "AUTH")
|
||||
self._cookie = get_cookie_with_expiration(path, self._api_key)
|
||||
self._path_return_value_map[path] = self._cookie
|
||||
if self._cookie:
|
||||
self._session.cookies.update({"gravity": self._cookie["gravity"]})
|
||||
if self._cookie["X-Grvt-Account-Id"]:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
|
||||
)
|
||||
self.logger.info(
|
||||
f"refresh_cookie {self._cookie=} {self._session.cookies=} {self._session.headers=}"
|
||||
)
|
||||
return self._cookie
|
||||
|
||||
# PRIVATE API CALLS
|
||||
def _auth_and_post(self, path: str, payload: dict) -> dict:
|
||||
FN = f"_auth_and_post {path=}"
|
||||
MAX_LEN_TO_LOG = 1280
|
||||
response: dict = {}
|
||||
if not path:
|
||||
self.logger.warning(f"{FN} Invalid path {path=} {payload=}")
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}")
|
||||
# Always see if need to referesh cookie before sending a request
|
||||
self.refresh_cookie()
|
||||
payload_json = json.dumps(payload, cls=EnumEncoder)
|
||||
self.logger.info(f"{FN} {payload=}\n{payload_json=}")
|
||||
return_value = self._session.post(path, data=payload_json, timeout=5)
|
||||
return_text: str = ""
|
||||
try:
|
||||
return_text = return_value.text
|
||||
response = return_value.json()
|
||||
except Exception as err:
|
||||
self.logger.warning(f"{FN} Unable to parse {return_value=} as json. {err=}")
|
||||
if not return_value.ok:
|
||||
self.logger.warning(f"{FN} ERROR {payload_json=}\n{return_value=}\n{response=}")
|
||||
else:
|
||||
if len(return_text) > MAX_LEN_TO_LOG:
|
||||
self.logger.debug(f"{FN} OK {return_value=} {response=}")
|
||||
self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**")
|
||||
else:
|
||||
self.logger.info(f"{FN} OK {return_value=} {response=}")
|
||||
self._path_return_value_map[path] = response
|
||||
return response
|
||||
|
||||
def _create_grvt_order(self, order: GrvtOrder) -> dict:
|
||||
"""
|
||||
Send a GrvtOrder object to the exchange.
|
||||
:param order: The GrvtOrder object.
|
||||
Return: dictionary representing the order response.
|
||||
"""
|
||||
FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}"
|
||||
order_payload = get_order_payload(
|
||||
order,
|
||||
private_key=self._private_key,
|
||||
env=self.env,
|
||||
instruments=self.markets,
|
||||
)
|
||||
path = get_grvt_endpoint(self.env, "CREATE_ORDER")
|
||||
self.logger.info(f"{FN} {path=} {order_payload=}")
|
||||
response: dict = self._auth_and_post(path, payload=order_payload)
|
||||
if response.get("result") is None:
|
||||
self.logger.error(f"{FN} Error: {response}")
|
||||
return {}
|
||||
self.logger.info(
|
||||
f"{FN} Order created:"
|
||||
f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}"
|
||||
)
|
||||
return response.get("result", {})
|
||||
|
||||
def create_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""Ccxt compliant signature."""
|
||||
self._check_account_auth()
|
||||
self._check_valid_symbol(symbol)
|
||||
# Validate order fields
|
||||
self._check_order_arguments(order_type, side, amount, price)
|
||||
# create GrvtOrder object
|
||||
order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60)
|
||||
order = get_grvt_order(
|
||||
sub_account_id=self.get_trading_account_id(),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side,
|
||||
amount=amount,
|
||||
limit_price=price,
|
||||
order_duration_secs=order_duration_secs,
|
||||
params=params,
|
||||
)
|
||||
return self._create_grvt_order(order)
|
||||
|
||||
def create_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
return self.create_order(symbol, "limit", side, amount, price, params)
|
||||
|
||||
def cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature BUT lacks symbol
|
||||
Cancel all orders for a sub-account.
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
FN = f"{self._clsname} cancel_all_orders"
|
||||
payload: dict = self._get_payload_cancel_all_orders(params)
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel orders: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
def cancel_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Cancel specific order for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
id (str): exchange assigned order ID<br>
|
||||
symbol (str): trading symbol<br>
|
||||
params:
|
||||
* client_order_id (str): client assigned order ID<br>
|
||||
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
|
||||
Returns:
|
||||
True if cancel request was acked by exchange. False otherwise.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} cancel_order"
|
||||
self._check_account_auth()
|
||||
payload: dict = {
|
||||
"sub_account_id": self.get_trading_account_id(),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = str(id)
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
|
||||
|
||||
if "time_to_live_ms" in params:
|
||||
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
|
||||
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ORDER")
|
||||
self.logger.info(
|
||||
f"{FN} Send cancel {payload=} for trading_account_id={self.get_trading_account_id()}"
|
||||
)
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel order: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
def set_derisk_mm_ratio(self, ratio: str) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Set the Derisk to Maintenance marginb ratio for the account.
|
||||
Private call requires authorization.
|
||||
See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio)
|
||||
for details.
|
||||
|
||||
Args:
|
||||
ratio (Amount): The new derisking market making ratio.
|
||||
|
||||
Returns:
|
||||
True if the request was acknowledged by the exchange. False otherwise.
|
||||
"""
|
||||
FN = f"{self._clsname} set_derisk_mm_ratio"
|
||||
self._check_account_auth()
|
||||
payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio))
|
||||
path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO")
|
||||
self.logger.info(
|
||||
f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}"
|
||||
)
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
# set_ack = response.get("result", {}).get("ack")
|
||||
# if not set_ack:
|
||||
# self.logger.warning(f"{FN} failed to set derisk_mm_ratio: {response=}")
|
||||
# return False
|
||||
self.logger.info(f"{FN} Set derisk_mm_ratio {response=}")
|
||||
return True
|
||||
|
||||
def fetch_open_orders(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch open orders for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: (str) get orders for this symbol only.<br>
|
||||
since: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
limit: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
Returns:
|
||||
a list of dictionaries, each dict represent an order.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_open_orders(symbol, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
open_orders: list = response.get("result", [])
|
||||
if symbol:
|
||||
open_orders = [
|
||||
o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol
|
||||
]
|
||||
return open_orders
|
||||
|
||||
def fetch_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Get Order status by order_id or client_order_id
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>
|
||||
Args:
|
||||
id: (str) order_id to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`client_order_id` (int): client assigned order ID.<br>
|
||||
Return: dict with order's details or {} if order was NOT found.
|
||||
"""
|
||||
self._check_account_auth()
|
||||
payload = {
|
||||
"sub_account_id": self.get_trading_account_id(),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = id
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(
|
||||
f"{self._clsname} fetch_order() requires order_id or params['client_order_id']"
|
||||
)
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
def fetch_order_history(self, params: dict = {}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Get Order status by order_id or client_order_id
|
||||
Private call requires authorization.<br>
|
||||
See [Order History](https://api-docs.grvt.io/trading_api/#order-history)
|
||||
for details.<br>
|
||||
Args:
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
|
||||
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
|
||||
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
|
||||
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
|
||||
Return: a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent an order state.<br>.
|
||||
"""
|
||||
self._check_account_auth()
|
||||
payload = self._get_payload_fetch_order_history(params)
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
def get_account_summary(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Return: The account summary.
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
|
||||
for details.<br>
|
||||
Returns: dictionary with account data.<br>.
|
||||
"""
|
||||
FN = f"{self._clsname} get_account_summary {type=}"
|
||||
self._check_account_auth()
|
||||
payload = {}
|
||||
if type == "sub-account":
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY")
|
||||
payload = {"sub_account_id": self.get_trading_account_id()}
|
||||
elif type == "funding":
|
||||
path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY")
|
||||
elif type == "aggregated":
|
||||
path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY")
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}")
|
||||
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
sub_account: dict = response.get("result", {})
|
||||
if not sub_account:
|
||||
self.logger.info(f"{FN} No account summary for {path=} {payload=}")
|
||||
return sub_account
|
||||
|
||||
def fetch_balance(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch balances for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'.
|
||||
Valid values: 'sub-account', 'funding', 'aggregated'.
|
||||
|
||||
Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.<br>.
|
||||
"""
|
||||
account_summary: dict = self.get_account_summary(type)
|
||||
return self._get_balances_from_account_summary(account_summary)
|
||||
|
||||
def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict:
|
||||
"""
|
||||
HISTORICAL data.<br>
|
||||
Get account history.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account History](https://api-docs.grvt.io/trading_api/#account-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
limit: maximum number of account snapshots per page to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
|
||||
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
|
||||
`cursor` (str): cursor for the pagination. If cursor is present then we ignore
|
||||
`start_time` and `end_time`.<br>
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : list of account history snapshots.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_account_history(limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
def fetch_positions(self, symbols: list[str] = [], params={}):
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch positions for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Positions](https://api-docs.grvt.io/trading_api/#positions)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbols: list(str) get positions for these symbols only.<br>
|
||||
|
||||
Returns: list of dictionaries, each dict represent a position.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_positions(symbols, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_POSITIONS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
positions: list = response.get("result", [])
|
||||
if symbols:
|
||||
self.logger.info(f"fetch_positions filter positions by {symbols=}")
|
||||
positions = [p for p in positions if p.get("instrument") in symbols]
|
||||
return positions
|
||||
|
||||
def fetch_my_trades(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Fetch past trades for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent a trade.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_my_trades(symbol, since, limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
if symbol:
|
||||
# filter result by symbol
|
||||
trades: list = response.get("result", [])
|
||||
trades = [t for t in trades if t.get("instrument") == symbol]
|
||||
response["result"] = trades
|
||||
return response
|
||||
|
||||
# **************** PUBLIC API CALLS
|
||||
def load_markets(self) -> dict[str, dict]:
|
||||
self.logger.info("load_markets START")
|
||||
instruments = self.fetch_markets(
|
||||
params={
|
||||
"kind": GrvtInstrumentKind.PERPETUAL,
|
||||
# "base": "BTC",
|
||||
# "quote": "USDT",
|
||||
}
|
||||
)
|
||||
if instruments:
|
||||
self.markets = {
|
||||
str(i.get("instrument", "")): i for i in instruments if i.get("instrument")
|
||||
}
|
||||
self.logger.info(f"load_markets: loaded {len(self.markets)} markets.")
|
||||
else:
|
||||
self.logger.warning("load_markets: No markets found.")
|
||||
return self.markets
|
||||
|
||||
def fetch_markets(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Retrieve the list of all instruments of matching kind, base and quote
|
||||
supported by the exchange.
|
||||
|
||||
Params: dict with keys:<br>
|
||||
`is_active` (bool) - defaults to True.<br>
|
||||
`limit` (int) - defaiults to 20.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns: list of dictionaries per instrument with keys:<br>
|
||||
`instrument`: symbol e.g. 'BTC_USDT_Perp'.<br>
|
||||
`instrument_hash`: hashed symbol for order signing e.g. '0x030501'.<br>
|
||||
`base`: base currency e.g. 'BTC'.<br>
|
||||
`quote`: quote currency e.g. 'USDT'.<br>
|
||||
`kind`: kind of instrument 'PERPETUAL'/'FUTURE'.<br>
|
||||
'base_decimals': size multiplier for order signing.<br>
|
||||
`tick_size`: price tick size.<br>
|
||||
`min_size`: minimum order size.<br>
|
||||
"""
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_markets(params)
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_all_markets(
|
||||
self,
|
||||
is_active: bool | None = True,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Retrieve the list of all instruments supported by the exchange.<br>
|
||||
Params:<br>
|
||||
`is_active` (bool) - defaults to True.<br>.
|
||||
|
||||
Returns: list of dictionaries per instrument. See fetch_markets().<br>
|
||||
"""
|
||||
# Prepare request payload
|
||||
payload = {"is_active": is_active}
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS")
|
||||
response: dict = self._auth_and_post(path, payload)
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_market(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the instrument object for a given symbol.
|
||||
:param symbol: The symbol of the instrument.
|
||||
"""
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENT")
|
||||
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_ticker(self, symbol: str, params: dict = {}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Retrieve the ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '592737000', 'best_ask_size': '21678', 'funding_rate_curr': 2544, 'funding_rate_avg': 0,
|
||||
# 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000',
|
||||
# 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500',
|
||||
# 'sell_volume_q': '68764000329900', 'high_price': '3435450000', 'low_price': '100000',
|
||||
# 'open_price': '32554000000000', 'open_interest': '8174350000000',
|
||||
# 'long_short_ratio': 1.0948905}
|
||||
path = get_grvt_endpoint(self.env, "GET_TICKER")
|
||||
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_mini_ticker(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the mini-ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The mini-ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569850000000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '59273700000000', 'best_ask_size': '21678000000'}
|
||||
path = get_grvt_endpoint(self.env, "GET_MINI_TICKER")
|
||||
response: dict = self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Retrieve the order book of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The order book dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '0', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...]
|
||||
# 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...]
|
||||
payload = {"instrument": symbol, "aggregate": 1}
|
||||
if limit:
|
||||
payload["depth"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
if self.is_order_book_ccxt_format():
|
||||
# Convert to ccxt format
|
||||
return self.convert_grvt_ob_to_ccxt(response.get("result", {}))
|
||||
return response.get("result", {})
|
||||
|
||||
def fetch_recent_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
limit: int | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Retrieve recent trades of a given instrument.
|
||||
:param symbol: The instrument name.
|
||||
:return: The list of trades for the instrument.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict[str, str | int] = {"instrument": symbol}
|
||||
if limit:
|
||||
payload["limit"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_TRADES")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response.get("result", [])
|
||||
|
||||
def fetch_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int | None = None,
|
||||
limit: int = 10,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Retrieve trade history of a given instrument.
|
||||
:param symbol: The instrument name.
|
||||
:return: dict with field 'result' containing a list of trades.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict = self._get_payload_fetch_trades(
|
||||
symbol,
|
||||
since=since,
|
||||
limit=limit,
|
||||
params=params,
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
def fetch_funding_rate_history(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int = 0,
|
||||
limit: int = 1_000,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Retrieve the funding rates history of a given instrument.<br>
|
||||
Args:
|
||||
symbol (str): The instrument name.<br>
|
||||
since (int): fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: int - maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
Returns:
|
||||
dict with field 'result' containing list of dictionaries repesenting funding rate
|
||||
at a point in time with fields:<br>
|
||||
`instrument` (str): instrument name.<br>
|
||||
'funding_rate' (float): funding rate.<br>
|
||||
'funding_time' (int): funding time in nanoseconds.<br>
|
||||
'mark_price' (float): mark price.<br>.
|
||||
"""
|
||||
payload: dict[str, str | int] = {"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"])
|
||||
if limit:
|
||||
payload["limit"] = int(limit)
|
||||
path = get_grvt_endpoint(self.env, "GET_FUNDING")
|
||||
response: dict = self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
def fetch_ohlcv(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe="1m",
|
||||
since: int = 0,
|
||||
limit: int = 10,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.
|
||||
|
||||
Retrieve the ohlc history of a given instrument.
|
||||
|
||||
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:
|
||||
dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:<br>
|
||||
`instrument` - instrument name.<br>
|
||||
`open_time` - start of interval in nanoseconds.<br>
|
||||
`close_time` - end of interval in nanoseconds.<br>
|
||||
`open` - opening price.<br>
|
||||
`close` - closing price.<br>
|
||||
`high` - highest price.<br>
|
||||
`low` - lowest price.<br>
|
||||
`volume_u` - volume in units.<br>
|
||||
`volume_q` - volume in quote(USDT).<br>
|
||||
`trades` - number of trades.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} fetch_ohlcv"
|
||||
payload: dict[str, Any] = self._get_payload_fetch_ohlcv(
|
||||
symbol, timeframe, since, limit, params
|
||||
)
|
||||
self.logger.info(f"{FN} {payload=}")
|
||||
path = get_grvt_endpoint(self.env, "GET_CANDLESTICK")
|
||||
return self._auth_and_post(path, payload=payload)
|
||||
|
||||
# Vault Management APIs
|
||||
def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict:
|
||||
payload: dict = self._get_fetch_vault_manager_investor_history_payload(
|
||||
vault_id=self.get_trading_account_id(),
|
||||
only_own_investments=only_own_investments, # Default to False to fetch all investments
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY")
|
||||
# path = "https://trades.grvt.io/full/v1/vault_manager_investor_history"
|
||||
return self._auth_and_post(path, payload=payload)
|
||||
|
||||
def fetch_vault_redemption_queue(self):
|
||||
payload: dict = self._get_fetch_vault_redemption_queue_payload(
|
||||
vault_id=self.get_trading_account_id()
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE")
|
||||
# path = "https://trades.grvt.io/full/v1/vault_view_redemption_queue"
|
||||
return self._auth_and_post(path, payload=payload)
|
||||
@@ -0,0 +1,586 @@
|
||||
# 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.<br>.
|
||||
|
||||
Args:
|
||||
params: (dict) with possible keys as:.<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
Returns: a dictionary with a payload for Rest API call to cancel all orders.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
`end_time` (int): fetch trades until this timestamp in nanoseconds.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with a payload for Rest API call to fetch trades.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with a payload for Rest API call to fetch trades.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
|
||||
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
|
||||
`cursor` (int):cursor for the pagination. If cursor is present then we ignore
|
||||
`start_time` and `end_time`.<br>
|
||||
Returns:
|
||||
a dictionary with a payload for Rest API call to fetch account history.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
symbols: list(str) get positions for these symbols only.<br>
|
||||
|
||||
Returns: a dictionary with a payload for Rest API call to fetch positions.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
params: (dict) with possible keys as:.<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
|
||||
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
|
||||
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
|
||||
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
|
||||
Returns: a dictionary with a payload for Rest API call to fetch order history.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
params: (dict) with possible keys as:.<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
Returns: a dictionary with a payload for Rest API call to fetch order history.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
symbol: The instrument name.<br>
|
||||
timeframe: The timeframe of the ohlc.
|
||||
See `ccxt_interval_to_grvt_candlestick_interval`.<br>
|
||||
since: fetch ohlc since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of ohlc to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.<br>
|
||||
|
||||
Returns: a dictionary with a payload for Rest API call to fetch_ohlcv.<br>
|
||||
See [Candlestick] (https://api-docs.grvt.io/market_data_api/#candlestick_1)
|
||||
for more details.<br>
|
||||
"""
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
vault_id: The vault id to fetch history for.<br>
|
||||
only_own_investments: If True, fetch only investments by the manager.<br>
|
||||
|
||||
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.<br>.
|
||||
|
||||
Args:
|
||||
vault_id: The vault id to fetch redemption queue for.<br>
|
||||
|
||||
Returns:
|
||||
A dictionary with a payload for Rest API call to fetch vault redemption queue.
|
||||
"""
|
||||
return {"vault_id": vault_id}
|
||||
@@ -0,0 +1,195 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GrvtEnv(str, Enum):
|
||||
PROD = "prod"
|
||||
TESTNET = "testnet"
|
||||
STAGING = "staging"
|
||||
DEV = "dev"
|
||||
|
||||
# GrvtEndpointType defines the root path for a family of endpoints
|
||||
class GrvtEndpointType(str, Enum):
|
||||
EDGE = "edge"
|
||||
TRADE_DATA = "tdg"
|
||||
MARKET_DATA = "mdg"
|
||||
|
||||
|
||||
class GrvtWSEndpointType(str, Enum):
|
||||
TRADE_DATA = "tdg"
|
||||
MARKET_DATA = "mdg"
|
||||
TRADE_DATA_RPC_FULL = "tdg_rpc_full"
|
||||
MARKET_DATA_RPC_FULL = "mdg_rpc_full"
|
||||
|
||||
|
||||
END_POINT_VERSION = os.getenv("GRVT_END_POINT_VERSION", "v1")
|
||||
|
||||
|
||||
def get_grvt_endpoint_domains(env_name: str) -> dict[GrvtEndpointType, str]:
|
||||
if env_name == GrvtEnv.PROD.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: "https://edge.grvt.io",
|
||||
GrvtEndpointType.TRADE_DATA: "https://trades.grvt.io",
|
||||
GrvtEndpointType.MARKET_DATA: "https://market-data.grvt.io",
|
||||
}
|
||||
if env_name == GrvtEnv.TESTNET.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: f"https://edge.{env_name}.grvt.io",
|
||||
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.grvt.io",
|
||||
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.grvt.io",
|
||||
}
|
||||
if env_name == GrvtEnv.STAGING.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io",
|
||||
}
|
||||
if env_name == GrvtEnv.DEV.value:
|
||||
return {
|
||||
GrvtEndpointType.EDGE: f"https://edge.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.TRADE_DATA: f"https://trades.{env_name}.gravitymarkets.io",
|
||||
GrvtEndpointType.MARKET_DATA: f"https://market-data.{env_name}.gravitymarkets.io",
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
def get_grvt_ws_endpoint(
|
||||
env: str,
|
||||
endpoint_type: GrvtWSEndpointType,
|
||||
) -> str:
|
||||
"""Returns string pointing to WS endpoint for given environment and endpoint type."""
|
||||
if env == GrvtEnv.PROD.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: "wss://trades.grvt.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: "wss://market-data.grvt.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: "wss://trades.grvt.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: "wss://market-data.grvt.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
if env == GrvtEnv.TESTNET.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.grvt.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.grvt.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.grvt.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.grvt.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
if env == GrvtEnv.STAGING.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
if env == GrvtEnv.DEV.value:
|
||||
return {
|
||||
GrvtWSEndpointType.TRADE_DATA: f"wss://trades.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.MARKET_DATA: f"wss://market-data.{env}.gravitymarkets.io/ws",
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL: f"wss://trades.{env}.gravitymarkets.io/ws/full",
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL: f"wss://market-data.{env}.gravitymarkets.io/ws/full",
|
||||
}.get(endpoint_type, "")
|
||||
return ""
|
||||
|
||||
# Mapping of WS stream names to DEFAULT endpoint types
|
||||
GRVT_WS_STREAMS = {
|
||||
# ******* Market Data ********
|
||||
"mini.s": GrvtWSEndpointType.MARKET_DATA,
|
||||
"mini.d": GrvtWSEndpointType.MARKET_DATA,
|
||||
"ticker.s": GrvtWSEndpointType.MARKET_DATA,
|
||||
"ticker.d": GrvtWSEndpointType.MARKET_DATA,
|
||||
"book.s": GrvtWSEndpointType.MARKET_DATA,
|
||||
"book.d": GrvtWSEndpointType.MARKET_DATA,
|
||||
"trade": GrvtWSEndpointType.MARKET_DATA,
|
||||
"candle": GrvtWSEndpointType.MARKET_DATA,
|
||||
# ******* Trade Data ********
|
||||
"order": GrvtWSEndpointType.TRADE_DATA,
|
||||
"state": GrvtWSEndpointType.TRADE_DATA,
|
||||
"cancel": GrvtWSEndpointType.TRADE_DATA,
|
||||
"position": GrvtWSEndpointType.TRADE_DATA,
|
||||
"fill": GrvtWSEndpointType.TRADE_DATA,
|
||||
"transfer": GrvtWSEndpointType.TRADE_DATA,
|
||||
"deposit": GrvtWSEndpointType.TRADE_DATA,
|
||||
"withdrawal": GrvtWSEndpointType.TRADE_DATA,
|
||||
}
|
||||
|
||||
|
||||
def is_trading_ws_endpoint(end_point_type: GrvtWSEndpointType) -> bool:
|
||||
return end_point_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]
|
||||
|
||||
|
||||
# "wss://market-data.testnet.grvt.io/ws"
|
||||
# wss://trades.testnet.grvt.io/ws
|
||||
# GRVT_ENDPOINTS defines the endpoint paths grouped by endpoint type
|
||||
GRVT_ENDPOINTS = {
|
||||
GrvtEndpointType.EDGE: {
|
||||
"GRAPHQL": "query",
|
||||
"AUTH": "auth/api_key/login",
|
||||
},
|
||||
GrvtEndpointType.TRADE_DATA: {
|
||||
"CREATE_ORDER": f"full/{END_POINT_VERSION}/create_order",
|
||||
"CANCEL_ALL_ORDERS": f"full/{END_POINT_VERSION}/cancel_all_orders",
|
||||
"CANCEL_ORDER": f"full/{END_POINT_VERSION}/cancel_order",
|
||||
"GET_OPEN_ORDERS": f"full/{END_POINT_VERSION}/open_orders",
|
||||
"GET_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/account_summary",
|
||||
"GET_FUNDING_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/funding_account_summary",
|
||||
"GET_AGGREGATED_ACCOUNT_SUMMARY": f"full/{END_POINT_VERSION}/aggregated_account_summary",
|
||||
"GET_ACCOUNT_HISTORY": f"full/{END_POINT_VERSION}/account_history",
|
||||
"GET_POSITIONS": f"full/{END_POINT_VERSION}/positions",
|
||||
"GET_ORDER": f"full/{END_POINT_VERSION}/order",
|
||||
"GET_ORDER_HISTORY": f"full/{END_POINT_VERSION}/order_history",
|
||||
"GET_FILL_HISTORY": f"full/{END_POINT_VERSION}/fill_history",
|
||||
"SET_DERISK_MM_RATIO": f"full/{END_POINT_VERSION}/set_derisk_mm_ratio",
|
||||
"GET_VAULT_MANAGER_INVESTOR_HISTORY": f"full/{END_POINT_VERSION}/vault_manager_investor_history",
|
||||
"GET_VAULT_REDEMPTION_QUEUE": f"full/{END_POINT_VERSION}/vault_view_redemption_queue",
|
||||
},
|
||||
GrvtEndpointType.MARKET_DATA: {
|
||||
"GET_ALL_INSTRUMENTS": f"full/{END_POINT_VERSION}/all_instruments",
|
||||
"GET_INSTRUMENTS": f"full/{END_POINT_VERSION}/instruments",
|
||||
"GET_INSTRUMENT": f"full/{END_POINT_VERSION}/instrument",
|
||||
"GET_TICKER": f"full/{END_POINT_VERSION}/ticker",
|
||||
"GET_MINI_TICKER": f"full/{END_POINT_VERSION}/mini",
|
||||
"GET_ORDER_BOOK": f"full/{END_POINT_VERSION}/book",
|
||||
"GET_TRADES": f"full/{END_POINT_VERSION}/trade",
|
||||
"GET_TRADE_HISTORY": f"full/{END_POINT_VERSION}/trade_history",
|
||||
"GET_FUNDING": f"full/{END_POINT_VERSION}/funding",
|
||||
"GET_CANDLESTICK": f"full/{END_POINT_VERSION}/kline",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_grvt_endpoint(environment: GrvtEnv, end_point: str) -> str:
|
||||
# if end_point == "GET_ALL_INSTRUMENTS":
|
||||
# return "https://market-data.testnet.grvt.io/full/v1/instruments"
|
||||
endpoint_domains = get_grvt_endpoint_domains(environment.value)
|
||||
for endpoints_type, endpoints in GRVT_ENDPOINTS.items():
|
||||
if end_point in endpoints:
|
||||
return f"{endpoint_domains[endpoints_type]}/{endpoints[end_point]}"
|
||||
return ""
|
||||
|
||||
|
||||
def get_all_grvt_endpoints(environment: GrvtEnv) -> dict[str, str]:
|
||||
endpoint_domains = get_grvt_endpoint_domains(environment.value)
|
||||
endpoints = {}
|
||||
for endpoints_type, endpoints_map in GRVT_ENDPOINTS.items():
|
||||
for endpoint, path in endpoints_map.items():
|
||||
endpoints[endpoint] = f"{endpoint_domains[endpoints_type]}/{path}"
|
||||
return endpoints
|
||||
|
||||
|
||||
CHAIN_IDS = {
|
||||
GrvtEnv.DEV.value: 327,
|
||||
GrvtEnv.STAGING.value: 327,
|
||||
GrvtEnv.TESTNET.value: 326,
|
||||
GrvtEnv.PROD.value: 325,
|
||||
}
|
||||
|
||||
########################################################
|
||||
@@ -0,0 +1,32 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
LOG_FILE = os.getenv("LOG_FILE", "FALSE").upper()
|
||||
GRVT_ENV = os.getenv("GRVT_ENV")
|
||||
|
||||
if LOG_FILE == "TRUE":
|
||||
LOG_TIMESTAMP = datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
||||
fn = sys.argv[0].split("/")[-1]
|
||||
fn_base = fn.split(".")[0]
|
||||
if GRVT_ENV:
|
||||
filename = f"logs/{fn_base}_{GRVT_ENV}_{LOG_TIMESTAMP}.log"
|
||||
else:
|
||||
filename = f"logs/{fn_base}_{LOG_TIMESTAMP}.log"
|
||||
os.makedirs("logs", exist_ok=True)
|
||||
logging.basicConfig(
|
||||
filename=filename,
|
||||
level=os.getenv("LOGGING_LEVEL", "INFO"),
|
||||
format="%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Using FILE logger {LOG_FILE=}")
|
||||
else:
|
||||
logging.basicConfig(
|
||||
level=os.getenv("LOGGING_LEVEL", "INFO"),
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Using CONSOLE logger {LOG_FILE=}")
|
||||
@@ -0,0 +1,841 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .grvt_ccxt_base import GrvtCcxtBase
|
||||
|
||||
# import requests
|
||||
# from env import ENDPOINTS
|
||||
from .grvt_ccxt_env import GrvtEnv, get_grvt_endpoint
|
||||
from .grvt_ccxt_types import (
|
||||
Amount,
|
||||
GrvtInstrumentKind,
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
from .grvt_ccxt_utils import (
|
||||
EnumEncoder,
|
||||
GrvtOrder,
|
||||
get_cookie_with_expiration,
|
||||
get_cookie_with_expiration_async,
|
||||
get_grvt_order,
|
||||
get_order_payload,
|
||||
)
|
||||
|
||||
|
||||
class GrvtCcxtPro(GrvtCcxtBase):
|
||||
"""
|
||||
GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode.
|
||||
|
||||
Args:
|
||||
env: GrvtCcxtPro (DEV, TESTNET, PROD)
|
||||
parameters: dict with trading_account_id, private_key, api_key etc
|
||||
|
||||
Examples:
|
||||
>>> from grvt_api_pro import GrvtCcxtPro
|
||||
>>> from grvt_env import GrvtEnv
|
||||
>>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET)
|
||||
>>> await grvt.fetch_markets()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
order_book_ccxt_format: bool = False,
|
||||
):
|
||||
"""Initialize the GrvtCcxt instance."""
|
||||
super().__init__(env, logger, parameters, order_book_ccxt_format)
|
||||
self._clsname: str = type(self).__name__
|
||||
self._session = aiohttp.ClientSession(headers={"Content-Type": "application/json"})
|
||||
# Force sync call to get cookie here
|
||||
self._cookie = get_cookie_with_expiration(
|
||||
get_grvt_endpoint(self.env, "AUTH"), self._api_key
|
||||
)
|
||||
self.update_session_with_cookie()
|
||||
|
||||
def __del__(self):
|
||||
"""Close the aiohttp session when the instance is deleted."""
|
||||
self.logger.info(f"{self._clsname} __del__() called")
|
||||
if self._session:
|
||||
self.logger.info(f"{self._clsname} closing session")
|
||||
asyncio.get_running_loop().create_task(self._session.close())
|
||||
|
||||
def update_session_with_cookie(self) -> None:
|
||||
if self._cookie:
|
||||
self._session.cookie_jar.update_cookies({"gravity": self._cookie["gravity"]})
|
||||
if self._cookie["X-Grvt-Account-Id"]:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
|
||||
)
|
||||
self.logger.info(
|
||||
f"update_session_with_cookie {self._cookie=} {self._session.cookie_jar=}"
|
||||
f" {self._session.headers=}"
|
||||
)
|
||||
|
||||
async def refresh_cookie(self) -> dict | None:
|
||||
"""Refresh the session cookie."""
|
||||
if not self.should_refresh_cookie():
|
||||
return self._cookie
|
||||
path: str = get_grvt_endpoint(self.env, "AUTH")
|
||||
self._cookie = await get_cookie_with_expiration_async(path, self._api_key)
|
||||
self._path_return_value_map[path] = self._cookie
|
||||
self.update_session_with_cookie()
|
||||
return self._cookie
|
||||
|
||||
# PRIVATE API CALLS
|
||||
async def _auth_and_post(self, path: str, payload: dict) -> dict:
|
||||
FN = f"{self._clsname} _auth_and_post {path=}"
|
||||
MAX_LEN_TO_LOG = 1280
|
||||
response: dict = {}
|
||||
if not path:
|
||||
self.logger.warning(f"{FN} Invalid path {path=} {payload=}")
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid path {path=} {payload=}")
|
||||
# Always see if need to referesh cookie before sending a request
|
||||
await self.refresh_cookie()
|
||||
payload_json = json.dumps(payload, cls=EnumEncoder)
|
||||
self.logger.info(f"{FN} {payload=}\n{payload_json=}")
|
||||
return_text: str = ""
|
||||
async with self._session.post(
|
||||
url=path,
|
||||
data=payload_json,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
) as return_value:
|
||||
return_text: str = ""
|
||||
try:
|
||||
return_text = await return_value.text()
|
||||
response = await return_value.json(content_type="application/json")
|
||||
except Exception as err:
|
||||
self.logger.warning(
|
||||
f"{FN} Unable to parse {return_value=} as "
|
||||
f" json(content_type='application/json'). {err=}"
|
||||
)
|
||||
if not return_value.ok:
|
||||
self.logger.warning(f"{FN} {payload_json=}\n{return_value=}\n{response=}")
|
||||
else:
|
||||
if len(return_text) > MAX_LEN_TO_LOG:
|
||||
self.logger.debug(f"{FN} OK {return_value=} response={response}")
|
||||
self.logger.info(f"{FN} OK {return_value=} response=**TOO LONG**")
|
||||
else:
|
||||
self.logger.info(f"{FN} OK {return_value=} response={response}")
|
||||
self._path_return_value_map[path] = response
|
||||
return response or {}
|
||||
|
||||
async def _create_grvt_order(self, order: GrvtOrder) -> dict:
|
||||
"""
|
||||
Send a GrvtOrder object to the exchange.
|
||||
:param order: The GrvtOrder object.
|
||||
Return: dictionary representing the order response.
|
||||
"""
|
||||
FN = f"{self._clsname} _create_grvt_order cloid:{order.metadata.client_order_id}"
|
||||
order_payload = get_order_payload(
|
||||
order,
|
||||
private_key=self._private_key,
|
||||
env=self.env,
|
||||
instruments=self.markets,
|
||||
)
|
||||
path = get_grvt_endpoint(self.env, "CREATE_ORDER")
|
||||
self.logger.info(f"{FN} {path=} {order_payload=}")
|
||||
response: dict = await self._auth_and_post(path, payload=order_payload)
|
||||
if response.get("result") is None:
|
||||
self.logger.error(f"Error creating order, {response}")
|
||||
return {}
|
||||
self.logger.info(
|
||||
f"{FN} Order created:"
|
||||
f"{response.get('result', {}).get('metadata', {}).get('client_order_id')}"
|
||||
)
|
||||
return response.get("result", {})
|
||||
|
||||
def _get_order_with_validations(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params: dict = {},
|
||||
) -> GrvtOrder:
|
||||
self._check_account_auth()
|
||||
self._check_valid_symbol(symbol)
|
||||
# Validate order fields
|
||||
self._check_order_arguments(order_type, side, amount, price)
|
||||
# create GrvtOrder object
|
||||
order_duration_secs = params.get("order_duration_secs", 24 * 60 * 60)
|
||||
return get_grvt_order(
|
||||
sub_account_id=self.get_trading_account_id(),
|
||||
symbol=symbol,
|
||||
order_type=order_type,
|
||||
side=side,
|
||||
amount=amount,
|
||||
limit_price=price,
|
||||
order_duration_secs=order_duration_secs,
|
||||
params=params,
|
||||
)
|
||||
|
||||
async def create_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""Ccxt compliant signature."""
|
||||
order = self._get_order_with_validations(symbol, order_type, side, amount, price, params)
|
||||
return await self._create_grvt_order(order)
|
||||
|
||||
async def create_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
return await self.create_order(symbol, "limit", side, amount, price, params)
|
||||
|
||||
async def cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature BUT lacks symbol
|
||||
Cancel all orders for a sub-account.
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
FN = f"{self._clsname} cancel_all_orders"
|
||||
payload: dict = self._get_payload_cancel_all_orders(params)
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ALL_ORDERS")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel orders: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
async def cancel_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> bool:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Cancel specific order for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
id (str): exchange assigned order ID<br>
|
||||
symbol (str): trading symbol<br>
|
||||
params:
|
||||
* client_order_id (str): client assigned order ID<br>
|
||||
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
|
||||
Returns:
|
||||
True if cancel request was acked by exchange. False otherwise.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} cancel_order"
|
||||
self._check_account_auth()
|
||||
# Prepare payload
|
||||
payload: dict = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = str(id)
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
|
||||
if "time_to_live_ms" in params:
|
||||
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
|
||||
|
||||
# Send cancel request
|
||||
path = get_grvt_endpoint(self.env, "CANCEL_ORDER")
|
||||
self.logger.info(
|
||||
f"{FN} Send cancel {payload=} for trading_account_id={self._trading_account_id}"
|
||||
)
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
cancel_ack = response.get("result", {}).get("ack")
|
||||
|
||||
if not cancel_ack:
|
||||
self.logger.warning(f"{FN} failed to cancel order: {response=}")
|
||||
return False
|
||||
self.logger.info(f"{FN} Cancelled {response=}")
|
||||
return True
|
||||
|
||||
async def set_derisk_mm_ratio(self, ratio: str) -> bool:
|
||||
"""
|
||||
|
||||
Set the Derisk to Maintenance marginb ratio for the account.
|
||||
Private call requires authorization.
|
||||
See [Set Derisk M M ratio](https://api-docs.grvt.io/trading_api/#set-derisk-m-m-ratio)
|
||||
for details.
|
||||
|
||||
Args:
|
||||
ratio (Amount): The new derisking market making ratio.
|
||||
|
||||
Returns:
|
||||
True if the request was acknowledged by the exchange. False otherwise.
|
||||
"""
|
||||
FN = f"{self._clsname} set_derisk_mm_ratio"
|
||||
self._check_account_auth()
|
||||
payload: dict[str, str | dict] = self._get_set_derisk_mm_ratio_payload(str(ratio))
|
||||
path = get_grvt_endpoint(self.env, "SET_DERISK_MM_RATIO")
|
||||
self.logger.info(
|
||||
f"{FN} Send {payload=} for trading_account_id={self.get_trading_account_id()}"
|
||||
)
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
self.logger.info(f"{FN} Set derisk_mm_ratio {response=}")
|
||||
return True
|
||||
|
||||
async def fetch_open_orders(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch open orders for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get orders for this symbol only.<br>
|
||||
since: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
limit: ccxt-compliant argument, NOT SUPPORTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values are 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch orders
|
||||
for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
Returns:
|
||||
a list of dictionaries, each dict represent an order.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_open_orders(symbol, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_OPEN_ORDERS")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
open_orders: list = response.get("result", [])
|
||||
if symbol:
|
||||
open_orders = [
|
||||
o for o in open_orders if o.get("legs") and o["legs"][0].get("instrument") == symbol
|
||||
]
|
||||
return open_orders
|
||||
|
||||
async def fetch_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Private call requires authorization.<br>
|
||||
See [Get Order](https://api-docs.grvt.io/trading_api/#get-order)
|
||||
for details.<br>.
|
||||
|
||||
Get Order status by either order_id or client_order_id
|
||||
Args:
|
||||
id: (str) order_id to fetch.<br>
|
||||
symbol: (str) NOT SUPPRTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`client_order_id` (int): client assigned order ID.<br>
|
||||
Returns:
|
||||
dict with order details or {} if order was NOT found.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} fetch_order"
|
||||
self._check_account_auth()
|
||||
payload = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = id
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or params['client_order_id']")
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
async def fetch_order_history(self, params: dict = {}) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Get Order history of orders by kind/base/quote.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Order History](https://api-docs.grvt.io/trading_api/#order-history)
|
||||
for details.<br>
|
||||
Args:
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind`: (str) - The kind filter to apply. Defaults to all kinds.<br>
|
||||
`base`: (str) - The base currency filter. Defaults to all base currencies.<br>
|
||||
`quote`: (str) - The quote currency filter. Defaults to all quote currencies.<br>
|
||||
`expiration`: (int) The expiration time in nanoseconds. Defaults to all.<br>
|
||||
`strike_price`: (str) The strike price to apply. Defaults to all strike prices.<br>
|
||||
`limit`: (int) The limit to query for. Defaults to 500; Max 1000.<br>
|
||||
`cursor`: (str) The cursor to use for pagination. If nil, return the first page.<br>
|
||||
Return: a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent an order state.<br>.
|
||||
"""
|
||||
self._check_account_auth()
|
||||
payload = self._get_payload_fetch_order_history(params)
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
return response
|
||||
|
||||
async def get_account_summary(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Return: The account summary.
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#account_summary)
|
||||
for details.<br>
|
||||
Returns: dictionary with account data.<br>.
|
||||
"""
|
||||
FN = f"{self._clsname} get_account_summary {type=}"
|
||||
self._check_account_auth()
|
||||
payload = {}
|
||||
if type == "sub-account":
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_SUMMARY")
|
||||
payload = {"sub_account_id": str(self._trading_account_id)}
|
||||
elif type == "funding":
|
||||
path = get_grvt_endpoint(self.env, "GET_FUNDING_ACCOUNT_SUMMARY")
|
||||
elif type == "aggregated":
|
||||
path = get_grvt_endpoint(self.env, "GET_AGGREGATED_ACCOUNT_SUMMARY")
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} Invalid account summary type {type}")
|
||||
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
sub_account: dict = response.get("result", {})
|
||||
if not sub_account:
|
||||
self.logger.info(f"{FN} No account summary for {path=} {payload=}")
|
||||
return sub_account
|
||||
|
||||
async def fetch_balance(
|
||||
self, type: Literal["sub-account", "funding", "aggregated"] = "sub-account"
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch balances for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account Summary](https://api-docs.grvt.io/trading_api/#sub-account_summary)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
type: (str) - The type of account to fetch balances for. Defaults to 'sub-account'.
|
||||
Valid values: 'sub-account', 'funding', 'aggregated'.
|
||||
|
||||
Returns: dictionary with ccxt-compliant balance data https://docs.ccxt.com/#/README?id=account-balance.<br>.
|
||||
"""
|
||||
account_summary: dict = await self.get_account_summary(type)
|
||||
return self._get_balances_from_account_summary(account_summary)
|
||||
|
||||
async def fetch_account_history(self, params: dict = {}, limit: int = 500) -> dict:
|
||||
"""
|
||||
HISTORICAL data.<br>
|
||||
Get account history.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Account History](https://api-docs.grvt.io/trading_api/#account-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
limit: maximum number of account snapshots per page to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`start_time` (int): fetch orders since this timestamp in nanoseconds.<br>
|
||||
`end_time` (int): fetch orders until this timestamp in nanoseconds.<br>
|
||||
`cursor` (str): cursor for the pagination. If cursor is present then we ignore
|
||||
`start_time` and `end_time`.<br>
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : list of account history snapshots.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_account_history(limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_ACCOUNT_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
async def fetch_positions(self, symbols: list[str] = [], params={}) -> list[dict]:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Fetch positions for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Positions](https://api-docs.grvt.io/trading_api/#positions)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbols: list(str) get positions for these symbols only.<br>
|
||||
|
||||
Returns: list of dictionaries, each dict represent a position.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_positions(symbols, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_POSITIONS")
|
||||
response: dict = await self._auth_and_post(path, payload)
|
||||
positions: list = response.get("result", [])
|
||||
if symbols:
|
||||
self.logger.info(f"fetch_positions filter positions by {symbols=}")
|
||||
positions = [p for p in positions if p.get("instrument") in symbols]
|
||||
return positions
|
||||
|
||||
async def fetch_my_trades(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
since: int | None = None,
|
||||
limit: int | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature, HISTORICAL data.<br>
|
||||
Fetch past trades for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Private Trade History](https://api-docs.grvt.io/trading_api/#private-trade-history)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
symbol: get trades for this symbol only.<br>
|
||||
since: fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns:
|
||||
a dictionary with keys:
|
||||
`total` : total number of account history snapshots.<br>
|
||||
`next` : cursor for the next page.<br>
|
||||
`result` : a list of dictionaries, each dict represent a trade.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload = self._get_payload_fetch_my_trades(symbol, since, limit, params)
|
||||
# Post payload and parse the response
|
||||
path = get_grvt_endpoint(self.env, "GET_FILL_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
if symbol:
|
||||
# filter result by symbol
|
||||
trades: list = response.get("result", [])
|
||||
trades = [t for t in trades if t.get("instrument") == symbol]
|
||||
response["result"] = trades
|
||||
return response
|
||||
|
||||
# **************** PUBLIC API CALLS
|
||||
async def load_markets(self) -> dict | None:
|
||||
self.logger.info("load_markets START")
|
||||
instruments = await self.fetch_markets(
|
||||
params={
|
||||
"kind": GrvtInstrumentKind.PERPETUAL,
|
||||
}
|
||||
)
|
||||
if instruments:
|
||||
self.markets = {i.get("instrument"): i for i in instruments}
|
||||
self.logger.info(f"load_markets: loaded {len(self.markets)} markets.")
|
||||
else:
|
||||
self.logger.warning("load_markets: No markets found.")
|
||||
return self.markets
|
||||
|
||||
async def fetch_markets(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> list[dict]:
|
||||
"""
|
||||
ccxt-compliant signature
|
||||
Retrieve the list of all instruments of matching kind, base and quote
|
||||
supported by the exchange.
|
||||
|
||||
Params: dict with keys:<br>
|
||||
`is_active` (bool) - defaults to True.<br>
|
||||
`limit` (int) - defaiults to 20.<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Default: 'USDT'.<br>
|
||||
|
||||
Returns: list of dictionaries per instrument with keys:<br>
|
||||
`instrument`: symbol e.g. 'BTC_USDT_Perp'.<br>
|
||||
`instrument_hash`: hashed symbol for order signing e.g. '0x030501'.<br>
|
||||
`base`: base currency e.g. 'BTC'.<br>
|
||||
`quote`: quote currency e.g. 'USDT'.<br>
|
||||
`kind`: kind of instrument 'PERPETUAL'/'FUTURE'.<br>
|
||||
'base_decimals': size multiplier for order signing.<br>
|
||||
`tick_size`: price tick size.<br>
|
||||
`min_size`: minimum order size.<br>
|
||||
"""
|
||||
# Prepare payload
|
||||
payload = self._get_payload_fetch_markets(params)
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENTS")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_all_markets(
|
||||
self,
|
||||
is_active: bool | None = True,
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Retrieve the list of all instruments supported by the exchange.<br>
|
||||
Params:<br>
|
||||
`is_active` (bool) - defaults to True.<br>.
|
||||
|
||||
Returns: list of dictionaries per instrument. See fetch_markets().<br>
|
||||
"""
|
||||
# Prepare payload
|
||||
payload = {"is_active": is_active}
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_ALL_INSTRUMENTS")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
# Extract and return the list of instruments
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_market(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the instrument object for a given symbol.
|
||||
:param symbol: The symbol of the instrument.
|
||||
"""
|
||||
# Make the POST request to get all instruments
|
||||
path = get_grvt_endpoint(self.env, "GET_INSTRUMENT")
|
||||
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_ticker(self, symbol: str, params: dict = {}) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature
|
||||
Retrieve the ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '59273700', 'best_ask_size': '21670', 'funding_rate_curr': 2544, 'funding_rate_avg': 0,
|
||||
# 'interest_rate': 0, 'forward_price': '0', 'buy_volume_u': '401930000000',
|
||||
# 'sell_volume_u': '1218289000000', 'buy_volume_q': '34637817515500',
|
||||
# 'sell_volume_q': '687640900', 'high_price': '343545000', 'low_price': '100000',
|
||||
# 'open_price': '32554000000000', 'open_interest': '8174350000000',
|
||||
# 'long_short_ratio': 1.0948905}
|
||||
path = get_grvt_endpoint(self.env, "GET_TICKER")
|
||||
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", {})
|
||||
|
||||
async def fetch_mini_ticker(self, symbol: str) -> dict:
|
||||
"""
|
||||
Retrieve the mini-ticker of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The mini-ticker dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '1724252426000000000', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'mark_price': '59373870996065', 'index_price': '59395287961367',
|
||||
# 'last_price': '99000000000000', 'last_size': '9917000000', 'mid_price': '59569000',
|
||||
# 'best_bid_price': '59866000000000', 'best_bid_size': '23705000000', 'best_ask_price':
|
||||
# '59273700000000', 'best_ask_size': '21678000000'}
|
||||
path = get_grvt_endpoint(self.env, "GET_MINI_TICKER")
|
||||
response: dict = await self._auth_and_post(path, payload={"instrument": symbol})
|
||||
return response.get("result", {})
|
||||
|
||||
async def fetch_order_book(self, symbol: str, limit: int = 10, params={}) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature
|
||||
Retrieve the order book of a given symbol.
|
||||
:param symbol: The instrument name.
|
||||
:return: The order book dictionary of the instrument.
|
||||
"""
|
||||
# {'event_time': '0', 'instrument': 'BTC_USDT_Perp',
|
||||
# 'bids': [{'price': '100000000', 'size': '86353000000', 'num_orders': 4},...]
|
||||
# 'asks': [{'price': '59273700000000', 'size': '21678000000', 'num_orders': 1}, ...]
|
||||
payload = {"instrument": symbol, "aggregate": 1}
|
||||
if limit:
|
||||
payload["depth"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_ORDER_BOOK")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
if self.is_order_book_ccxt_format():
|
||||
# Convert to ccxt format
|
||||
return self.convert_grvt_ob_to_ccxt(response.get("result", {}))
|
||||
return response.get("result", {})
|
||||
|
||||
async def fetch_recent_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
limit: int | None = None,
|
||||
) -> list:
|
||||
"""
|
||||
Retrieve the recent trades a given instrument.<br>
|
||||
:param instrument: The instrument name.
|
||||
:return: The order book dictionary of the instrument.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict[str, str | int] = {"instrument": symbol}
|
||||
if limit:
|
||||
payload["limit"] = limit
|
||||
path = get_grvt_endpoint(self.env, "GET_TRADES")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response.get("result", [])
|
||||
|
||||
async def fetch_trades(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int | None = None,
|
||||
limit: int = 10,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt-compliant signature, HISTORICAL data.<br>
|
||||
Retrieve trade history of a given instrument.
|
||||
:param symbol: The instrument name.
|
||||
:return: dict with field 'result' containing a list of trades.
|
||||
"""
|
||||
# List of {'event_time': '1724248876870635916', 'instrument': 'ETH_USDT_Perp',
|
||||
# 'is_taker_buyer': True, 'size': '24000000000', 'price': '2600000000000',
|
||||
# 'mark_price': '2591055564869', 'index_price': '2592459142472', 'interest_rate': 0,
|
||||
# 'forward_price': '0', 'trade_id': '729726', 'venue': 'ORDERBOOK'}
|
||||
payload: dict = self._get_payload_fetch_trades(
|
||||
symbol,
|
||||
since=since,
|
||||
limit=limit,
|
||||
params=params,
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_TRADE_HISTORY")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
async def fetch_funding_rate_history(
|
||||
self,
|
||||
symbol: str,
|
||||
since: int = 0,
|
||||
limit: int = 1_000,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature, HISTORICAL data.<br>
|
||||
Retrieve the funding rates history of a given instrument.<br>
|
||||
Args:
|
||||
symbol (str): The instrument name.<br>
|
||||
since (int): fetch trades since this timestamp in nanoseconds.<br>
|
||||
limit: int - maximum number of trades to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
Returns:
|
||||
dict with field 'result' containing list of dictionaries repesenting funding rate
|
||||
at a point in time with fields:<br>
|
||||
`instrument` (str): instrument name.<br>
|
||||
'funding_rate' (float): funding rate.<br>
|
||||
'funding_time' (int): funding time in nanoseconds.<br>
|
||||
'mark_price' (float): mark price.<br>.
|
||||
"""
|
||||
payload: dict[str, str | int] = {"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"])
|
||||
if limit:
|
||||
payload["limit"] = int(limit)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_FUNDING")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
async def fetch_ohlcv(
|
||||
self,
|
||||
symbol: str,
|
||||
timeframe: str = "1m",
|
||||
since: int = 0,
|
||||
limit: int = 10,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
ccxt-compliant signature, HISTORICAL data.<br>
|
||||
Retrieve the ohlc history of a given instrument.<br>
|
||||
Args:
|
||||
symbol: The instrument name.<br>
|
||||
timeframe: The timeframe of the ohlc.
|
||||
See `ccxt_interval_to_grvt_candlestick_interval`.<br>
|
||||
since: fetch ohlc since this timestamp in nanoseconds.<br>
|
||||
limit: maximum number of ohlc to fetch.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`cursor` (str): cursor for the pagination.
|
||||
If cursor is present then we ignore other filters.<br>
|
||||
`end_time` (int): end time in nanoseconds.<br>
|
||||
`candle_type` (str): candle type. Valid values: 'TRADE', 'MARK', 'INDEX'.<br>
|
||||
Returns:
|
||||
dict with field 'result' containing a list of dictionaries, each dict representing a candlestick with fields:<br>
|
||||
`instrument` - instrument name.<br>
|
||||
`open_time` - start of interval in nanoseconds.<br>
|
||||
`close_time` - end of interval in nanoseconds.<br>
|
||||
`open` - opening price.<br>
|
||||
`close` - closing price.<br>
|
||||
`high` - highest price.<br>
|
||||
`low` - lowest price.<br>
|
||||
`volume_u` - volume in units.<br>
|
||||
`volume_q` - volume in quote(USDT).<br>
|
||||
`trades` - number of trades.<br>.
|
||||
"""
|
||||
FN: str = f"{self._clsname} fetch_ohlcv"
|
||||
payload: dict = self._get_payload_fetch_ohlcv(symbol, timeframe, since, limit, params)
|
||||
self.logger.info(f"{FN} {payload=}")
|
||||
path: str = get_grvt_endpoint(self.env, "GET_CANDLESTICK")
|
||||
response: dict = await self._auth_and_post(path, payload=payload)
|
||||
return response
|
||||
|
||||
# Vault Management APIs
|
||||
async def fetch_vault_manager_investor_history(self, only_own_investments: bool = False) -> dict:
|
||||
payload: dict = self._get_fetch_vault_manager_investor_history_payload(
|
||||
vault_id=self.get_trading_account_id(),
|
||||
only_own_investments=only_own_investments, # Default to False to fetch all investments
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_MANAGER_INVESTOR_HISTORY")
|
||||
return await self._auth_and_post(path, payload=payload)
|
||||
|
||||
async def fetch_vault_redemption_queue(self):
|
||||
payload: dict = self._get_fetch_vault_redemption_queue_payload(
|
||||
vault_id=self.get_trading_account_id()
|
||||
)
|
||||
path: str = get_grvt_endpoint(self.env, "GET_VAULT_REDEMPTION_QUEUE")
|
||||
return await self._auth_and_post(path, payload=payload)
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
|
||||
from .grvt_ccxt import GrvtCcxt
|
||||
from .grvt_ccxt_env import get_all_grvt_endpoints
|
||||
from .grvt_ccxt_pro import GrvtCcxtPro
|
||||
|
||||
|
||||
def default_check(return_value: dict) -> str:
|
||||
if not isinstance(return_value, list | dict):
|
||||
return "return_value is not a list or dict"
|
||||
if not return_value:
|
||||
return "return_value is empty"
|
||||
return "OK"
|
||||
|
||||
|
||||
def validate_return_values(api: GrvtCcxt | GrvtCcxtPro, result_filename: str) -> None:
|
||||
logging.info("validate_return_values: START")
|
||||
endpoint_check_map: dict[str, Callable] = {
|
||||
"GRAPHQL": default_check,
|
||||
"AUTH": default_check,
|
||||
"CREATE_ORDER": default_check,
|
||||
"CANCEL_ALL_ORDERS": default_check,
|
||||
"CANCEL_ORDER": default_check,
|
||||
"GET_OPEN_ORDERS": default_check,
|
||||
"GET_ACCOUNT_SUMMARY": default_check,
|
||||
"GET_FUNDING_ACCOUNT_SUMMARY": default_check,
|
||||
"GET_AGGREGATED_ACCOUNT_SUMMARY": default_check,
|
||||
"GET_ACCOUNT_HISTORY": default_check,
|
||||
"GET_POSITIONS": default_check,
|
||||
"GET_ORDER": default_check,
|
||||
"GET_ORDER_HISTORY": default_check,
|
||||
"GET_FILL_HISTORY": default_check,
|
||||
"GET_ALL_INSTRUMENTS": default_check,
|
||||
"GET_INSTRUMENTS": default_check,
|
||||
"GET_INSTRUMENT": default_check,
|
||||
"GET_TICKER": default_check,
|
||||
"GET_MINI_TICKER": default_check,
|
||||
"GET_ORDER_BOOK": default_check,
|
||||
"GET_TRADES": default_check,
|
||||
"GET_TRADE_HISTORY": default_check,
|
||||
"GET_FUNDING": default_check,
|
||||
"GET_CANDLESTICK": default_check,
|
||||
}
|
||||
all_endpoints = get_all_grvt_endpoints(api.env)
|
||||
end_point_status = {}
|
||||
for short_name, endpoint in all_endpoints.items():
|
||||
if not api.was_path_called(endpoint):
|
||||
logging.info(f"validate_return_values: {short_name=}, {endpoint=}, not called")
|
||||
end_point_status[short_name] = [endpoint, "not called"]
|
||||
else:
|
||||
return_value = api.get_endpoint_return_value(endpoint)
|
||||
if short_name in endpoint_check_map:
|
||||
check_function = endpoint_check_map.get(short_name)
|
||||
if not check_function or not callable(check_function):
|
||||
logging.error(
|
||||
f"validate_return_values: {short_name=} "
|
||||
f"not found in {endpoint_check_map.keys()=}"
|
||||
)
|
||||
continue
|
||||
check_result = check_function(return_value)
|
||||
logging.info(
|
||||
f"validate_return_values: {short_name=}, {endpoint=}, {check_result=}"
|
||||
)
|
||||
end_point_status[short_name] = [endpoint, check_result]
|
||||
else:
|
||||
logging.error(
|
||||
f"validate_return_values: NO {short_name=} in {endpoint_check_map.keys()=}"
|
||||
)
|
||||
end_point_status[short_name] = [endpoint, "no check"]
|
||||
with open(result_filename, "w") as file_handle:
|
||||
file_handle.write("NAME, URL, STATUS\n")
|
||||
for short_name, status in end_point_status.items():
|
||||
file_handle.write(f"{short_name}, {status[0]}, {status[1]}\n")
|
||||
logging.info("validate_return_values: END")
|
||||
@@ -0,0 +1,81 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
Num = None | str | float | int | Decimal
|
||||
Amount = Decimal | int | float | str
|
||||
GrvtOrderSide = Literal["buy", "sell"]
|
||||
GrvtOrderType = Literal["limit", "market"]
|
||||
|
||||
DURATION_SECOND_IN_NSEC = 1_000_000_000
|
||||
PRICE_MULTIPLIER = 1_000_000_000
|
||||
BTC_ETH_SIZE_MULTIPLIER = 1_000_000_000
|
||||
|
||||
|
||||
class GrvtInvalidOrder(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CandlestickInterval(Enum):
|
||||
CI_1_M = "CI_1_M"
|
||||
CI_3_M = "CI_3_M"
|
||||
CI_5_M = "CI_5_M"
|
||||
CI_15_M = "CI_15_M"
|
||||
CI_30_M = "CI_30_M"
|
||||
CI_1_H = "CI_1_H"
|
||||
CI_2_H = "CI_2_H"
|
||||
CI_4_H = "CI_4_H"
|
||||
CI_6_H = "CI_6_H"
|
||||
CI_8_H = "CI_8_H"
|
||||
CI_12_H = "CI_12_H"
|
||||
CI_1_D = "CI_1_D"
|
||||
CI_3_D = "CI_3_D"
|
||||
CI_5_D = "CI_5_D"
|
||||
CI_1_W = "CI_1_W"
|
||||
CI_2_W = "CI_2_W"
|
||||
CI_3_W = "CI_3_W"
|
||||
CI_4_W = "CI_4_W"
|
||||
|
||||
|
||||
ccxt_interval_to_grvt_candlestick_interval = {
|
||||
"1m": CandlestickInterval.CI_1_M,
|
||||
"3m": CandlestickInterval.CI_3_M,
|
||||
"5m": CandlestickInterval.CI_5_M,
|
||||
"15m": CandlestickInterval.CI_15_M,
|
||||
"30m": CandlestickInterval.CI_30_M,
|
||||
"1h": CandlestickInterval.CI_1_H,
|
||||
"2h": CandlestickInterval.CI_2_H,
|
||||
"4h": CandlestickInterval.CI_4_H,
|
||||
"6h": CandlestickInterval.CI_6_H,
|
||||
"8h": CandlestickInterval.CI_8_H,
|
||||
"12h": CandlestickInterval.CI_12_H,
|
||||
"1d": CandlestickInterval.CI_1_D,
|
||||
"3d": CandlestickInterval.CI_3_D,
|
||||
"5d": CandlestickInterval.CI_5_D,
|
||||
"1w": CandlestickInterval.CI_1_W,
|
||||
"2w": CandlestickInterval.CI_2_W,
|
||||
"3w": CandlestickInterval.CI_3_W,
|
||||
"4w": CandlestickInterval.CI_4_W,
|
||||
}
|
||||
|
||||
|
||||
class CandlestickType(Enum):
|
||||
TRADE = "TRADE"
|
||||
MARK = "MARK"
|
||||
INDEX = "INDEX"
|
||||
MID = "MID"
|
||||
|
||||
|
||||
class GrvtInstrumentKind(Enum):
|
||||
PERPETUAL = "PERPETUAL"
|
||||
FUTURE = "FUTURE"
|
||||
CALL = "CALL"
|
||||
PUT = "PUT"
|
||||
@@ -0,0 +1,544 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from http.cookies import SimpleCookie
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_typed_data, SignableMessage
|
||||
|
||||
from .grvt_ccxt_env import CHAIN_IDS, GrvtEnv
|
||||
from .grvt_ccxt_types import (
|
||||
BTC_ETH_SIZE_MULTIPLIER,
|
||||
DURATION_SECOND_IN_NSEC,
|
||||
Amount,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
|
||||
|
||||
def rand_uint32():
|
||||
return random.randint(0, 2**32 - 1)
|
||||
|
||||
|
||||
class TimeInForce(Enum):
|
||||
"""
|
||||
| | Must Fill All | Can Fill Partial |
|
||||
| - | - | - |
|
||||
| Must Fill Immediately | FOK | IOC |
|
||||
| Can Fill Till Time | AON | GTC |.
|
||||
|
||||
"""
|
||||
|
||||
# GTT - Remains open until it is cancelled, or expired
|
||||
GOOD_TILL_TIME = "GOOD_TILL_TIME"
|
||||
# AON - Either fill the whole order or none of it (Block Trades Only)
|
||||
ALL_OR_NONE = "ALL_OR_NONE"
|
||||
# IOC - Fill the order as much as possible, when hitting the orderbook. Then cancel it
|
||||
IMMEDIATE_OR_CANCEL = "IMMEDIATE_OR_CANCEL"
|
||||
# FOK - Both AoN and IoC. Either fill the full order when hitting the orderbook, or cancel it
|
||||
FILL_OR_KILL = "FILL_OR_KILL"
|
||||
|
||||
|
||||
class SignTimeInForce(Enum):
|
||||
GOOD_TILL_TIME = 1
|
||||
ALL_OR_NONE = 2
|
||||
IMMEDIATE_OR_CANCEL = 3
|
||||
FILL_OR_KILL = 4
|
||||
|
||||
|
||||
TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = {
|
||||
TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME,
|
||||
TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE,
|
||||
TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL,
|
||||
TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL,
|
||||
}
|
||||
|
||||
|
||||
def get_EIP712_domain_data(env: GrvtEnv) -> dict[str, str | int]:
|
||||
# DO NOT MODIFY THESE VALUES ##############
|
||||
return {
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": CHAIN_IDS[env.value],
|
||||
}
|
||||
|
||||
|
||||
def get_cookie_with_expiration(
|
||||
path: str, api_key: str | None
|
||||
) -> dict[str, str | float | None] | None:
|
||||
"""
|
||||
Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token.
|
||||
:return: The session cookie.
|
||||
"""
|
||||
FN = f"get_cookie_with_expiration {path=}"
|
||||
if api_key:
|
||||
data = {}
|
||||
try:
|
||||
data = {"api_key": api_key}
|
||||
session = requests.Session()
|
||||
return_value = session.post(
|
||||
path,
|
||||
json=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
)
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(return_value.headers.get("Set-Cookie", ""))
|
||||
cookie_value: str = cookie["gravity"].value
|
||||
cookie_expiry: datetime = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "")
|
||||
logging.info(
|
||||
f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}"
|
||||
)
|
||||
return {
|
||||
"gravity": cookie_value,
|
||||
"expires": cookie_expiry.timestamp(),
|
||||
"X-Grvt-Account-Id": grvt_account_id,
|
||||
}
|
||||
logging.warning(f"{FN} Invalid return_value {data=} {path=} {return_value=}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
async def get_cookie_with_expiration_async(
|
||||
path: str, api_key: str | None
|
||||
) -> dict[str, str | float | None] | None:
|
||||
"""
|
||||
Authenticates and retrieves the session cookie, its expiration time and grvt-account-id token.
|
||||
:return: The session cookie.
|
||||
"""
|
||||
FN = f"get_cookie_with_expiration_async {path=}"
|
||||
if api_key:
|
||||
data = {}
|
||||
try:
|
||||
data = {"api_key": api_key}
|
||||
logging.info(f"{FN} ask for cookie {path=} {data=}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url=path, json=data, timeout=5) as return_value:
|
||||
logging.info(f"{FN} {return_value=}")
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(return_value.headers.get("Set-Cookie", ""))
|
||||
cookie_value: str = cookie["gravity"].value
|
||||
cookie_expiry: datetime = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str = return_value.headers.get("X-Grvt-Account-Id", "")
|
||||
logging.info(
|
||||
f"{FN} OK response {cookie_value=} {cookie_expiry=} {grvt_account_id=}"
|
||||
)
|
||||
return {
|
||||
"gravity": cookie_value,
|
||||
"expires": cookie_expiry.timestamp(),
|
||||
"X-Grvt-Account-Id": grvt_account_id,
|
||||
}
|
||||
except Exception as e:
|
||||
logging.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class GrvtKind(Enum):
|
||||
PERPETUAL = 1
|
||||
FUTURE = 2
|
||||
CALL = 3
|
||||
PUT = 4
|
||||
SPOT = 5
|
||||
|
||||
|
||||
class GrvtCurrency(Enum):
|
||||
USD = 1
|
||||
USDC = 2
|
||||
USDT = 3
|
||||
ETH = 4
|
||||
BTC = 5
|
||||
|
||||
|
||||
def hexlify(data: bytes) -> str:
|
||||
"""Convert a byte array to a hex string with a 0x prefix."""
|
||||
return f"0x{data.hex()}"
|
||||
|
||||
|
||||
class EnumEncoder(json.JSONEncoder):
|
||||
def default(self, o):
|
||||
"""
|
||||
Custom JSON encoder for Enum types.
|
||||
:param obj: Object to serialize.
|
||||
:return: Serialized object.
|
||||
"""
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
|
||||
|
||||
def get_kuq_from_symbol(symbol: str) -> tuple[str, str, str]:
|
||||
parts = symbol.split("_")
|
||||
if len(parts) == 3:
|
||||
underlying, quote, kind = parts
|
||||
if kind == "Perp":
|
||||
kind = "PERPETUAL"
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=} {kind=}")
|
||||
elif len(parts) == 4:
|
||||
underlying, quote, kind, time_str = parts
|
||||
if kind == "Fut":
|
||||
kind = "FUTURE"
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=} {kind=}")
|
||||
elif len(parts) == 5:
|
||||
underlying, quote, kind, time_str, strike_price = parts
|
||||
if kind in {"Call", "Put"}:
|
||||
kind = kind.upper()
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=} {kind=}")
|
||||
else:
|
||||
raise ValueError(f"Invalid {symbol=}")
|
||||
return kind, underlying, quote
|
||||
|
||||
|
||||
# Custom types
|
||||
EIP712_ORDER_MESSAGE_TYPE = {
|
||||
"Order": [
|
||||
{"name": "subAccountID", "type": "uint64"},
|
||||
{"name": "isMarket", "type": "bool"},
|
||||
{"name": "timeInForce", "type": "uint8"},
|
||||
{"name": "postOnly", "type": "bool"},
|
||||
{"name": "reduceOnly", "type": "bool"},
|
||||
{"name": "legs", "type": "OrderLeg[]"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
"OrderLeg": [
|
||||
{"name": "assetID", "type": "uint256"},
|
||||
{"name": "contractSize", "type": "uint64"},
|
||||
{"name": "limitPrice", "type": "uint64"},
|
||||
{"name": "isBuyingContract", "type": "bool"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtSignature:
|
||||
# The address (public key) of the wallet signing the payload
|
||||
signer: str
|
||||
r: str
|
||||
s: str
|
||||
v: int
|
||||
# Timestamp after which this signature expires, expressed in unix nanoseconds.
|
||||
# Must be capped at 30 days
|
||||
expiration: str
|
||||
"""
|
||||
Users can randomly generate this value, used as a signature deconflicting key.
|
||||
ie. You can send the same exact instruction twice with different nonces.
|
||||
When the same nonce is used, the same payload will generate the same signature.
|
||||
Our system will consider the payload a duplicate, and ignore it.
|
||||
"""
|
||||
nonce: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class OrderMetadata:
|
||||
"""
|
||||
Metadata fields are used to support Backend only operations.
|
||||
Hence, fields in here are never signed, and is never transmitted to the smart contract.
|
||||
"""
|
||||
|
||||
"""
|
||||
`client_order_id`: A unique identifier of an active order, specified by the client
|
||||
This is used to identify the order in the client's system
|
||||
This value must be unique for all active orders in a subaccount,
|
||||
otehrwise amendment / cancellation will not work as expected
|
||||
Gravity UI will generate a random clientOrderID for each order in the range [0, 2^63 - 1]
|
||||
To prevent any conflicts, client machines should generate a random clientOrderID
|
||||
in the range [2^63, 2^64 - 1].
|
||||
When GRVT Backend receives an order with duplicate `client_order_id`, it will reject the order
|
||||
with rejectReason set to duplicate `client_order_id`.
|
||||
"""
|
||||
client_order_id: str
|
||||
# [Filled by GRVT Backend] Time at which the order was received by GRVT in unix nanoseconds
|
||||
create_time: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtOrderLeg:
|
||||
# The instrument to trade in this leg
|
||||
instrument: str
|
||||
# The total number of contracts to trade in this leg, expressed in base currency units.
|
||||
size: Decimal
|
||||
# Specifies if the order leg is a buy or sell
|
||||
is_buying_asset: bool
|
||||
"""
|
||||
The limit price of the order leg, expressed in `9` decimals.
|
||||
This is the number of quote currency units to pay/receive for this leg.
|
||||
This should be `null/0` if the order is a market order
|
||||
"""
|
||||
limit_price: Decimal
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtOrder:
|
||||
"""
|
||||
Order is a typed payload used throughout the GRVT platform to express all orders.
|
||||
GRVT orders are capable of expressing both single-legged, and multi-legged orders by default.
|
||||
All fields in the Order payload (except `id`, `metadata`, and `state`) are trustlessly enforced
|
||||
on our Hyperchain.
|
||||
This minimizes the amount of trust users have to offer to GRVT.
|
||||
"""
|
||||
|
||||
# The subaccount initiating the order
|
||||
sub_account_id: str
|
||||
# Supported time_in_force : GTT, IOC, FOK:<ul>
|
||||
time_in_force: TimeInForce
|
||||
legs: list[GrvtOrderLeg]
|
||||
# The signature approving this order
|
||||
signature: GrvtSignature
|
||||
# Order Metadata, ignored by the smart contract, and unsigned by the client
|
||||
metadata: OrderMetadata
|
||||
# is_market: If the order is a market order
|
||||
is_market: bool
|
||||
post_only: bool = False
|
||||
# If True, Order must reduce the position size, or be cancelled
|
||||
reduce_only: bool = False
|
||||
|
||||
|
||||
def get_signable_message(
|
||||
order: GrvtOrder, env: GrvtEnv, instruments: dict[str, dict]
|
||||
) -> bytes | None:
|
||||
FN = f"get_signable_message {order=}"
|
||||
size_multiplier = BTC_ETH_SIZE_MULTIPLIER
|
||||
PRICE_MULTIPLIER = 1_000_000_000
|
||||
legs = []
|
||||
for leg in order.legs:
|
||||
instrument = instruments.get(leg.instrument)
|
||||
if not instrument or not isinstance(instrument, dict):
|
||||
logging.error(f"{FN}: {leg.instrument=} not found in {instruments=}")
|
||||
return None
|
||||
if "base_decimals" not in instrument:
|
||||
logging.error(f"{FN}: no 'base_decimals' in {instrument=}")
|
||||
return None
|
||||
size_multiplier = 10 ** instrument["base_decimals"]
|
||||
if "instrument_hash" not in instrument:
|
||||
logging.error(f"{FN}: no 'instrument_hash' in {instrument=}")
|
||||
return None
|
||||
legs.append(
|
||||
{
|
||||
"assetID": instrument["instrument_hash"],
|
||||
"contractSize": int(Decimal(leg.size) * Decimal(size_multiplier)),
|
||||
"limitPrice": int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER)),
|
||||
"isBuyingContract": leg.is_buying_asset,
|
||||
}
|
||||
)
|
||||
message_data = {
|
||||
"subAccountID": order.sub_account_id,
|
||||
"isMarket": order.is_market or False,
|
||||
"timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value,
|
||||
"postOnly": order.post_only or False,
|
||||
"reduceOnly": order.reduce_only or False,
|
||||
"legs": legs,
|
||||
"nonce": order.signature.nonce,
|
||||
"expiration": order.signature.expiration,
|
||||
}
|
||||
domain_data: dict[str, str | int]= get_EIP712_domain_data(env)
|
||||
logging.info(f"{FN} {domain_data=}\n{EIP712_ORDER_MESSAGE_TYPE=}\n{message_data=}")
|
||||
return encode_typed_data(domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data)
|
||||
|
||||
|
||||
def get_order_payload(
|
||||
order: GrvtOrder, private_key: str, env: GrvtEnv, instruments: dict[str, dict]
|
||||
) -> dict:
|
||||
signable_message = get_signable_message(order, env, instruments)
|
||||
if signable_message is None:
|
||||
raise ValueError("Failed to create signable message")
|
||||
signed_message = Account.sign_message(signable_message, private_key)
|
||||
order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.v = signed_message.v
|
||||
order.signature.signer = Account.from_key(private_key).address
|
||||
|
||||
return {
|
||||
"order": {
|
||||
"sub_account_id": str(order.sub_account_id),
|
||||
"is_market": order.is_market,
|
||||
"time_in_force": order.time_in_force.name,
|
||||
"post_only": order.post_only,
|
||||
"reduce_only": order.reduce_only,
|
||||
"legs": [
|
||||
{
|
||||
"instrument": leg.instrument,
|
||||
"size": str(leg.size),
|
||||
"limit_price": str(leg.limit_price),
|
||||
"is_buying_asset": bool(leg.is_buying_asset),
|
||||
}
|
||||
for leg in order.legs
|
||||
],
|
||||
"signature": {
|
||||
"r": order.signature.r,
|
||||
"s": order.signature.s,
|
||||
"v": order.signature.v,
|
||||
"expiration": order.signature.expiration,
|
||||
"nonce": order.signature.nonce,
|
||||
"signer": order.signature.signer,
|
||||
},
|
||||
"metadata": {
|
||||
"client_order_id": order.metadata.client_order_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_order_rpc_payload(
|
||||
order: GrvtOrder,
|
||||
private_key: str,
|
||||
env: GrvtEnv,
|
||||
instruments: dict[str, dict],
|
||||
version: str = "v1",
|
||||
) -> dict:
|
||||
order_payload = get_order_payload(order, private_key, env, instruments)
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": f"{version}/create_order",
|
||||
"params": order_payload,
|
||||
}
|
||||
|
||||
|
||||
def get_grvt_order(
|
||||
sub_account_id: str,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: Amount,
|
||||
limit_price: Num,
|
||||
order_duration_secs: float = 5 * 60,
|
||||
params: dict = {},
|
||||
) -> GrvtOrder:
|
||||
"""
|
||||
Creates an order for a specified symbol with the given limit price and size.
|
||||
|
||||
Args:
|
||||
symbol .
|
||||
limit_price (int): The limit price for the order.
|
||||
size (float): The size of the order.
|
||||
is_buying_asset(bool) : Buy or Sell.
|
||||
|
||||
Returns:
|
||||
Order: The created perpetual order.
|
||||
"""
|
||||
limit_price = limit_price or 0
|
||||
is_buying_asset = side == "buy"
|
||||
is_market = order_type == "market"
|
||||
leg = GrvtOrderLeg(
|
||||
instrument=symbol,
|
||||
size=round(Decimal(amount), 9),
|
||||
is_buying_asset=is_buying_asset,
|
||||
limit_price=round(Decimal(limit_price), 9),
|
||||
)
|
||||
|
||||
# create an expiry time
|
||||
time_in_force = TimeInForce.GOOD_TILL_TIME
|
||||
if "time_in_force" in params:
|
||||
time_in_force = TimeInForce[params["time_in_force"]]
|
||||
post_only: bool = False
|
||||
if "post_only" in params:
|
||||
post_only = params["post_only"]
|
||||
reduce_only: bool = False
|
||||
if "reduce_only" in params:
|
||||
reduce_only = params["reduce_only"]
|
||||
expiry_ns: int = 0
|
||||
if order_duration_secs:
|
||||
expiry_ns = time.time_ns() + int(order_duration_secs * DURATION_SECOND_IN_NSEC)
|
||||
if "client_order_id" in params:
|
||||
client_order_id = int(params["client_order_id"])
|
||||
else:
|
||||
client_order_id = rand_uint32()
|
||||
signature = GrvtSignature(
|
||||
signer="",
|
||||
r="",
|
||||
s="",
|
||||
v=0,
|
||||
expiration=str(expiry_ns),
|
||||
nonce=rand_uint32(),
|
||||
)
|
||||
metadata = OrderMetadata(client_order_id=str(client_order_id))
|
||||
return GrvtOrder(
|
||||
sub_account_id=sub_account_id,
|
||||
time_in_force=time_in_force,
|
||||
legs=[leg],
|
||||
signature=signature,
|
||||
metadata=metadata,
|
||||
is_market=is_market,
|
||||
post_only=post_only,
|
||||
reduce_only=reduce_only,
|
||||
)
|
||||
|
||||
def sign_derisk_mm_ratio_request(
|
||||
env: GrvtEnv, sub_account_id: int, ratio: str, private_key_hex: str
|
||||
):
|
||||
"""
|
||||
Generate a signature for setting the derisk to maintenance margin ratio.
|
||||
|
||||
:param sub_account_id: The sub-account ID to set the ratio for.
|
||||
:param ratio: The derisk to maintenance margin ratio as a string (e.g., "2.0").
|
||||
:param private_key_hex: The private key in hexadecimal format.
|
||||
:return: A dictionary containing the signature for the payload.
|
||||
"""
|
||||
derisk_ratio_int = int(Decimal(ratio) * 1_000_000)
|
||||
expiration_ns = int((time.time() + 86400) * 1_000_000_000)
|
||||
nonce = random.randint(1, 2**32 - 1)
|
||||
|
||||
domain_data = get_EIP712_domain_data(env)
|
||||
|
||||
types = {
|
||||
"SetDeriskToMaintenanceMarginRatio": [
|
||||
{"name": "subAccountID", "type": "uint64"},
|
||||
{"name": "deriskToMaintenanceMarginRatio", "type": "uint32"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
]
|
||||
}
|
||||
|
||||
signature_payload = {
|
||||
"subAccountID": sub_account_id,
|
||||
"deriskToMaintenanceMarginRatio": derisk_ratio_int,
|
||||
"nonce": nonce,
|
||||
"expiration": expiration_ns,
|
||||
}
|
||||
|
||||
message = encode_typed_data(domain_data, types, signature_payload)
|
||||
signed = Account.sign_message(message, private_key_hex)
|
||||
signer = Account.from_key(private_key_hex)
|
||||
|
||||
return {
|
||||
"signer": signer.address.lower(),
|
||||
"r": hex(signed.r),
|
||||
"s": hex(signed.s),
|
||||
"v": signed.v,
|
||||
"expiration": str(expiration_ns),
|
||||
"nonce": nonce,
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
# ruff: noqa: D200
|
||||
# ruff: noqa: D204
|
||||
# ruff: noqa: D205
|
||||
# ruff: noqa: D404
|
||||
# ruff: noqa: W291
|
||||
# ruff: noqa: D400
|
||||
# ruff: noqa: E501
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import traceback
|
||||
from asyncio.events import AbstractEventLoop
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
|
||||
import websockets
|
||||
|
||||
# import requests
|
||||
# from env import ENDPOINTS
|
||||
from .grvt_ccxt_env import (
|
||||
GRVT_WS_STREAMS,
|
||||
GrvtEnv,
|
||||
GrvtWSEndpointType,
|
||||
get_grvt_ws_endpoint,
|
||||
is_trading_ws_endpoint,
|
||||
)
|
||||
from .grvt_ccxt_pro import GrvtCcxtPro
|
||||
from .grvt_ccxt_types import (
|
||||
GrvtInvalidOrder,
|
||||
GrvtOrderSide,
|
||||
GrvtOrderType,
|
||||
Num,
|
||||
)
|
||||
from .grvt_ccxt_utils import get_order_rpc_payload
|
||||
|
||||
WS_READ_TIMEOUT = 5
|
||||
|
||||
|
||||
class GrvtCcxtWS(GrvtCcxtPro):
|
||||
"""
|
||||
GrvtCcxtPro class to interact with Grvt Rest API and WebSockets in asynchronous mode.
|
||||
|
||||
Args:
|
||||
env: GrvtCcxtPro (DEV, TESTNET, PROD)
|
||||
parameters: dict with trading_account_id, private_key, api_key etc
|
||||
|
||||
Examples:
|
||||
>>> from grvt_api_pro import GrvtCcxtPro
|
||||
>>> from grvt_env import GrvtEnv
|
||||
>>> grvt = GrvtCcxtPro(env=GrvtEnv.TESTNET)
|
||||
>>> await grvt.fetch_markets()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
env: GrvtEnv,
|
||||
loop: AbstractEventLoop,
|
||||
logger: logging.Logger | None = None,
|
||||
parameters: dict = {},
|
||||
):
|
||||
"""Initialize the GrvtCcxt instance."""
|
||||
super().__init__(env, logger, parameters)
|
||||
self._loop = loop
|
||||
self._clsname: str = type(self).__name__
|
||||
self.api_ws_version = parameters.get("api_ws_version", "v1")
|
||||
self.force_reconnect_flag: bool = False
|
||||
self.ws: dict[GrvtWSEndpointType, websockets.WebSocketClientProtocol | None] = {}
|
||||
self.callbacks: dict[GrvtWSEndpointType, dict[str, dict[str, Callable]]] = {}
|
||||
self.subscribed_streams: dict[GrvtWSEndpointType, dict] = {}
|
||||
self.api_url: dict[GrvtWSEndpointType, str] = {}
|
||||
self._last_message: dict[str, dict] = {}
|
||||
self._request_id = 0
|
||||
self.endpoint_types = [
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]
|
||||
# Initialize dictionaries for each endpoint type
|
||||
for grvt_endpoint_type in self.endpoint_types:
|
||||
self.api_url[grvt_endpoint_type] = get_grvt_ws_endpoint(
|
||||
self.env.value, grvt_endpoint_type
|
||||
)
|
||||
self.callbacks[grvt_endpoint_type] = {}
|
||||
self.subscribed_streams[grvt_endpoint_type] = {}
|
||||
self.ws[grvt_endpoint_type] = None
|
||||
self._loop.create_task(self._read_messages(grvt_endpoint_type))
|
||||
self.logger.info(f"{self._clsname} initialized {self.api_url=}")
|
||||
self.logger.info(f"{self._clsname} initialized {self.ws=}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self._clsname} {self.env=} {self.api_ws_version=}"
|
||||
|
||||
async def __aexit__(self):
|
||||
for grvt_endpoint_type in self.endpoint_types:
|
||||
await self._close_connection(grvt_endpoint_type)
|
||||
|
||||
def force_reconnect(self) -> None:
|
||||
self.force_reconnect_flag = True
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Prepares the GrvtCcxtPro instance and connects to WS server.
|
||||
"""
|
||||
await self.load_markets()
|
||||
await self.refresh_cookie()
|
||||
self._loop.create_task(self.connect_all_channels())
|
||||
|
||||
def is_connection_open(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
|
||||
return (
|
||||
self.ws[grvt_endpoint_type] is not None and self.ws[grvt_endpoint_type].open
|
||||
)
|
||||
|
||||
def is_endpoint_connected(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
|
||||
"""
|
||||
For MARKET_DATA returns True if connection is open.
|
||||
for TRADE_DATA returns True if one of the following is true:
|
||||
1. No cookie - this means this is public connection and we can't connect to TRADE_DATA
|
||||
2. Connection to TRADE_DATA is open
|
||||
"""
|
||||
if grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
]:
|
||||
return self.is_connection_open(grvt_endpoint_type)
|
||||
if grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]:
|
||||
return bool(not self._cookie or self.is_connection_open(grvt_endpoint_type))
|
||||
raise ValueError(f"Unknown endpoint type {grvt_endpoint_type}")
|
||||
|
||||
def are_endpoints_connected(
|
||||
self, grvt_endpoint_types: list[GrvtWSEndpointType]
|
||||
) -> bool:
|
||||
return all(
|
||||
self.is_endpoint_connected(endpoint) for endpoint in grvt_endpoint_types
|
||||
)
|
||||
|
||||
async def connect_all_channels(self) -> None:
|
||||
"""
|
||||
Connects to all channels that are possible to connect.
|
||||
If cookie is NOT available, it will NOT connect to GrvtWSEndpointType.TRADE_DATA
|
||||
For trading connection: run this method after cookie is available.
|
||||
"""
|
||||
FN = "connect_all_channels"
|
||||
while True:
|
||||
try:
|
||||
for end_point_type in self.endpoint_types:
|
||||
if (
|
||||
not self.is_endpoint_connected(end_point_type)
|
||||
or self.force_reconnect_flag
|
||||
):
|
||||
await self._reconnect(end_point_type)
|
||||
all_are_connected = self.are_endpoints_connected(self.endpoint_types)
|
||||
self.logger.info(
|
||||
f"{FN} Connection status: {all_are_connected=} {self.force_reconnect_flag=}"
|
||||
)
|
||||
self.force_reconnect_flag = False
|
||||
except Exception as e:
|
||||
self.logger.exception(f"{FN} {e=}")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def connect_channel(self, grvt_endpoint_type: GrvtWSEndpointType) -> bool:
|
||||
FN = f"{self._clsname} connect_channel {grvt_endpoint_type}"
|
||||
try:
|
||||
if self.is_endpoint_connected(grvt_endpoint_type):
|
||||
self.logger.info(f"{FN} Already connected")
|
||||
return True
|
||||
self.subscribed_streams[grvt_endpoint_type] = {}
|
||||
extra_headers = {}
|
||||
if self._cookie:
|
||||
extra_headers = {"Cookie": f"gravity={self._cookie['gravity']}"}
|
||||
if self._cookie["X-Grvt-Account-Id"]:
|
||||
extra_headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie["X-Grvt-Account-Id"]}
|
||||
)
|
||||
if grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL,
|
||||
]:
|
||||
if self._cookie:
|
||||
self.ws[grvt_endpoint_type] = await websockets.connect(
|
||||
uri=self.api_url[grvt_endpoint_type],
|
||||
extra_headers=extra_headers,
|
||||
logger=self.logger,
|
||||
open_timeout=5,
|
||||
)
|
||||
self.logger.info(
|
||||
f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}"
|
||||
)
|
||||
else:
|
||||
self.logger.info(f"{FN} Waiting for cookie.")
|
||||
elif grvt_endpoint_type in [
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA_RPC_FULL,
|
||||
]:
|
||||
self.ws[grvt_endpoint_type] = await websockets.connect(
|
||||
uri=self.api_url[grvt_endpoint_type],
|
||||
extra_headers=extra_headers,
|
||||
logger=self.logger,
|
||||
open_timeout=5,
|
||||
)
|
||||
self.logger.info(f"{FN} Connected to {self.api_url[grvt_endpoint_type]} {extra_headers=}")
|
||||
except (
|
||||
websockets.exceptions.ConnectionClosedOK,
|
||||
websockets.exceptions.ConnectionClosed,
|
||||
) as e:
|
||||
self.logger.info(f"{FN} connection already closed:{e}")
|
||||
self.ws[grvt_endpoint_type] = None
|
||||
except Exception as e:
|
||||
self.logger.warning(f"{FN} error:{e} traceback:{traceback.format_exc()}")
|
||||
self.ws[grvt_endpoint_type] = None
|
||||
# return True if connection successful
|
||||
return self.is_endpoint_connected(grvt_endpoint_type)
|
||||
|
||||
async def _close_connection(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
try:
|
||||
if self.ws[grvt_endpoint_type]:
|
||||
self.logger.info(f"{self._clsname} Closing connection...")
|
||||
await self.ws[grvt_endpoint_type].close()
|
||||
self.subscribed_streams[grvt_endpoint_type] = {}
|
||||
self.logger.info(f"{self._clsname} Connection closed")
|
||||
else:
|
||||
self.logger.info(f"{self._clsname} No connection to close")
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
f"{self._clsname} Error when closing connection {traceback.format_exc()}"
|
||||
)
|
||||
|
||||
async def _reconnect(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
FN = f"{self._clsname} _reconnect {grvt_endpoint_type=}"
|
||||
try:
|
||||
self.logger.info(f"{FN} STARTS")
|
||||
await self._close_connection(grvt_endpoint_type)
|
||||
success: bool = await self.connect_channel(grvt_endpoint_type)
|
||||
if success:
|
||||
await self._resubscribe(grvt_endpoint_type)
|
||||
except Exception:
|
||||
self.logger.exception(f"{FN} failed {traceback.format_exc()}")
|
||||
|
||||
async def _resubscribe(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
if self.is_connection_open(grvt_endpoint_type):
|
||||
for versioned_stream in self.callbacks[grvt_endpoint_type]:
|
||||
for selector in self.callbacks[grvt_endpoint_type][versioned_stream]:
|
||||
self.logger.info(
|
||||
f"{self._clsname} _resubscribe {grvt_endpoint_type=}"
|
||||
f" {versioned_stream=}/{selector=}"
|
||||
)
|
||||
await self._subscribe_to_stream(
|
||||
grvt_endpoint_type, versioned_stream, selector
|
||||
)
|
||||
else:
|
||||
self.logger.warning(f"{self._clsname} _resubscribe - No connection.")
|
||||
|
||||
# **************** PUBLIC API CALLS
|
||||
def is_stream_subscribed(
|
||||
self, grvt_endpoint_type: GrvtWSEndpointType, stream: str
|
||||
) -> bool:
|
||||
versioned_stream = self.get_versioned_stream(stream)
|
||||
return self.subscribed_streams.get(grvt_endpoint_type, {}).get(
|
||||
versioned_stream, False
|
||||
)
|
||||
|
||||
def _check_susbcribed_stream(
|
||||
self, grvt_endpoint_type: GrvtWSEndpointType, message: dict
|
||||
) -> None:
|
||||
stream_subscribed: str = ""
|
||||
if "stream" in message:
|
||||
stream_subscribed = message["stream"]
|
||||
elif "result" in message and "stream" in message["result"]:
|
||||
stream_subscribed = message.get("result", {}).get("stream", "")
|
||||
if stream_subscribed:
|
||||
if not self.subscribed_streams[grvt_endpoint_type].get(stream_subscribed):
|
||||
self.logger.info(
|
||||
f"{self._clsname} subscribed to stream:{stream_subscribed}"
|
||||
)
|
||||
self.subscribed_streams[grvt_endpoint_type][stream_subscribed] = True
|
||||
|
||||
async def _read_messages(self, grvt_endpoint_type: GrvtWSEndpointType):
|
||||
FN = f"{self._clsname} _read_messages {grvt_endpoint_type.value}"
|
||||
while True:
|
||||
if self.is_connection_open(grvt_endpoint_type):
|
||||
try:
|
||||
self.logger.debug(f"{FN} waiting for message")
|
||||
response = await asyncio.wait_for(
|
||||
self.ws[grvt_endpoint_type].recv(), timeout=WS_READ_TIMEOUT
|
||||
)
|
||||
message = json.loads(response)
|
||||
self.logger.debug(f"{FN} received {message=}")
|
||||
self._check_susbcribed_stream(grvt_endpoint_type, message)
|
||||
if "feed" in message:
|
||||
stream_subscribed: str | None = message.get("stream")
|
||||
selector: str = message.get("selector")
|
||||
if stream_subscribed is None:
|
||||
self.logger.warning(f"{FN} missing stream in {message=}")
|
||||
if selector is None:
|
||||
self.logger.warning(f"{FN} missing selector in {message=}")
|
||||
if stream_subscribed and selector:
|
||||
callback = (
|
||||
self.callbacks[grvt_endpoint_type]
|
||||
.get(stream_subscribed, {})
|
||||
.get(selector, None)
|
||||
)
|
||||
if callback:
|
||||
await callback(message)
|
||||
stream: str = self.get_non_versioned_stream(
|
||||
stream_subscribed
|
||||
)
|
||||
self._last_message[stream] = message
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"{FN} No callback for {stream_subscribed=}/{selector=}"
|
||||
)
|
||||
elif "jsonrpc" in message:
|
||||
"""
|
||||
{'jsonrpc': '', 'result': {'result':
|
||||
{'order_id': '0x00', 'sub_account_id': '8751933338735530',
|
||||
'is_market': False, 'time_in_force': 'GOOD_TILL_TIME', 'post_only': False,
|
||||
'reduce_only': False, 'legs': [{'instrument': 'BTC_USDT_Perp', 'size': '0.001',
|
||||
'limit_price': '50000.0', 'is_buying_asset': True}],
|
||||
'signature': {'signer': '0x2989e3783e2ae05f9a1538dd411a22a4cd9554ad',
|
||||
'r': '0xa566702c1e5557ab96e8d5197b6871456765a80556bba46c9d4928bd573ca66c',
|
||||
's': '0x6f6e0be6dca125643fce884ca28c0ae341b201efe49e10a9626859517b4a09af',
|
||||
'v': 28, 'expiration': '1729005262433997000', 'nonce': 3898454329},
|
||||
'metadata': {'client_order_id': '123', 'create_time': '1728918862633971628'},
|
||||
'state': {'status': 'OPEN', 'reject_reason': 'UNSPECIFIED',
|
||||
'book_size': ['0.001'], 'traded_size': ['0.0'], 'update_time': '1728918862633971628'}}},
|
||||
'id': 2}
|
||||
"""
|
||||
self.logger.debug(f"{FN} jsonrpc result:{message.get('result')}")
|
||||
else:
|
||||
self.logger.info(f"{FN} Non-actionable message:{message}")
|
||||
except (
|
||||
websockets.exceptions.ConnectionClosedError,
|
||||
websockets.exceptions.ConnectionClosedOK,
|
||||
):
|
||||
self.logger.exception(
|
||||
f"{FN} connection closed {traceback.format_exc()}"
|
||||
)
|
||||
await self._reconnect(grvt_endpoint_type)
|
||||
except asyncio.TimeoutError: # noqa: UP041
|
||||
self.logger.debug(f"{FN} Timeout {WS_READ_TIMEOUT} secs")
|
||||
pass
|
||||
except Exception:
|
||||
self.logger.exception(
|
||||
f"{FN} connection failed {traceback.format_exc()}"
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
self.logger.info(f"{FN} connection not open")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def _send(self, end_point_type: GrvtWSEndpointType, message: str):
|
||||
try:
|
||||
if self.ws[end_point_type] and self.ws[end_point_type].open:
|
||||
self.logger.info(
|
||||
f"{self._clsname} _send() {end_point_type=}"
|
||||
f" url:{self.api_url[end_point_type]} {message=}"
|
||||
)
|
||||
await self.ws[end_point_type].send(message)
|
||||
except websockets.exceptions.ConnectionClosedError as e:
|
||||
self.logger.info(f"{self._clsname} _send() Restarted connection {e}")
|
||||
await self._reconnect(end_point_type)
|
||||
if self.ws[end_point_type]:
|
||||
self.logger.info(
|
||||
f"{self._clsname} _send() RESEND on RECONNECT {end_point_type=}"
|
||||
f" url:{self.api_url[end_point_type]} {message=}"
|
||||
)
|
||||
await self.ws[end_point_type].send(message)
|
||||
except Exception:
|
||||
self.logger.exception(f"{self._clsname} send failed {traceback.format_exc()}")
|
||||
await self._reconnect(end_point_type)
|
||||
|
||||
def _construct_selector(self, stream: str, params: dict) -> str:
|
||||
feed: str = ""
|
||||
# ******** Market Data ********
|
||||
if stream.endswith(("mini.s", "mini.d", "ticker.s", "ticker.d")):
|
||||
feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}"
|
||||
if stream.endswith("book.s"):
|
||||
feed = (
|
||||
f"{params.get('instrument', '')}@{params.get('rate', '500')}-"
|
||||
f"{params.get('depth', '10')}"
|
||||
)
|
||||
if stream.endswith("book.d"):
|
||||
feed = f"{params.get('instrument', '')}@{params.get('rate', '500')}"
|
||||
if stream.endswith("trade"):
|
||||
feed = f"{params.get('instrument', '')}@{params.get('limit', '50')}"
|
||||
if stream.endswith("candle"):
|
||||
feed = (
|
||||
f"{params.get('instrument', '')}@{params.get('interval', 'CI_1_M')}-"
|
||||
f"{params.get('type', 'TRADE')}"
|
||||
)
|
||||
# ******** Trade Data ********
|
||||
if stream.endswith(("order", "state", "position", "fill")):
|
||||
if not params:
|
||||
feed = f"{self._trading_account_id}"
|
||||
elif params.get("instrument"):
|
||||
feed = f"{self._trading_account_id}-{params.get('instrument', '')}"
|
||||
else:
|
||||
feed = (
|
||||
f"{self._trading_account_id}-{params.get('kind', '')}-"
|
||||
f"{params.get('base', '')}-{params.get('quote', '')}"
|
||||
)
|
||||
# Deposit, Transfer, Withdrawal
|
||||
if stream.endswith(("deposit", "transfer", "withdrawal")):
|
||||
feed = ""
|
||||
# f"{params.get('sub_account_id', '')}-{params.get('main_account_id', '')}"
|
||||
|
||||
return feed
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
stream: str,
|
||||
callback: Callable,
|
||||
ws_end_point_type: GrvtWSEndpointType | None = None,
|
||||
params: dict = {},
|
||||
) -> None:
|
||||
"""
|
||||
Subscribe to a stream with optional parameters.
|
||||
Call the callback function when a message is received.
|
||||
callback function should have the following signature:
|
||||
(dict) -> None.
|
||||
"""
|
||||
FN = f"{self._clsname} subscribe {stream=}"
|
||||
if not ws_end_point_type: # use default endpoint type
|
||||
ws_end_point_type = GRVT_WS_STREAMS.get(stream)
|
||||
if not ws_end_point_type:
|
||||
self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}")
|
||||
return
|
||||
is_trade_data = is_trading_ws_endpoint(ws_end_point_type)
|
||||
if is_trade_data and not self._trading_account_id:
|
||||
self.logger.error(
|
||||
f"{FN} {stream=} is a trading data connection. Requires trading_account_id."
|
||||
)
|
||||
return
|
||||
# create selector string and register callback
|
||||
selector: str = self._construct_selector(stream, params)
|
||||
versioned_stream: str = self.get_versioned_stream(stream)
|
||||
if versioned_stream not in self.callbacks[ws_end_point_type]:
|
||||
self.callbacks[ws_end_point_type][versioned_stream] = {}
|
||||
self.callbacks[ws_end_point_type][versioned_stream][selector] = callback
|
||||
self.logger.info(
|
||||
f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}"
|
||||
)
|
||||
# check if connection is open and subscribe
|
||||
if self.is_connection_open(ws_end_point_type):
|
||||
await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector)
|
||||
else:
|
||||
self.logger.info(f"{FN} Connection not open. Will subscribe on connect.")
|
||||
|
||||
async def re_subscribe_stream(
|
||||
self,
|
||||
stream: str,
|
||||
callback: Callable,
|
||||
ws_end_point_type: GrvtWSEndpointType | None = None,
|
||||
params: dict = {},
|
||||
) -> None:
|
||||
""" This method should be called in a separate task -
|
||||
otherwise it will block the event loop for 5 seconds.
|
||||
Unsubscribe from a specific stream and subscribe again with optional parameters.
|
||||
Call the callback function when a message is received.
|
||||
callback function should have the following signature:
|
||||
(dict) -> None.
|
||||
"""
|
||||
FN = f"{self._clsname} re_subscribe {stream=}"
|
||||
if not ws_end_point_type: # use default endpoint type
|
||||
ws_end_point_type = GRVT_WS_STREAMS.get(stream)
|
||||
if not ws_end_point_type:
|
||||
self.logger.error(f"{FN} unknown GrvtWSEndpointType for {stream=}")
|
||||
return
|
||||
if not self.is_connection_open(ws_end_point_type):
|
||||
self.logger.info(f"{FN} {ws_end_point_type=} not open. Try again after connect.")
|
||||
return
|
||||
is_trade_data = is_trading_ws_endpoint(ws_end_point_type)
|
||||
if is_trade_data and not self._trading_account_id:
|
||||
self.logger.error(
|
||||
f"{FN} {stream=} is a trading data connection. Requires trading_account_id."
|
||||
)
|
||||
return
|
||||
# create selector string and register callback
|
||||
selector: str = self._construct_selector(stream, params)
|
||||
versioned_stream: str = self.get_versioned_stream(stream)
|
||||
if versioned_stream not in self.callbacks[ws_end_point_type]:
|
||||
self.callbacks[ws_end_point_type][versioned_stream] = {}
|
||||
self.callbacks[ws_end_point_type][versioned_stream][selector] = callback
|
||||
# self.logger.info(
|
||||
# f"{FN} {params=} {ws_end_point_type=}/{versioned_stream=}/{selector=} callback:{callback}"
|
||||
# )
|
||||
await self._unsubscribe_to_stream(ws_end_point_type, versioned_stream, selector)
|
||||
await asyncio.sleep(5) # wait for unsubscribe to complete
|
||||
await self._subscribe_to_stream(ws_end_point_type, versioned_stream, selector)
|
||||
|
||||
|
||||
def get_versioned_stream(self, stream: str) -> str:
|
||||
return (
|
||||
stream if self.api_ws_version == "v0" else f"{self.api_ws_version}.{stream}"
|
||||
)
|
||||
|
||||
def get_non_versioned_stream(self, versioned_stream: str) -> str:
|
||||
if self.api_ws_version == "v0":
|
||||
return versioned_stream
|
||||
return versioned_stream.split(".")[1]
|
||||
|
||||
async def _subscribe_to_stream(
|
||||
self,
|
||||
ws_end_point_type: GrvtWSEndpointType,
|
||||
versioned_stream: str,
|
||||
selector: str,
|
||||
) -> None:
|
||||
FN = (
|
||||
f"{self._clsname} _subscribe_to_stream {ws_end_point_type=}"
|
||||
f" {versioned_stream=} {selector=}"
|
||||
)
|
||||
self._request_id += 1
|
||||
if ws_end_point_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
]: # Legacy subscription
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"request_id": self._request_id,
|
||||
"stream": versioned_stream,
|
||||
"feed": [selector],
|
||||
"method": "subscribe",
|
||||
"is_full": True,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
else: # RPC WS format
|
||||
self._request_id += 1
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "subscribe",
|
||||
"params": {
|
||||
"stream": versioned_stream,
|
||||
"selectors": [selector],
|
||||
},
|
||||
"id": self._request_id,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
await self._send(ws_end_point_type, subscribe_json)
|
||||
stream: str = self.get_non_versioned_stream(versioned_stream)
|
||||
if stream not in self._last_message:
|
||||
self._last_message[stream] = {}
|
||||
|
||||
async def _unsubscribe_to_stream(
|
||||
self,
|
||||
ws_end_point_type: GrvtWSEndpointType,
|
||||
versioned_stream: str,
|
||||
selector: str,
|
||||
) -> None:
|
||||
FN = (
|
||||
f"{self._clsname} _unsubscribe_to_stream {ws_end_point_type=}"
|
||||
f" {versioned_stream=} {selector=}"
|
||||
)
|
||||
self._request_id += 1
|
||||
if ws_end_point_type in [
|
||||
GrvtWSEndpointType.TRADE_DATA,
|
||||
GrvtWSEndpointType.MARKET_DATA,
|
||||
]: # Legacy subscription
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"request_id": self._request_id,
|
||||
"stream": versioned_stream,
|
||||
"feed": [selector],
|
||||
"method": "unsubscribe",
|
||||
"is_full": True,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
else: # RPC WS format
|
||||
self._request_id += 1
|
||||
subscribe_json = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "unsubscribe",
|
||||
"params": {
|
||||
"stream": versioned_stream,
|
||||
"selectors": [selector],
|
||||
},
|
||||
"id": self._request_id,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"{FN} {versioned_stream=} {subscribe_json=}")
|
||||
await self._send(ws_end_point_type, subscribe_json)
|
||||
|
||||
def jsonrpc_wrap_payload(
|
||||
self, payload: dict, method: str, version: str = "v1"
|
||||
) -> dict:
|
||||
"""
|
||||
Wrap the payload in JSON-RPC format.
|
||||
"""
|
||||
self._request_id += 1
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": f"{version}/{method}",
|
||||
"params": payload,
|
||||
"id": self._request_id,
|
||||
}
|
||||
|
||||
async def send_rpc_message(
|
||||
self, end_point_type: GrvtWSEndpointType, message: dict
|
||||
) -> None:
|
||||
"""
|
||||
Send a message to the server.
|
||||
"""
|
||||
await self._send(end_point_type, json.dumps(message))
|
||||
self.logger.info(f"{self._clsname} send_rpc_message {end_point_type=} {message=}")
|
||||
|
||||
async def rpc_create_order(
|
||||
self,
|
||||
symbol: str,
|
||||
order_type: GrvtOrderType,
|
||||
side: GrvtOrderSide,
|
||||
amount: float | Decimal | str | int,
|
||||
price: Num = None,
|
||||
params={},
|
||||
) -> dict:
|
||||
"""
|
||||
Create an order.
|
||||
"""
|
||||
FN = f"{self._clsname} rpc_create_order"
|
||||
if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL):
|
||||
raise GrvtInvalidOrder("Trade data connection not available.")
|
||||
order = self._get_order_with_validations(
|
||||
symbol, order_type, side, amount, price, params
|
||||
)
|
||||
self.logger.info(f"{FN} {order=}")
|
||||
payload = get_order_rpc_payload(order, self._private_key, self.env, self.markets)
|
||||
self._request_id += 1
|
||||
payload["id"] = self._request_id
|
||||
self.logger.info(f"{FN} {payload=}")
|
||||
await self.send_rpc_message(GrvtWSEndpointType.TRADE_DATA_RPC_FULL, payload)
|
||||
return payload
|
||||
|
||||
async def rpc_create_limit_order(
|
||||
self,
|
||||
symbol: str,
|
||||
side: GrvtOrderSide,
|
||||
amount: float | Decimal | str | int,
|
||||
price: Num,
|
||||
params={},
|
||||
) -> dict:
|
||||
return await self.rpc_create_order(symbol, "limit", side, amount, price, params)
|
||||
|
||||
async def rpc_cancel_all_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature BUT lacks symbol
|
||||
Cancel all orders for a sub-account.
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values: 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch
|
||||
orders for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# FN = f"{self._clsname} rpc_cancel_all_orders"
|
||||
payload: dict = self._get_payload_cancel_all_orders(params)
|
||||
jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="cancel_all_orders")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
|
||||
async def rpc_cancel_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature
|
||||
Cancel specific order for the account by sending JsonRpc call on WebSocket.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Cancel order](https://api-docs.grvt.io/trading_api/#cancel-order)
|
||||
for details.<br>.
|
||||
|
||||
Args:
|
||||
id (str): exchange assigned order ID<br>
|
||||
symbol (str): trading symbol<br>
|
||||
params:
|
||||
* client_order_id (str): client assigned order ID<br>
|
||||
* time_to_live_ms (str): lifetime of cancel requiest in millisecs<br>
|
||||
Returns:
|
||||
payload used to cancel order.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} rpc_cancel_order"
|
||||
if not self.is_endpoint_connected(GrvtWSEndpointType.TRADE_DATA_RPC_FULL):
|
||||
raise GrvtInvalidOrder("Trade data connection not available.")
|
||||
self._check_account_auth()
|
||||
# Prepare payload
|
||||
payload: dict = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = str(id)
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(f"{FN} requires either order_id or client_order_id")
|
||||
if "time_to_live_ms" in params:
|
||||
payload["time_to_live_ms"] = str(params["time_to_live_ms"])
|
||||
# Send cancel requiest
|
||||
jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="cancel_order")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
|
||||
async def rpc_fetch_open_orders(
|
||||
self,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Fetch open orders for the account.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Open orders](https://api-docs.grvt.io/trading_api/#open-orders)
|
||||
for details.<br>.
|
||||
Fetches open orders for the account.<br>
|
||||
Sends JsonRpc call on WebSocket.<br>
|
||||
Args:
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`kind` (str): instrument kind. Valid values are 'PERPETUAL'.<br>
|
||||
`base` (str): base currency. If missing/empty then fetch orders
|
||||
for all base currencies.<br>
|
||||
`quote` (str): quote currency. Defaults to all.<br>
|
||||
Returns:
|
||||
payload used to fetch open orders.<br><br>
|
||||
"""
|
||||
self._check_account_auth()
|
||||
# Prepare request payload
|
||||
payload: dict = self._get_payload_fetch_open_orders(symbol=None, params=params)
|
||||
jsonrpc_payload: dict = self.jsonrpc_wrap_payload(payload, method="open_orders")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
|
||||
async def rpc_fetch_order(
|
||||
self,
|
||||
id: str | None = None,
|
||||
symbol: str | None = None,
|
||||
params: dict = {},
|
||||
) -> dict:
|
||||
"""
|
||||
Ccxt compliant signature.<br>
|
||||
Private call requires authorization.<br>
|
||||
See [Get Order](https://api-docs.grvt.io/trading_api/#get-order)
|
||||
for details.<br>.
|
||||
Get Order status by either order_id or client_order_id.<br>
|
||||
Sends JsonRpc call on WebSocket.<br>
|
||||
Args:
|
||||
id: (str) order_id to fetch.<br>
|
||||
symbol: (str) NOT SUPPRTED.<br>
|
||||
params: dictionary with parameters. Valid keys:<br>
|
||||
`client_order_id` (int): client assigned order ID.<br>
|
||||
Returns:
|
||||
payload used to fetch order.<br>
|
||||
"""
|
||||
FN = f"{self._clsname} rpc_fetch_order"
|
||||
self._check_account_auth()
|
||||
payload = {
|
||||
"sub_account_id": str(self._trading_account_id),
|
||||
}
|
||||
if id:
|
||||
payload["order_id"] = id
|
||||
elif "client_order_id" in params:
|
||||
payload["client_order_id"] = str(params["client_order_id"])
|
||||
else:
|
||||
raise GrvtInvalidOrder(
|
||||
f"{FN} requires either order_id or params['client_order_id']"
|
||||
)
|
||||
jsonrpc_payload = self.jsonrpc_wrap_payload(payload, method="order")
|
||||
await self.send_rpc_message(
|
||||
GrvtWSEndpointType.TRADE_DATA_RPC_FULL, jsonrpc_payload
|
||||
)
|
||||
return jsonrpc_payload
|
||||
@@ -0,0 +1,25 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .grvt_raw_types import Signature, TransferType
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transfer:
|
||||
# The account to transfer from
|
||||
from_account_id: str
|
||||
# The subaccount to transfer from (0 if transferring from main account)
|
||||
from_sub_account_id: str
|
||||
# The account to deposit into
|
||||
to_account_id: str
|
||||
# The subaccount to transfer to (0 if transferring to main account)
|
||||
to_sub_account_id: str
|
||||
# The token currency to transfer
|
||||
currency: str
|
||||
# The number of tokens to transfer
|
||||
num_tokens: str
|
||||
# The signature of the transfer
|
||||
signature: Signature
|
||||
# The type of transfer
|
||||
transfer_type: TransferType
|
||||
# The metadata of the transfer
|
||||
transfer_metadata: str
|
||||
@@ -0,0 +1,365 @@
|
||||
from enum import Enum
|
||||
|
||||
from dacite import Config, from_dict
|
||||
|
||||
from . import grvt_raw_types as types
|
||||
from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawAsyncBase
|
||||
|
||||
# mypy: disable-error-code="no-any-return"
|
||||
|
||||
|
||||
class GrvtRawAsync(GrvtRawAsyncBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
self.md_rpc = self.env.market_data.rpc_endpoint
|
||||
self.td_rpc = self.env.trade_data.rpc_endpoint
|
||||
|
||||
async def get_instrument_v1(
|
||||
self, req: types.ApiGetInstrumentRequest
|
||||
) -> types.ApiGetInstrumentResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/instrument", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def get_all_instruments_v1(
|
||||
self, req: types.ApiGetAllInstrumentsRequest
|
||||
) -> types.ApiGetAllInstrumentsResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/all_instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def get_filtered_instruments_v1(
|
||||
self, req: types.ApiGetFilteredInstrumentsRequest
|
||||
) -> types.ApiGetFilteredInstrumentsResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def get_currency_v1(
|
||||
self, req: types.ApiGetCurrencyRequest
|
||||
) -> types.ApiGetCurrencyResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/currency", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def mini_ticker_v1(
|
||||
self, req: types.ApiMiniTickerRequest
|
||||
) -> types.ApiMiniTickerResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/mini", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def ticker_v1(
|
||||
self, req: types.ApiTickerRequest
|
||||
) -> types.ApiTickerResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/ticker", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def orderbook_levels_v1(
|
||||
self, req: types.ApiOrderbookLevelsRequest
|
||||
) -> types.ApiOrderbookLevelsResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/book", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def trade_v1(
|
||||
self, req: types.ApiTradeRequest
|
||||
) -> types.ApiTradeResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/trade", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def trade_history_v1(
|
||||
self, req: types.ApiTradeHistoryRequest
|
||||
) -> types.ApiTradeHistoryResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/trade_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def candlestick_v1(
|
||||
self, req: types.ApiCandlestickRequest
|
||||
) -> types.ApiCandlestickResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/kline", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def funding_rate_v1(
|
||||
self, req: types.ApiFundingRateRequest
|
||||
) -> types.ApiFundingRateResponse | GrvtError:
|
||||
resp = await self._post(False, self.md_rpc + "/full/v1/funding", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def create_order_v1(
|
||||
self, req: types.ApiCreateOrderRequest
|
||||
) -> types.ApiCreateOrderResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/create_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def cancel_order_v1(
|
||||
self, req: types.ApiCancelOrderRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def cancel_all_orders_v1(
|
||||
self, req: types.ApiCancelAllOrdersRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def get_order_v1(
|
||||
self, req: types.ApiGetOrderRequest
|
||||
) -> types.ApiGetOrderResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def open_orders_v1(
|
||||
self, req: types.ApiOpenOrdersRequest
|
||||
) -> types.ApiOpenOrdersResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/open_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def order_history_v1(
|
||||
self, req: types.ApiOrderHistoryRequest
|
||||
) -> types.ApiOrderHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/order_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def cancel_on_disconnect_v1(
|
||||
self, req: types.ApiCancelOnDisconnectRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def fill_history_v1(
|
||||
self, req: types.ApiFillHistoryRequest
|
||||
) -> types.ApiFillHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/fill_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def positions_v1(
|
||||
self, req: types.ApiPositionsRequest
|
||||
) -> types.ApiPositionsResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/positions", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def funding_payment_history_v1(
|
||||
self, req: types.ApiFundingPaymentHistoryRequest
|
||||
) -> types.ApiFundingPaymentHistoryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/funding_payment_history", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def deposit_history_v1(
|
||||
self, req: types.ApiDepositHistoryRequest
|
||||
) -> types.ApiDepositHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/deposit_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def transfer_v1(
|
||||
self, req: types.ApiTransferRequest
|
||||
) -> types.ApiTransferResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/transfer", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def transfer_history_v1(
|
||||
self, req: types.ApiTransferHistoryRequest
|
||||
) -> types.ApiTransferHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/transfer_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def withdrawal_v1(
|
||||
self, req: types.ApiWithdrawalRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def withdrawal_history_v1(
|
||||
self, req: types.ApiWithdrawalHistoryRequest
|
||||
) -> types.ApiWithdrawalHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def sub_account_summary_v1(
|
||||
self, req: types.ApiSubAccountSummaryRequest
|
||||
) -> types.ApiSubAccountSummaryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def sub_account_history_v1(
|
||||
self, req: types.ApiSubAccountHistoryRequest
|
||||
) -> types.ApiSubAccountHistoryResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/account_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def aggregated_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiAggregatedAccountSummaryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/aggregated_account_summary", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def funding_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiFundingAccountSummaryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/funding_account_summary", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def set_derisk_mm_ratio_v1(
|
||||
self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest
|
||||
) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def get_all_initial_leverage_v1(
|
||||
self, req: types.ApiGetAllInitialLeverageRequest
|
||||
) -> types.ApiGetAllInitialLeverageResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/get_all_initial_leverage", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def set_initial_leverage_v1(
|
||||
self, req: types.ApiSetInitialLeverageRequest
|
||||
) -> types.ApiSetInitialLeverageResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_burn_tokens_v1(
|
||||
self, req: types.ApiVaultBurnTokensRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_invest_v1(
|
||||
self, req: types.ApiVaultInvestRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_invest", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_investor_summary_v1(
|
||||
self, req: types.ApiVaultInvestorSummaryRequest
|
||||
) -> types.ApiVaultInvestorSummaryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_investor_summary", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_redeem_v1(
|
||||
self, req: types.ApiVaultRedeemRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_redeem_cancel_v1(
|
||||
self, req: types.ApiVaultRedeemCancelRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = await self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
async def vault_redemption_queue_v1(
|
||||
self, req: types.ApiVaultViewRedemptionQueueRequest
|
||||
) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
async def query_vault_manager_investor_history_v1(
|
||||
self, req: types.ApiQueryVaultManagerInvestorHistoryRequest
|
||||
) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError:
|
||||
resp = await self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_manager_investor_history", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
@@ -0,0 +1,267 @@
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from http.cookies import SimpleCookie
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import requests # type: ignore
|
||||
from eth_account import Account
|
||||
|
||||
from .grvt_raw_env import GrvtEnv, GrvtEnvConfig, get_env_config
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtApiConfig:
|
||||
env: GrvtEnv
|
||||
trading_account_id: str | None
|
||||
private_key: str | None
|
||||
api_key: str | None
|
||||
logger: logging.Logger | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtError:
|
||||
code: int
|
||||
message: str
|
||||
status: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtCookie:
|
||||
gravity: str
|
||||
expires: datetime
|
||||
grvt_account_id: str | None = None
|
||||
|
||||
|
||||
class GrvtRawBase:
|
||||
"""
|
||||
GrvtRawBase is base class for Grvt Rest API classes.
|
||||
|
||||
This should not be used directly, but rather through a derivative API class.
|
||||
"""
|
||||
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
self.config = config
|
||||
self.env: GrvtEnvConfig = get_env_config(config.env)
|
||||
self.logger: logging.Logger = config.logger or logging.getLogger(__name__)
|
||||
self._cookie: GrvtCookie | None = None
|
||||
if self.config.private_key is not None:
|
||||
self.account: Account = Account.from_key(self.config.private_key)
|
||||
|
||||
"""
|
||||
Cookie handling
|
||||
"""
|
||||
|
||||
def _should_refresh_cookie(self) -> bool:
|
||||
if not self.config.api_key:
|
||||
raise ValueError("Attempting to use Authenticated API without API key set")
|
||||
time_till_expiration = None
|
||||
if self._cookie and self._cookie.expires:
|
||||
time_till_expiration = self._cookie.expires.timestamp() - 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 now={time.time()}"
|
||||
f" {time_till_expiration=} secs"
|
||||
)
|
||||
return not is_cookie_fresh
|
||||
|
||||
|
||||
class GrvtRawSyncBase(GrvtRawBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
# Sync API session
|
||||
self._session: requests.Session = requests.Session()
|
||||
self._session.headers.update({"Content-Type": "application/json"})
|
||||
|
||||
"""
|
||||
Cookie handling
|
||||
"""
|
||||
|
||||
def _refresh_cookie(self) -> None:
|
||||
if not self._should_refresh_cookie():
|
||||
return None
|
||||
# Get cookie
|
||||
self._cookie = self._get_cookie(
|
||||
self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key)
|
||||
)
|
||||
self.logger.info(f"refresh_cookie cookie={self._cookie}")
|
||||
# Update cookie in session
|
||||
if self._cookie:
|
||||
self._session.cookies.update({"gravity": self._cookie.gravity})
|
||||
if self._cookie.grvt_account_id:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie.grvt_account_id}
|
||||
)
|
||||
return None
|
||||
|
||||
def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None:
|
||||
FN = f"_get_cookie {path=}"
|
||||
try:
|
||||
return_value = self._session.post(
|
||||
path,
|
||||
json={"api_key": api_key},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=5,
|
||||
)
|
||||
self.logger.info(f"{FN} {return_value=}")
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie_header = return_value.headers.get("Set-Cookie")
|
||||
grvt_cookie = return_value.cookies.get("gravity")
|
||||
self.logger.info(
|
||||
f"{FN} OK {return_value.headers=} \n "
|
||||
f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}"
|
||||
)
|
||||
cookie.load(cookie_header)
|
||||
cookie_value = cookie["gravity"].value
|
||||
cookie_expiry = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str | None = return_value.headers.get(
|
||||
"X-Grvt-Account-Id"
|
||||
)
|
||||
return GrvtCookie(
|
||||
gravity=cookie_value,
|
||||
expires=cookie_expiry,
|
||||
grvt_account_id=grvt_account_id,
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
|
||||
"""
|
||||
Post handling
|
||||
"""
|
||||
|
||||
def _post(self, is_auth: bool, path: str, req: Any) -> Any:
|
||||
FN = f"_post {path=}"
|
||||
# Always see if need to referesh cookie before sending an authenticated request
|
||||
if is_auth:
|
||||
self._refresh_cookie()
|
||||
|
||||
req_json = json.dumps(req, cls=DataclassJSONEncoder)
|
||||
resp_json: Any = {}
|
||||
|
||||
self.logger.debug(f"{FN} {req_json=}")
|
||||
resp: requests.Response = self._session.post(path, data=req_json, timeout=5)
|
||||
try:
|
||||
resp_json = resp.json()
|
||||
if not resp.ok:
|
||||
self.logger.warning(f"{FN} Error {resp_json=}")
|
||||
else:
|
||||
self.logger.debug(f"{FN} OK {resp_json=}")
|
||||
except Exception as err:
|
||||
self.logger.error(f"{FN} Unable to parse {resp.text=} as json:{err=}")
|
||||
return resp_json
|
||||
|
||||
|
||||
class GrvtRawAsyncBase(GrvtRawBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
# Async API session
|
||||
self._session: aiohttp.ClientSession = aiohttp.ClientSession(
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
"""
|
||||
Cookie handling
|
||||
"""
|
||||
|
||||
async def _refresh_cookie(self) -> None:
|
||||
if not self._should_refresh_cookie():
|
||||
return None
|
||||
|
||||
# Get cookie
|
||||
self._cookie = await self._get_cookie(
|
||||
self.env.edge.rpc_endpoint + "/auth/api_key/login", str(self.config.api_key)
|
||||
)
|
||||
self.logger.info(f"refresh_cookie cookie={self._cookie}")
|
||||
|
||||
# Update cookie in session
|
||||
if self._cookie:
|
||||
self._session.cookie_jar.update_cookies({"gravity": self._cookie.gravity})
|
||||
if self._cookie.grvt_account_id:
|
||||
self._session.headers.update(
|
||||
{"X-Grvt-Account-Id": self._cookie.grvt_account_id}
|
||||
)
|
||||
return None
|
||||
|
||||
async def _get_cookie(self, path: str, api_key: str) -> GrvtCookie | None:
|
||||
FN = f"_get_cookie {path=}"
|
||||
try:
|
||||
data = {"api_key": api_key}
|
||||
self.logger.info(f"{FN} ask for cookie {path=} {data=}")
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url=path, json=data, timeout=5) as return_value:
|
||||
self.logger.info(f"{FN} {return_value=}")
|
||||
if return_value.ok:
|
||||
cookie = SimpleCookie()
|
||||
cookie_header = return_value.headers.get("Set-Cookie")
|
||||
grvt_cookie = return_value.cookies.get("gravity")
|
||||
self.logger.info(
|
||||
f"{FN} OK {return_value.headers=} \n "
|
||||
f"{return_value.cookies=}\n{grvt_cookie=}\n{cookie_header=}"
|
||||
)
|
||||
cookie.load(cookie_header)
|
||||
cookie_value = cookie["gravity"].value
|
||||
cookie_expiry = datetime.strptime(
|
||||
cookie["gravity"]["expires"],
|
||||
"%a, %d %b %Y %H:%M:%S %Z",
|
||||
)
|
||||
grvt_account_id: str | None = return_value.headers.get(
|
||||
"X-Grvt-Account-Id"
|
||||
)
|
||||
return GrvtCookie(
|
||||
gravity=cookie_value,
|
||||
expires=cookie_expiry,
|
||||
grvt_account_id=grvt_account_id,
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"{FN} Error getting cookie: {e}")
|
||||
return None
|
||||
|
||||
"""
|
||||
Post handling
|
||||
"""
|
||||
|
||||
async def _post(self, is_auth: bool, path: str, req: Any) -> Any:
|
||||
FN = f"_post {path=}"
|
||||
# Always see if need to referesh cookie before sending an authenticated request
|
||||
if is_auth:
|
||||
await self._refresh_cookie()
|
||||
|
||||
req_json = json.dumps(req, cls=DataclassJSONEncoder)
|
||||
resp_json: Any = {}
|
||||
|
||||
self.logger.debug(f"{FN} {req_json=}")
|
||||
resp: aiohttp.ClientResponse = await self._session.post(
|
||||
path, data=req_json, timeout=5
|
||||
)
|
||||
try:
|
||||
resp_text = await resp.text()
|
||||
resp_json = json.loads(resp_text)
|
||||
if not resp.ok:
|
||||
self.logger.warning(f"{FN} Error {resp_text=}")
|
||||
else:
|
||||
self.logger.debug(f"{FN} OK {resp_text=}")
|
||||
except Exception as err:
|
||||
self.logger.error(f"{FN} Unable to parse {resp_text=} as json:{err=}")
|
||||
return resp_json
|
||||
|
||||
|
||||
class DataclassJSONEncoder(json.JSONEncoder):
|
||||
def default(self, o: Any) -> Any:
|
||||
if dataclasses.is_dataclass(o):
|
||||
return dataclasses.asdict(o) # type: ignore
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
@@ -0,0 +1,77 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class GrvtEnv(Enum):
|
||||
DEV = "dev"
|
||||
STAGING = "staging"
|
||||
TESTNET = "testnet"
|
||||
PROD = "prod"
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtEndpointConfig:
|
||||
rpc_endpoint: str
|
||||
ws_endpoint: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GrvtEnvConfig:
|
||||
edge: GrvtEndpointConfig
|
||||
trade_data: GrvtEndpointConfig
|
||||
market_data: GrvtEndpointConfig
|
||||
chain_id: int
|
||||
|
||||
|
||||
def get_env_config(environment: GrvtEnv) -> GrvtEnvConfig:
|
||||
match environment:
|
||||
case GrvtEnv.PROD:
|
||||
return GrvtEnvConfig(
|
||||
edge=GrvtEndpointConfig(
|
||||
rpc_endpoint="https://edge.grvt.io",
|
||||
ws_endpoint=None,
|
||||
),
|
||||
trade_data=GrvtEndpointConfig(
|
||||
rpc_endpoint="https://trades.grvt.io",
|
||||
ws_endpoint="wss://trades.grvt.io/ws",
|
||||
),
|
||||
market_data=GrvtEndpointConfig(
|
||||
rpc_endpoint="https://market-data.grvt.io",
|
||||
ws_endpoint="wss://market-data.grvt.io/ws",
|
||||
),
|
||||
chain_id=325,
|
||||
)
|
||||
case GrvtEnv.TESTNET:
|
||||
return GrvtEnvConfig(
|
||||
edge=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://edge.{environment.value}.grvt.io",
|
||||
ws_endpoint=None,
|
||||
),
|
||||
trade_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://trades.{environment.value}.grvt.io",
|
||||
ws_endpoint=f"wss://trades.{environment.value}.grvt.io/ws",
|
||||
),
|
||||
market_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://market-data.{environment.value}.grvt.io",
|
||||
ws_endpoint=f"wss://market-data.{environment.value}.grvt.io/ws",
|
||||
),
|
||||
chain_id=326,
|
||||
)
|
||||
case GrvtEnv.DEV | GrvtEnv.STAGING:
|
||||
return GrvtEnvConfig(
|
||||
edge=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://edge.{environment.value}.gravitymarkets.io",
|
||||
ws_endpoint=None,
|
||||
),
|
||||
trade_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://trades.{environment.value}.gravitymarkets.io",
|
||||
ws_endpoint=f"wss://trades.{environment.value}.gravitymarkets.io/ws",
|
||||
),
|
||||
market_data=GrvtEndpointConfig(
|
||||
rpc_endpoint=f"https://market-data.{environment.value}.gravitymarkets.io",
|
||||
ws_endpoint=f"wss://market-data.{environment.value}.gravitymarkets.io/ws",
|
||||
),
|
||||
chain_id=327 if environment == GrvtEnv.DEV else 328,
|
||||
)
|
||||
case _:
|
||||
raise ValueError(f"Unknown environment={environment}")
|
||||
@@ -0,0 +1,248 @@
|
||||
from enum import Enum
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional
|
||||
|
||||
from eth_account import Account
|
||||
from eth_account.messages import encode_typed_data
|
||||
|
||||
from .grvt_ccxt_utils import GrvtCurrency
|
||||
from .grvt_raw_base import GrvtApiConfig, GrvtEnv
|
||||
from .grvt_raw_types import Instrument, Order, Withdrawal, TimeInForce
|
||||
from .grvt_fixed_types import Transfer
|
||||
|
||||
#########################
|
||||
# INSTRUMENT CONVERSION #
|
||||
#########################
|
||||
|
||||
|
||||
PRICE_MULTIPLIER = 1_000_000_000
|
||||
|
||||
|
||||
class SignTimeInForce(Enum):
|
||||
GOOD_TILL_TIME = 1
|
||||
ALL_OR_NONE = 2
|
||||
IMMEDIATE_OR_CANCEL = 3
|
||||
FILL_OR_KILL = 4
|
||||
|
||||
|
||||
TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE = {
|
||||
TimeInForce.GOOD_TILL_TIME: SignTimeInForce.GOOD_TILL_TIME,
|
||||
TimeInForce.ALL_OR_NONE: SignTimeInForce.ALL_OR_NONE,
|
||||
TimeInForce.IMMEDIATE_OR_CANCEL: SignTimeInForce.IMMEDIATE_OR_CANCEL,
|
||||
TimeInForce.FILL_OR_KILL: SignTimeInForce.FILL_OR_KILL,
|
||||
}
|
||||
|
||||
|
||||
#####################
|
||||
# EIP-712 chain IDs #
|
||||
#####################
|
||||
CHAIN_IDS = {
|
||||
GrvtEnv.DEV: 327,
|
||||
GrvtEnv.STAGING: 327,
|
||||
GrvtEnv.TESTNET: 326,
|
||||
GrvtEnv.PROD: 325,
|
||||
}
|
||||
|
||||
|
||||
def get_EIP712_domain_data(env: GrvtEnv, chainId: int | None) -> dict[str, str | int]:
|
||||
return {
|
||||
"name": "GRVT Exchange",
|
||||
"version": "0",
|
||||
"chainId": chainId or CHAIN_IDS[env],
|
||||
}
|
||||
|
||||
|
||||
#####################
|
||||
# Sign Order #
|
||||
#####################
|
||||
|
||||
EIP712_ORDER_MESSAGE_TYPE = {
|
||||
"Order": [
|
||||
{"name": "subAccountID", "type": "uint64"},
|
||||
{"name": "isMarket", "type": "bool"},
|
||||
{"name": "timeInForce", "type": "uint8"},
|
||||
{"name": "postOnly", "type": "bool"},
|
||||
{"name": "reduceOnly", "type": "bool"},
|
||||
{"name": "legs", "type": "OrderLeg[]"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
"OrderLeg": [
|
||||
{"name": "assetID", "type": "uint256"},
|
||||
{"name": "contractSize", "type": "uint64"},
|
||||
{"name": "limitPrice", "type": "uint64"},
|
||||
{"name": "isBuyingContract", "type": "bool"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def sign_order(
|
||||
order: Order,
|
||||
config: GrvtApiConfig,
|
||||
account: Account,
|
||||
instruments: dict[str, Instrument],
|
||||
) -> Order:
|
||||
if config.private_key is None:
|
||||
raise ValueError("Private key is not set")
|
||||
|
||||
message_data = build_EIP712_order_message_data(order, instruments)
|
||||
|
||||
domain_data = get_EIP712_domain_data(config.env, CHAIN_IDS[config.env])
|
||||
signable_message = encode_typed_data(
|
||||
domain_data, EIP712_ORDER_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
order.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
order.signature.v = signed_message.v
|
||||
order.signature.signer = str(account.address)
|
||||
|
||||
return order
|
||||
|
||||
|
||||
def build_EIP712_order_message_data(
|
||||
order: Order, instruments: dict[str, Instrument]
|
||||
) -> dict[str, Any]:
|
||||
legs = []
|
||||
for leg in order.legs:
|
||||
instrument = instruments[leg.instrument]
|
||||
size_multiplier = 10**instrument.base_decimals
|
||||
|
||||
# use Decimal() instead of float() to avoid precision loss
|
||||
# int(float("1.013") * 1e9) = 1012999999
|
||||
# int(Decimal("1.013") * Decimal(1e9) = 1013000000
|
||||
size_int = int(Decimal(leg.size) * Decimal(size_multiplier))
|
||||
price_int = int(Decimal(leg.limit_price) * Decimal(PRICE_MULTIPLIER))
|
||||
legs.append(
|
||||
{
|
||||
"assetID": instrument.instrument_hash,
|
||||
"contractSize": size_int,
|
||||
"limitPrice": price_int,
|
||||
"isBuyingContract": leg.is_buying_asset,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"subAccountID": order.sub_account_id,
|
||||
"isMarket": order.is_market or False,
|
||||
"timeInForce": TIME_IN_FORCE_TO_SIGN_TIME_IN_FORCE[order.time_in_force].value,
|
||||
"postOnly": order.post_only or False,
|
||||
"reduceOnly": order.reduce_only or False,
|
||||
"legs": legs,
|
||||
"nonce": order.signature.nonce,
|
||||
"expiration": order.signature.expiration,
|
||||
}
|
||||
|
||||
|
||||
#####################
|
||||
# Sign Transfer #
|
||||
#####################
|
||||
|
||||
EIP712_TRANSFER_MESSAGE_TYPE = {
|
||||
"Transfer": [
|
||||
{"name": "fromAccount", "type": "address"},
|
||||
{"name": "fromSubAccount", "type": "uint64"},
|
||||
{"name": "toAccount", "type": "address"},
|
||||
{"name": "toSubAccount", "type": "uint64"},
|
||||
{"name": "tokenCurrency", "type": "uint8"},
|
||||
{"name": "numTokens", "type": "uint64"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_EIP712_transfer_message_data(transfer: Transfer, currencyId: int):
|
||||
return {
|
||||
"fromAccount": transfer.from_account_id,
|
||||
"fromSubAccount": transfer.from_sub_account_id,
|
||||
"toAccount": transfer.to_account_id,
|
||||
"toSubAccount": transfer.to_sub_account_id,
|
||||
"tokenCurrency": currencyId,
|
||||
"numTokens": int(
|
||||
Decimal(transfer.num_tokens) * Decimal(1e6)
|
||||
), # USDT has 6 decimals
|
||||
"nonce": transfer.signature.nonce,
|
||||
"expiration": transfer.signature.expiration,
|
||||
}
|
||||
|
||||
|
||||
def sign_transfer(
|
||||
transfer: Transfer,
|
||||
config: GrvtApiConfig,
|
||||
account: Account,
|
||||
chainId: int | None = None,
|
||||
currencyId: int = 3, # currencyId of USDT; refer to Get Currency API
|
||||
) -> Transfer:
|
||||
if config.private_key is None:
|
||||
raise ValueError("Private key is not set")
|
||||
|
||||
domain = get_EIP712_domain_data(config.env, chainId)
|
||||
|
||||
message_data = build_EIP712_transfer_message_data(transfer, currencyId)
|
||||
signable_message = encode_typed_data(
|
||||
domain, EIP712_TRANSFER_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
transfer.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
transfer.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
transfer.signature.v = signed_message.v
|
||||
transfer.signature.signer = str(account.address)
|
||||
|
||||
return transfer
|
||||
|
||||
|
||||
#####################
|
||||
# Sign Withdrawal #
|
||||
#####################
|
||||
|
||||
EIP712_WITHDRAWAL_MESSAGE_TYPE = {
|
||||
"Withdrawal": [
|
||||
{"name": "fromAccount", "type": "address"},
|
||||
{"name": "toEthAddress", "type": "address"},
|
||||
{"name": "tokenCurrency", "type": "uint8"},
|
||||
{"name": "numTokens", "type": "uint64"},
|
||||
{"name": "nonce", "type": "uint32"},
|
||||
{"name": "expiration", "type": "int64"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def build_EIP712_withdrawal_message_data(withdrawal: Withdrawal, currencyId: int):
|
||||
return {
|
||||
"fromAccount": withdrawal.from_account_id,
|
||||
"toEthAddress": withdrawal.to_eth_address,
|
||||
"tokenCurrency": currencyId,
|
||||
"numTokens": int(
|
||||
Decimal(withdrawal.num_tokens) * Decimal(1e6)
|
||||
), # USDT has 6 decimals
|
||||
"nonce": withdrawal.signature.nonce,
|
||||
"expiration": withdrawal.signature.expiration,
|
||||
}
|
||||
|
||||
|
||||
def sign_withdrawal(
|
||||
withdrawal: Withdrawal,
|
||||
config: GrvtApiConfig,
|
||||
account: Account,
|
||||
chainId: int | None = None,
|
||||
currencyId: int = 3, # currencyId of USDT; refer to Get Currency API
|
||||
) -> Withdrawal:
|
||||
if config.private_key is None:
|
||||
raise ValueError("Private key is not set")
|
||||
|
||||
domain = get_EIP712_domain_data(config.env, chainId)
|
||||
|
||||
message_data = build_EIP712_withdrawal_message_data(withdrawal, currencyId)
|
||||
signable_message = encode_typed_data(
|
||||
domain, EIP712_WITHDRAWAL_MESSAGE_TYPE, message_data
|
||||
)
|
||||
signed_message = account.sign_message(signable_message)
|
||||
|
||||
withdrawal.signature.r = "0x" + signed_message.r.to_bytes(32, byteorder="big").hex()
|
||||
withdrawal.signature.s = "0x" + signed_message.s.to_bytes(32, byteorder="big").hex()
|
||||
withdrawal.signature.v = signed_message.v
|
||||
withdrawal.signature.signer = str(account.address)
|
||||
|
||||
return withdrawal
|
||||
@@ -0,0 +1,351 @@
|
||||
from enum import Enum
|
||||
|
||||
from dacite import Config, from_dict
|
||||
|
||||
from . import grvt_raw_types as types
|
||||
from .grvt_raw_base import GrvtApiConfig, GrvtError, GrvtRawSyncBase
|
||||
|
||||
# mypy: disable-error-code="no-any-return"
|
||||
|
||||
|
||||
class GrvtRawSync(GrvtRawSyncBase):
|
||||
def __init__(self, config: GrvtApiConfig):
|
||||
super().__init__(config)
|
||||
self.md_rpc = self.env.market_data.rpc_endpoint
|
||||
self.td_rpc = self.env.trade_data.rpc_endpoint
|
||||
|
||||
def get_instrument_v1(
|
||||
self, req: types.ApiGetInstrumentRequest
|
||||
) -> types.ApiGetInstrumentResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/instrument", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetInstrumentResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def get_all_instruments_v1(
|
||||
self, req: types.ApiGetAllInstrumentsRequest
|
||||
) -> types.ApiGetAllInstrumentsResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/all_instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetAllInstrumentsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def get_filtered_instruments_v1(
|
||||
self, req: types.ApiGetFilteredInstrumentsRequest
|
||||
) -> types.ApiGetFilteredInstrumentsResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/instruments", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetFilteredInstrumentsResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def get_currency_v1(
|
||||
self, req: types.ApiGetCurrencyRequest
|
||||
) -> types.ApiGetCurrencyResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/currency", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetCurrencyResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def mini_ticker_v1(
|
||||
self, req: types.ApiMiniTickerRequest
|
||||
) -> types.ApiMiniTickerResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/mini", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiMiniTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def ticker_v1(
|
||||
self, req: types.ApiTickerRequest
|
||||
) -> types.ApiTickerResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/ticker", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTickerResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def orderbook_levels_v1(
|
||||
self, req: types.ApiOrderbookLevelsRequest
|
||||
) -> types.ApiOrderbookLevelsResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/book", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderbookLevelsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def trade_v1(self, req: types.ApiTradeRequest) -> types.ApiTradeResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/trade", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def trade_history_v1(
|
||||
self, req: types.ApiTradeHistoryRequest
|
||||
) -> types.ApiTradeHistoryResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/trade_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTradeHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def candlestick_v1(
|
||||
self, req: types.ApiCandlestickRequest
|
||||
) -> types.ApiCandlestickResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/kline", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCandlestickResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def funding_rate_v1(
|
||||
self, req: types.ApiFundingRateRequest
|
||||
) -> types.ApiFundingRateResponse | GrvtError:
|
||||
resp = self._post(False, self.md_rpc + "/full/v1/funding", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFundingRateResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def create_order_v1(
|
||||
self, req: types.ApiCreateOrderRequest
|
||||
) -> types.ApiCreateOrderResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/create_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiCreateOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def cancel_order_v1(
|
||||
self, req: types.ApiCancelOrderRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/cancel_order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def cancel_all_orders_v1(
|
||||
self, req: types.ApiCancelAllOrdersRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/cancel_all_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def get_order_v1(
|
||||
self, req: types.ApiGetOrderRequest
|
||||
) -> types.ApiGetOrderResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/order", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiGetOrderResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def open_orders_v1(
|
||||
self, req: types.ApiOpenOrdersRequest
|
||||
) -> types.ApiOpenOrdersResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/open_orders", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOpenOrdersResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def order_history_v1(
|
||||
self, req: types.ApiOrderHistoryRequest
|
||||
) -> types.ApiOrderHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/order_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiOrderHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def cancel_on_disconnect_v1(
|
||||
self, req: types.ApiCancelOnDisconnectRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/cancel_on_disconnect", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def fill_history_v1(
|
||||
self, req: types.ApiFillHistoryRequest
|
||||
) -> types.ApiFillHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/fill_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiFillHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def positions_v1(
|
||||
self, req: types.ApiPositionsRequest
|
||||
) -> types.ApiPositionsResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/positions", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiPositionsResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def funding_payment_history_v1(
|
||||
self, req: types.ApiFundingPaymentHistoryRequest
|
||||
) -> types.ApiFundingPaymentHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/funding_payment_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingPaymentHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def deposit_history_v1(
|
||||
self, req: types.ApiDepositHistoryRequest
|
||||
) -> types.ApiDepositHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/deposit_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiDepositHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def transfer_v1(
|
||||
self, req: types.ApiTransferRequest
|
||||
) -> types.ApiTransferResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/transfer", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def transfer_history_v1(
|
||||
self, req: types.ApiTransferHistoryRequest
|
||||
) -> types.ApiTransferHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/transfer_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiTransferHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def withdrawal_v1(
|
||||
self, req: types.ApiWithdrawalRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/withdrawal", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def withdrawal_history_v1(
|
||||
self, req: types.ApiWithdrawalHistoryRequest
|
||||
) -> types.ApiWithdrawalHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/withdrawal_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiWithdrawalHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def sub_account_summary_v1(
|
||||
self, req: types.ApiSubAccountSummaryRequest
|
||||
) -> types.ApiSubAccountSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def sub_account_history_v1(
|
||||
self, req: types.ApiSubAccountHistoryRequest
|
||||
) -> types.ApiSubAccountHistoryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/account_history", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSubAccountHistoryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def aggregated_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiAggregatedAccountSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/aggregated_account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiAggregatedAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def funding_account_summary_v1(
|
||||
self, req: types.EmptyRequest
|
||||
) -> types.ApiFundingAccountSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/funding_account_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiFundingAccountSummaryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def set_derisk_mm_ratio_v1(
|
||||
self, req: types.ApiSetDeriskToMaintenanceMarginRatioRequest
|
||||
) -> types.ApiSetDeriskToMaintenanceMarginRatioResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/set_derisk_mm_ratio", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiSetDeriskToMaintenanceMarginRatioResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def get_all_initial_leverage_v1(
|
||||
self, req: types.ApiGetAllInitialLeverageRequest
|
||||
) -> types.ApiGetAllInitialLeverageResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/get_all_initial_leverage", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiGetAllInitialLeverageResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def set_initial_leverage_v1(
|
||||
self, req: types.ApiSetInitialLeverageRequest
|
||||
) -> types.ApiSetInitialLeverageResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/set_initial_leverage", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiSetInitialLeverageResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_burn_tokens_v1(
|
||||
self, req: types.ApiVaultBurnTokensRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_burn_tokens", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_invest_v1(
|
||||
self, req: types.ApiVaultInvestRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_invest", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_investor_summary_v1(
|
||||
self, req: types.ApiVaultInvestorSummaryRequest
|
||||
) -> types.ApiVaultInvestorSummaryResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_investor_summary", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.ApiVaultInvestorSummaryResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_redeem_v1(
|
||||
self, req: types.ApiVaultRedeemRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_redeem_cancel_v1(
|
||||
self, req: types.ApiVaultRedeemCancelRequest
|
||||
) -> types.AckResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_redeem_cancel", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(types.AckResponse, resp, Config(cast=[Enum]))
|
||||
|
||||
def vault_redemption_queue_v1(
|
||||
self, req: types.ApiVaultViewRedemptionQueueRequest
|
||||
) -> types.ApiVaultViewRedemptionQueueResponse | GrvtError:
|
||||
resp = self._post(True, self.td_rpc + "/full/v1/vault_view_redemption_queue", req)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiVaultViewRedemptionQueueResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
|
||||
def query_vault_manager_investor_history_v1(
|
||||
self, req: types.ApiQueryVaultManagerInvestorHistoryRequest
|
||||
) -> types.ApiQueryVaultManagerInvestorHistoryResponse | GrvtError:
|
||||
resp = self._post(
|
||||
True, self.td_rpc + "/full/v1/vault_manager_investor_history", req
|
||||
)
|
||||
if resp.get("code"):
|
||||
return GrvtError(**resp)
|
||||
return from_dict(
|
||||
types.ApiQueryVaultManagerInvestorHistoryResponse, resp, Config(cast=[Enum])
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user