mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-11 09:18:08 +00:00
更新环境配置示例,添加 GRVT 相关的 API 凭证和选项,增强文档以支持新的交易所适配器,确保用户能够正确配置和使用 GRVT 交易功能。
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user