3159 lines
147 KiB
Python
3159 lines
147 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
from ccxt.base.exchange import Exchange
|
|
from ccxt.abstract.grvt import ImplicitAPI
|
|
import math
|
|
from ccxt.base.types import Any, Balances, Currencies, Currency, Int, Leverage, Leverages, MarginMode, MarginModes, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Trade, Transaction, TransferEntry
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import PermissionDenied
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
from ccxt.base.errors import OperationRejected
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OperationFailed
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
from ccxt.base.errors import InvalidNonce
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class grvt(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
rlOthers = 40
|
|
rlOrders = 20
|
|
return self.deep_extend(super(grvt, self).describe(), {
|
|
'id': 'grvt',
|
|
'name': 'GRVT',
|
|
'countries': ['SG'], # Singapore
|
|
'rateLimit': 10,
|
|
'certified': False,
|
|
'version': 'v1',
|
|
'dex': True,
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': False,
|
|
'margin': False,
|
|
'swap': True,
|
|
'future': False,
|
|
'option': False,
|
|
'cancelAllOrders': True,
|
|
'cancelOrder': True,
|
|
'createOrder': True,
|
|
'fetchBalance': True,
|
|
'fetchCurrencies': True,
|
|
'fetchDeposits': True,
|
|
'fetchFundingHistory': True,
|
|
'fetchFundingRateHistory': True,
|
|
'fetchLeverages': True,
|
|
'fetchMarginModes': True,
|
|
'fetchMarkets': True,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrders': True,
|
|
'fetchPositions': True,
|
|
'fetchTicker': True,
|
|
'fetchTrades': True,
|
|
'fetchTransfers': True,
|
|
'fetchWithdrawals': True,
|
|
'setLeverage': True,
|
|
'signIn': True,
|
|
'transfer': True,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': 'CI_1_M',
|
|
'3m': 'CI_3_M',
|
|
'5m': 'CI_5_M',
|
|
'15m': 'CI_15_M',
|
|
'30m': 'CI_30_M',
|
|
'1h': 'CI_1_H',
|
|
'2h': 'CI_2_H',
|
|
'4h': 'CI_4_H',
|
|
'6h': 'CI_6_H',
|
|
'8h': 'CI_8_H',
|
|
'12h': 'CI_12_H',
|
|
'1d': 'CI_1_D',
|
|
'3d': 'CI_3_D',
|
|
'5d': 'CI_5_D',
|
|
'1w': 'CI_1_W',
|
|
'2w': 'CI_2_W',
|
|
'3w': 'CI_3_W',
|
|
'4w': 'CI_4_W',
|
|
},
|
|
'urls': {
|
|
'logo': 'https://github.com/user-attachments/assets/7a2e8108-29f6-45d1-822d-48eb1c8cbbe6',
|
|
'api': {
|
|
'privateEdge': 'https://edge.grvt.io/',
|
|
'privateTrading': 'https://trades.grvt.io/',
|
|
'publicMarket': 'https://market-data.grvt.io/',
|
|
},
|
|
'test': {
|
|
'privateEdge': 'https://edge.testnet.grvt.io/',
|
|
'privateTrading': 'https://trades.testnet.grvt.io/',
|
|
'publicMarket': 'https://market-data.testnet.grvt.io/',
|
|
},
|
|
'www': 'https://grvt.io',
|
|
'referral': 'https://grvt.io/?ref=WBLS9D1',
|
|
'doc': [
|
|
'https://api-docs.grvt.io/',
|
|
],
|
|
'fees': 'https://help.grvt.io/en/articles/9614699-how-does-grvt-s-fee-model-work',
|
|
},
|
|
'api': {
|
|
# RL : https://help.grvt.io/en/articles/9636566-what-are-the-rate-limitations-on-grvt
|
|
'privateEdge': {
|
|
'post': {
|
|
'auth/api_key/login': 100,
|
|
'auth/wallet/login': 100,
|
|
},
|
|
},
|
|
'publicMarket': {
|
|
'post': {
|
|
'full/v1/instrument': 4,
|
|
'full/v1/all_instruments': 4,
|
|
'full/v1/instruments': 4,
|
|
'full/v1/currency': 12,
|
|
'full/v1/margin_rules': 12,
|
|
'full/v1/mini': 4,
|
|
'full/v1/ticker': 4,
|
|
'full/v1/book': 12,
|
|
'full/v1/trade': 12,
|
|
'full/v1/trade_history': 12,
|
|
'full/v1/kline': 12,
|
|
'full/v1/funding': 12,
|
|
},
|
|
},
|
|
'privateTrading': {
|
|
'post': {
|
|
'full/v1/create_order': 5,
|
|
'full/v1/cancel_order': 5,
|
|
'full/v1/cancel_on_disconnect': 100,
|
|
'full/v1/cancel_all_orders': 50,
|
|
'full/v1/order': rlOrders,
|
|
'full/v1/order_history': rlOrders,
|
|
'full/v1/open_orders': rlOrders,
|
|
'full/v1/fill_history': rlOrders,
|
|
'full/v1/positions': rlOrders,
|
|
'full/v1/funding_payment_history': rlOthers,
|
|
'full/v1/get_sub_accounts': rlOthers,
|
|
'full/v1/account_summary': rlOthers,
|
|
'full/v1/account_history': rlOthers,
|
|
'full/v1/aggregated_account_summary': rlOthers,
|
|
'full/v1/funding_account_summary': rlOthers,
|
|
'full/v1/transfer': 100,
|
|
'full/v1/deposit_history': 100,
|
|
'full/v1/transfer_history': 100,
|
|
'full/v1/withdrawal': 100,
|
|
'full/v1/withdrawal_history': 100,
|
|
'full/v1/add_position_margin': rlOthers, # addMargin
|
|
'full/v1/get_position_margin_limits': rlOthers,
|
|
'full/v1/set_position_config': rlOthers, # setPositionMode/setMarginMode
|
|
'full/v1/set_initial_leverage': rlOthers,
|
|
'full/v1/get_all_initial_leverage': rlOthers,
|
|
'full/v1/set_derisk_mm_ratio': rlOthers,
|
|
'full/v1/vault_burn_tokens': rlOthers,
|
|
'full/v1/vault_invest': rlOthers,
|
|
'full/v1/vault_investor_summary': rlOthers,
|
|
'full/v1/vault_redeem': rlOthers,
|
|
'full/v1/vault_redeem_cancel': rlOthers,
|
|
'full/v1/vault_view_redemption_queue': rlOthers,
|
|
'full/v1/vault_manager_investor_history': rlOthers,
|
|
'full/v1/authorize_builder': rlOthers, # https://pastebin(dot)com/0Mb8cFhN
|
|
'full/v1/get_authorized_builders': rlOthers,
|
|
'full/v1/builder_fill_history': rlOthers,
|
|
},
|
|
},
|
|
},
|
|
# exchange-specific options
|
|
'options': {
|
|
'accountId': None, # needs to be set manually by user
|
|
# https://api.rhino.fi/bridge/configs
|
|
'networks': {
|
|
'ARBONE': '42161',
|
|
'AVAXC': '43114',
|
|
'BASE': '8453',
|
|
'BSC': '56',
|
|
'ETH': '1',
|
|
'ERC20': '1',
|
|
'OP': '10',
|
|
'SOL': '900',
|
|
'TRX': '728126428',
|
|
'ZKSYNCERA': '324',
|
|
'KAIA': '8217',
|
|
},
|
|
'networksById': {
|
|
'1': 'ERC20',
|
|
},
|
|
'builderFee': True,
|
|
'builder': '0x21d2a053495994b1132a38cd1171acec40c6741e',
|
|
'builderRate': 0.01,
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'features': {
|
|
'default': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': {
|
|
'last': True,
|
|
'mark': True,
|
|
'index': True,
|
|
'median': True, # mid
|
|
},
|
|
'triggerDirection': True,
|
|
'stopLossPrice': True,
|
|
'takeProfitPrice': True,
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': False,
|
|
},
|
|
'hedged': False,
|
|
'leverage': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'marketBuyByCost': False,
|
|
'selfTradePrevention': False,
|
|
'trailing': False,
|
|
'iceberg': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 1000,
|
|
'daysBack': 1000,
|
|
'untilDays': 1000,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': None,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': False,
|
|
'limit': 1000,
|
|
'daysBack': None,
|
|
'untilDays': None,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchClosedOrders': None,
|
|
'fetchOHLCV': {
|
|
'limit': 1000,
|
|
},
|
|
},
|
|
'spot': None,
|
|
'swap': {
|
|
'linear': {
|
|
'extends': 'default',
|
|
},
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'requiredCredentials': {
|
|
'privateKey': True,
|
|
'apiKey': False,
|
|
'secret': False,
|
|
},
|
|
'quoteJsonNumbers': False, # needed for some endpoints(todo: specify in implementations)
|
|
'exceptions': {
|
|
'exact': {
|
|
'1000': AuthenticationError, # "You need to authenticate prior to using self functionality"
|
|
'1001': PermissionDenied, # "You are not authorized to access self functionality"
|
|
'1002': OperationFailed, # "Internal Server Error"
|
|
'1003': BadRequest, # "Request could not be processed due to malformed syntax"
|
|
'1004': OperationRejected, # "Data Not Found"
|
|
'1005': OperationFailed, # "Unknown Error"
|
|
'1006': RateLimitExceeded, # "You have surpassed the allocated rate limit for your tier"
|
|
'1008': PermissionDenied, # "Your IP has not been whitelisted for access"
|
|
'1009': OperationRejected, # "We are temporarily deactivating self API endpoint, please try again later"
|
|
'1012': BadRequest, # "Invalid signature chain ID"
|
|
'1400': PermissionDenied, # "Signer does not have trade permission"
|
|
'2000': PermissionDenied, # "Signature is from an unauthorized signer"
|
|
'2001': InvalidNonce, # "Signature has expired"
|
|
'2002': BadRequest, # "Signature does not match payload"
|
|
'2003': PermissionDenied, # "Order sub account does not match logged in user"
|
|
'2004': InvalidNonce, # "Signature is from an expired session key"
|
|
'2005': BadRequest, # "Signature V must be 27/28"
|
|
'2006': BadRequest, # "Signature R/S must have exactly 64 characters long without 0x prefix"
|
|
'2007': BadRequest, # "Signature S must be in the lower half of the curve"
|
|
'2008': BadRequest, # "Signature exceeds maximum allowed duration."
|
|
'2010': InvalidOrder, # "Order ID should be empty when creating an order"
|
|
'2011': InvalidOrder, # "Client Order ID should be supplied when creating an order"
|
|
'2012': InvalidOrder, # "Client Order ID overlaps with existing active order"
|
|
'2020': InvalidOrder, # "Market Order must always be supplied without a limit price"
|
|
'2021': InvalidOrder, # "Limit Order must always be supplied with a limit price"
|
|
'2030': InvalidOrder, # "Orderbook Orders must have a TimeInForce of GTT/IOC/FOK"
|
|
'2031': InvalidOrder, # "RFQ Orders must have a TimeInForce of GTT/AON/IOC/FOK"
|
|
'2032': InvalidOrder, # "Post Only can only be set to True for GTT/AON orders"
|
|
'2040': InvalidOrder, # "Order must contain at least one leg"
|
|
'2041': InvalidOrder, # "Order Legs must be sorted by Derivative.Instrument/Underlying/BaseCurrency/Expiration/StrikePrice"
|
|
'2042': InvalidOrder, # "Orderbook Orders must contain only one leg"
|
|
'2050': InvalidOrder, # "Order state must be empty upon creation"
|
|
'2051': InvalidOrder, # "Order execution metadata must be empty upon creation"
|
|
'2060': BadSymbol, # "Order Legs contain one or more inactive derivative"
|
|
'2061': BadSymbol, # "Unsupported Instrument Requested"
|
|
'2062': InvalidOrder, # "Order size smaller than min size"
|
|
'2063': InvalidOrder, # "Order size smaller than min block size in block trade venue"
|
|
'2064': InvalidOrder, # "Invalid limit price tick"
|
|
'2065': InvalidOrder, # "Order size too granular"
|
|
'2070': InvalidOrder, # "Liquidation Order is not supported"
|
|
'2080': InsufficientFunds, # "Insufficient margin to create order"
|
|
'2081': OperationRejected, # "Order Fill would result in exceeding maximum position size"
|
|
'2082': InvalidOrder, # "Pre-order check failed"
|
|
'2083': OperationRejected, # "Order Fill would result in exceeding maximum position size under current configurable leverage tier"
|
|
'2090': RateLimitExceeded, # "Max open orders exceeded"
|
|
'2100': BadRequest, # "Invalid initial leverage"
|
|
'2101': BadRequest, # "Vaults cannot configure leverage"
|
|
'2102': OperationRejected, # "Margin type change failed, has open position for self instrument"
|
|
'2103': OperationRejected, # "Margin type change failed, has open orders for self instrument"
|
|
'2104': BadRequest, # "Margin type not supported"
|
|
'2105': BadRequest, # "Margin type change failed"
|
|
'2107': BadRequest, # "Attempted to set leverage below minimum"
|
|
'2108': BadRequest, # "Attempted to set leverage above maximum"
|
|
'2110': InvalidOrder, # "Invalid trigger by"
|
|
'2111': InvalidOrder, # "Unsupported trigger by"
|
|
'2112': InvalidOrder, # "Invalid trigger order"
|
|
'2113': InvalidOrder, # "Trigger price must be non-zero"
|
|
'2114': InvalidOrder, # "Invalid position linked TPSL orders, position linked TPSL must be a reduce-only order"
|
|
'2115': InvalidOrder, # "Invalid position linked TPSL orders, position linked TPSL must not have smaller size than the position"
|
|
'2116': InvalidOrder, # "Position linked TPSL order for self asset already exists"
|
|
'2117': InvalidOrder, # "Position linked TPSL orders must be created from web or mobile clients"
|
|
'2300': OperationRejected, # "Order cancel time-to-live settings currently disabled."
|
|
'2301': OperationRejected, # "Order cancel time-to-live exceeds maximum allowed value."
|
|
'2400': OperationRejected, # "Reduce only order with no position"
|
|
'2401': OperationRejected, # "Reduce only order must not increase position size"
|
|
'2402': OperationRejected, # "Reduce only order size exceeds maximum allowed value"
|
|
'3000': BadSymbol, # "Instrument is invalid"
|
|
'3004': OperationRejected, # "Instrument does not have a valid maintenance margin configuration"
|
|
'3005': OperationRejected, # "Instrument's underlying currency does not have a valid balance decimal configuration"
|
|
'3006': OperationRejected, # "Instrument's quote currency does not have a valid balance decimal configuration"
|
|
'3021': BadRequest, # "Either order ID or client order ID must be supplied"
|
|
'3031': BadRequest, # "Depth is invalid"
|
|
'4000': InsufficientFunds, # "Insufficient balance to complete transfer"
|
|
'4002': OperationFailed, # "Transfer failed with an unrefined failure reason, please report to GRVT"
|
|
'4010': OperationRejected, # "This wallet is not supported. Please try another wallet."
|
|
'5000': OperationRejected, # "Transfer Metadata does not match the expected structure."
|
|
'5001': OperationRejected, # "Transfer Provider does not match the expected provider."
|
|
'5002': OperationRejected, # "Direction of the transfer does not match the expected direction."
|
|
'5003': OperationRejected, # "Endpoint account ID is invalid."
|
|
'5004': OperationRejected, # "Funding account does not exist in our system."
|
|
'5005': OperationRejected, # "Invalid ChainID for the transfer request."
|
|
'6000': OperationRejected, # "Countdown time is bigger than 300s supported"
|
|
'6100': OperationRejected, # "Derisk MM Ratio is out of range"
|
|
'7000': OperationRejected, # "Vault ID provided is invalid and does not belong to any vault"
|
|
'7001': InsufficientFunds, # "Vault does not have sufficient LP token balance"
|
|
'7002': OperationFailed, # "User has an ongoing redemption"
|
|
'7003': OperationRejected, # "This vault has been delisted/closed."
|
|
'7004': OperationRejected, # "This investment would cause the vault to exceed its valuation cap."
|
|
'7005': InsufficientFunds, # "You are attempting to burn more vault tokens than you own."
|
|
'7006': OperationFailed, # "You are attempting to burn vault tokens whilst having an active redemption request."
|
|
'7007': PermissionDenied, # "The investor is not an LP for self vault."
|
|
'7100': OperationFailed, # "Unknown transaction type"
|
|
'7101': OperationRejected, # "Transfer account not found"
|
|
'7102': OperationRejected, # "Transfer sub-account not found"
|
|
'7103': OperationRejected, # "Charged trading fee below the config minimum"
|
|
'7201': OperationRejected, # "Attempted to create a limit order at a price outside of asset's price protection band."
|
|
'7450': OperationRejected, # "Add margin failed"
|
|
'7451': OperationRejected, # "Add margin to empty position"
|
|
'7452': OperationRejected, # "Add margin to non isolated position"
|
|
'7453': OperationRejected, # "Max addable amount exceeded"
|
|
'7454': OperationRejected, # "Max removable amount exceeded"
|
|
'7455': OperationRejected, # "Not isolated margin position"
|
|
'7500': OperationRejected, # "Builder Fee exceeds the allowed program limit."
|
|
'7501': BadRequest, # "Builder Fee can't be negative."
|
|
'7502': OperationRejected, # "Builder Account does not exist."
|
|
'7503': OperationRejected, # "Builder is already authorized for self account with the given fee."
|
|
'7504': OperationRejected, # "Builder is not authorized for the specified user.","status":400
|
|
},
|
|
'broad': {},
|
|
},
|
|
})
|
|
|
|
def eip_definitions(self):
|
|
return {
|
|
'EIP712_ORDER_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'},
|
|
],
|
|
},
|
|
'EIP712_ORDER_WITH_BUILDER_TYPE': {
|
|
'OrderWithBuilderFee': [
|
|
{'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': 'builder', 'type': 'address'},
|
|
{'name': 'builderFee', 'type': 'uint32'},
|
|
{'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'},
|
|
],
|
|
},
|
|
'EIP712_TRANSFER_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'},
|
|
],
|
|
},
|
|
'EIP712_WITHDRAWAL_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'},
|
|
],
|
|
},
|
|
'EIP712_BUILDER_APPROVAL_TYPE': {
|
|
'AuthorizeBuilder': [
|
|
{'name': 'mainAccountID', 'type': 'address'},
|
|
{'name': 'builderAccountID', 'type': 'address'},
|
|
{'name': 'maxFutureFeeRate', 'type': 'uint32'},
|
|
{'name': 'maxSpotFeeRate', 'type': 'uint32'},
|
|
{'name': 'nonce', 'type': 'uint32'},
|
|
{'name': 'expiration', 'type': 'int64'},
|
|
],
|
|
},
|
|
'EIP712_WALLETLOGIN_TYPE': {
|
|
'WalletLogin': [
|
|
{'name': 'signer', 'type': 'address'},
|
|
{'name': 'nonce', 'type': 'uint32'},
|
|
{'name': 'expiration', 'type': 'int64'},
|
|
],
|
|
},
|
|
}
|
|
|
|
def uses_private_key(self):
|
|
privateKeyDefined = self.privateKey is not None and self.privateKey != ''
|
|
apiKeyDefined = self.apiKey is not None and self.apiKey != ''
|
|
if privateKeyDefined and apiKeyDefined:
|
|
raise ExchangeError('You should provide either "privateKey" or "apikey & secret"')
|
|
return privateKeyDefined
|
|
|
|
def sign_in(self, params={}):
|
|
"""
|
|
sign in, must be called prior to using other authenticated methods
|
|
|
|
https://api-docs.grvt.io/#authentication
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns: response from exchange
|
|
"""
|
|
# if self.uses_private_key():
|
|
# self.sign_in_with_private_key(params)
|
|
# self.initialize_client(params)
|
|
# else:
|
|
# self.sign_in_with_api_key(params)
|
|
# }
|
|
if self.privateKey is None or self.privateKey == '':
|
|
raise PermissionDenied('Private key is required for self operation. If you used joined GRVT through email registration instead of Web3 wallet, then read: https://github.com/ccxt/ccxt/wiki/FAQ#how-to-use-the-grvt-exchange-in-ccxt')
|
|
self.sign_in_with_private_key(params)
|
|
self.initialize_client(params)
|
|
self.load_account_infos()
|
|
return True
|
|
|
|
def sign_in_with_api_key(self, params={}):
|
|
now = self.milliseconds()
|
|
# expires in 24 hours suggested
|
|
expires = self.safe_integer(self.options, 'signInExpiration', 0)
|
|
# if previous sign-in not expired(give 10 seconds margin)
|
|
if expires is not None and expires > now + 10000:
|
|
return {}
|
|
request = {
|
|
'api_key': self.apiKey,
|
|
}
|
|
response = self.privateEdgePostAuthApiKeyLogin(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "location": "",
|
|
# "status": "success"
|
|
# }
|
|
#
|
|
self.options['signInExpiration'] = now + 86400000 # 24 hours
|
|
return response
|
|
|
|
def sign_in_with_private_key(self, params={}):
|
|
self.check_required_credentials()
|
|
now = self.milliseconds()
|
|
# expires in 24 hours suggested
|
|
expires = self.safe_integer(self.options, 'signInExpiration', 0)
|
|
# if previous sign-in not expired(give 10 seconds margin)
|
|
if expires is not None and expires > now + 10000:
|
|
return {}
|
|
walletAddress = self.eth_get_address_from_private_key(self.privateKey)
|
|
request: dict = {
|
|
'address': walletAddress,
|
|
'signature': self.default_signature(),
|
|
}
|
|
request = self.create_signed_request(request, 'EIP712_WALLETLOGIN_TYPE')
|
|
response = self.privateEdgePostAuthWalletLogin(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "location": "",
|
|
# "status": "success"
|
|
# }
|
|
#
|
|
self.options['signInExpiration'] = now + 86400000 # 24 hours
|
|
return response
|
|
|
|
def initialize_client(self, params={}):
|
|
builderFee = self.safe_bool(params, 'builderFee', self.safe_bool(self.options, 'builderFee', True)) # we shouldn't omit here
|
|
if not builderFee:
|
|
return False # skip if builder fee is not enabled
|
|
approvedBuilderFee = self.safe_bool(self.options, 'approvedBuilderFee', False)
|
|
if approvedBuilderFee:
|
|
return True # skip if builder fee is already approved
|
|
results = [self.privateTradingPostFullV1GetAuthorizedBuilders(), self.load_account_infos()]
|
|
#
|
|
# {
|
|
# "results": [{
|
|
# "builder_account_id": "GRVT_MAIN_ACCOUNT_ID_HERE",
|
|
# "max_futures_fee_rate": 0.001,
|
|
# "max_spot_fee_rate": 0.0001
|
|
# }]
|
|
# }
|
|
#
|
|
currentBuilders = results[0]
|
|
approvedBuilder = self.safe_list(currentBuilders, 'results', [])
|
|
length = len(approvedBuilder)
|
|
found = False
|
|
for i in range(0, length):
|
|
builderInfo = self.safe_dict(approvedBuilder, i, {})
|
|
builderAccountId = self.safe_string(builderInfo, 'builder_account_id')
|
|
if builderAccountId == self.safe_string(self.options, 'builder'):
|
|
found = True
|
|
break
|
|
if found:
|
|
self.options['approvedBuilderFee'] = True
|
|
else:
|
|
try:
|
|
defaultFromAccountId = self.safe_string(self.options, 'userMainAccountId') # self.eth_get_address_from_private_key(self.secret) # self.safe_string(self.options, 'userMainAccountId')
|
|
request: dict = {
|
|
'main_account_id': defaultFromAccountId,
|
|
'builder_account_id': self.safe_string(self.options, 'builder'),
|
|
'max_futures_fee_rate': self.safe_string(self.options, 'builderRate'),
|
|
'max_spot_fee_rate': self.safe_string(self.options, 'builderRate'),
|
|
'signature': self.default_signature(),
|
|
}
|
|
request = self.create_signed_request(request, 'EIP712_BUILDER_APPROVAL_TYPE')
|
|
authResponse = self.privateTradingPostFullV1AuthorizeBuilder(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "ack": "true",
|
|
# "tx_id":"0"
|
|
# }
|
|
# }
|
|
#
|
|
authResult = self.safe_dict(authResponse, 'result')
|
|
ack = self.safe_bool(authResult, 'ack')
|
|
if not ack:
|
|
raise ExchangeError('Builder authorization failed, ' + self.json(authResponse))
|
|
self.options['approvedBuilderFee'] = True
|
|
except Exception as e:
|
|
self.options['builderFee'] = False # disable builder fee if an error occurs
|
|
return None # just c#
|
|
|
|
def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets
|
|
|
|
https://api-docs.grvt.io/market_data_api/#get-instrument-prod
|
|
|
|
:param dict [params]: extra parameters specific to the exchange api endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
marketsPromise = self.publicMarketPostFullV1AllInstruments(params)
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "instrument": "AAVE_USDT_Perp",
|
|
# "instrument_hash": "0x032201",
|
|
# "base": "AAVE",
|
|
# "quote": "USDT",
|
|
# "kind": "PERPETUAL",
|
|
# "venues": [
|
|
# "ORDERBOOK",
|
|
# "RFQ"
|
|
# ],
|
|
# "settlement_period": "PERPETUAL",
|
|
# "base_decimals": "9",
|
|
# "quote_decimals": "6",
|
|
# "tick_size": "0.01",
|
|
# "min_size": "0.1",
|
|
# "create_time": "1764303867576216941",
|
|
# "max_position_size": "3000.0",
|
|
# "funding_interval_hours": "8",
|
|
# "adjusted_funding_rate_cap": "0.75",
|
|
# "adjusted_funding_rate_floor": "-0.75"
|
|
# },
|
|
# ...
|
|
#
|
|
promises = [marketsPromise]
|
|
if not self.is_empty_string(self.apiKey) or not self.is_empty_string(self.privateKey):
|
|
promises.append(self.sign_in())
|
|
results = promises
|
|
response = results[0]
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_markets(result)
|
|
|
|
def parse_market(self, market) -> Market:
|
|
#
|
|
# {
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "instrument_hash": "0x030501",
|
|
# "base": "BTC",
|
|
# "quote": "USDT",
|
|
# "kind": "PERPETUAL",
|
|
# "venues": [
|
|
# "ORDERBOOK",
|
|
# "RFQ"
|
|
# ],
|
|
# "settlement_period": "PERPETUAL",
|
|
# "base_decimals": 9,
|
|
# "quote_decimals": 6,
|
|
# "tick_size": "0.1",
|
|
# "min_size": "0.001",
|
|
# "create_time": "1768040726362828205",
|
|
# "max_position_size": "1000.0",
|
|
# "funding_interval_hours": 8,
|
|
# "adjusted_funding_rate_cap": "0.3",
|
|
# "adjusted_funding_rate_floor": "-0.3",
|
|
# "min_notional": "100.0"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(market, 'instrument')
|
|
baseId = self.safe_string(market, 'base')
|
|
quoteId = self.safe_string(market, 'quote')
|
|
settleId = quoteId
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
settle = self.safe_currency_code(settleId)
|
|
symbol = base + '/' + quote + ':' + settle
|
|
type: Str = None
|
|
typeRaw = self.safe_string(market, 'kind')
|
|
if typeRaw == 'PERPETUAL':
|
|
type = 'swap'
|
|
isSpot = (type == 'spot')
|
|
isSwap = (type == 'swap')
|
|
isFuture = (type == 'future')
|
|
isContract = isSwap or isFuture
|
|
return {
|
|
'id': marketId,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': settle,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': settleId,
|
|
'type': type,
|
|
'spot': isSpot,
|
|
'margin': False,
|
|
'swap': isSwap,
|
|
'future': isFuture,
|
|
'option': False,
|
|
'active': None, # todo: ask support to add
|
|
'contract': isContract,
|
|
'linear': True if isSwap else None,
|
|
'inverse': False if isSwap else None,
|
|
'contractSize': self.parse_number('1'), # tbd, vague response from support
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': self.safe_number(market, 'min_size'), # confirmed, not 'base_decimals'
|
|
'price': self.safe_number(market, 'tick_size'),
|
|
'base': self.parse_number(self.parse_precision(self.safe_string(market, 'base_decimals'))),
|
|
'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quote_decimals'))),
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'amount': {
|
|
'min': self.safe_number(market, 'min_size'),
|
|
'max': self.safe_number(market, 'max_position_size'),
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': self.safe_number(market, 'min_notional'),
|
|
'max': None,
|
|
},
|
|
},
|
|
'created': self.safe_integer_product(market, 'create_time', 0.000001),
|
|
'info': market,
|
|
}
|
|
|
|
def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://api-docs.grvt.io/market_data_api/#get-currency-response
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
request = {'': ''} # workaround for php [] empty arr
|
|
response = self.publicMarketPostFullV1Currency(request)
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "id": "4",
|
|
# "symbol": "ETH",
|
|
# "balance_decimals": "9",
|
|
# "quantity_multiplier": "1000000000"
|
|
# },
|
|
# ..
|
|
#
|
|
responseResult = self.safe_list(response, 'result', [])
|
|
return self.parse_currencies(responseResult)
|
|
|
|
def parse_currency(self, rawCurrency: dict) -> Currency:
|
|
#
|
|
# {
|
|
# "id": "4",
|
|
# "symbol": "ETH",
|
|
# "balance_decimals": "9",
|
|
# "quantity_multiplier": "1000000000"
|
|
# },
|
|
#
|
|
id = self.safe_string(rawCurrency, 'symbol')
|
|
code = self.safe_currency_code(id)
|
|
return self.safe_currency_structure({
|
|
'info': rawCurrency,
|
|
'id': id,
|
|
'code': code,
|
|
'name': None,
|
|
'active': None,
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
'fee': None,
|
|
'precision': self.parse_number(self.parse_precision(self.safe_string(rawCurrency, 'balance_decimals'))),
|
|
'limits': {
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'deposit': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'type': 'crypto', # only crypto for now
|
|
'networks': None,
|
|
'numericId': self.safe_integer(rawCurrency, 'id'),
|
|
})
|
|
|
|
def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
|
|
https://api-docs.grvt.io/market_data_api/#ticker_1
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request = {
|
|
'instrument': self.market_id(symbol),
|
|
}
|
|
response = self.publicMarketPostFullV1Ticker(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "event_time": "1764774730025055205",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "mark_price": "92697.300078773",
|
|
# "index_price": "92727.818122278",
|
|
# "last_price": "92683.0",
|
|
# "last_size": "0.001",
|
|
# "mid_price": "92682.95",
|
|
# "best_bid_price": "92682.9",
|
|
# "best_bid_size": "5.332",
|
|
# "best_ask_price": "92683.0",
|
|
# "best_ask_size": "0.009",
|
|
# "funding_rate_8h_curr": "0.0037",
|
|
# "funding_rate_8h_avg": "0.0037",
|
|
# "interest_rate": "0.0",
|
|
# "forward_price": "0.0",
|
|
# "buy_volume_24h_b": "2893.898",
|
|
# "sell_volume_24h_b": "2907.847",
|
|
# "buy_volume_24h_q": "266955739.1606",
|
|
# "sell_volume_24h_q": "268170211.7109",
|
|
# "high_price": "93908.3",
|
|
# "low_price": "89900.1",
|
|
# "open_price": "90129.2",
|
|
# "open_interest": "1523.218935908",
|
|
# "long_short_ratio": "1.472543",
|
|
# "funding_rate": "0.0037",
|
|
# "next_funding_time": "1764777600000000000"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_ticker(result, market)
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# {
|
|
# "event_time": "1764774730025055205",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "mark_price": "92697.300078773",
|
|
# "index_price": "92727.818122278",
|
|
# "last_price": "92683.0",
|
|
# "last_size": "0.001",
|
|
# "mid_price": "92682.95",
|
|
# "best_bid_price": "92682.9",
|
|
# "best_bid_size": "5.332",
|
|
# "best_ask_price": "92683.0",
|
|
# "best_ask_size": "0.009",
|
|
# "funding_rate_8h_curr": "0.0037",
|
|
# "funding_rate_8h_avg": "0.0037",
|
|
# "interest_rate": "0.0",
|
|
# "forward_price": "0.0",
|
|
# "buy_volume_24h_b": "2893.898",
|
|
# "sell_volume_24h_b": "2907.847",
|
|
# "buy_volume_24h_q": "266955739.1606",
|
|
# "sell_volume_24h_q": "268170211.7109",
|
|
# "high_price": "93908.3",
|
|
# "low_price": "89900.1",
|
|
# "open_price": "90129.2",
|
|
# "open_interest": "1523.218935908",
|
|
# "long_short_ratio": "1.472543",
|
|
# "funding_rate": "0.0037",
|
|
# "next_funding_time": "1764777600000000000"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(ticker, 'instrument')
|
|
return self.safe_ticker({
|
|
'info': ticker,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'open': self.safe_string(ticker, 'open_price'),
|
|
'high': self.safe_string(ticker, 'high_price'),
|
|
'low': self.safe_string(ticker, 'low_price'),
|
|
'last': self.safe_string(ticker, 'last_price'),
|
|
'bid': self.safe_string(ticker, 'best_bid_price'),
|
|
'bidVolume': self.safe_string(ticker, 'best_bid_size'),
|
|
'ask': self.safe_string(ticker, 'best_ask_price'),
|
|
'askVolume': self.safe_string(ticker, 'best_ask_size'),
|
|
'change': None,
|
|
'percentage': None,
|
|
'baseVolume': self.safe_string(ticker, 'buy_volume_24h_b'),
|
|
'quoteVolume': self.safe_string(ticker, 'buy_volume_24h_q'),
|
|
'markPrice': self.safe_string(ticker, 'mark_price'),
|
|
'indexPrice': self.safe_string(ticker, 'index_price'),
|
|
'vwap': None,
|
|
'average': None,
|
|
'previousClose': None,
|
|
})
|
|
|
|
def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://api-docs.grvt.io/market_data_api/#orderbook-levels
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.loc]: crypto location, default: us
|
|
:returns dict: A dictionary of `order book structures <https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure>` indexed by market symbols
|
|
"""
|
|
self.load_markets()
|
|
request = {
|
|
'instrument': self.market_id(symbol),
|
|
}
|
|
if limit is None:
|
|
limit = 100
|
|
if limit <= 500:
|
|
request['depth'] = self.find_nearest_ceiling([10, 50, 100, 500], limit)
|
|
response = self.publicMarketPostFullV1Book(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "event_time": "1764777396650000000",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "bids": [
|
|
# {"price": "92336.0", "size": "0.005", "num_orders": "1"},
|
|
# ...
|
|
# ],
|
|
# "asks": [
|
|
# {"price": "92336.1", "size": "5.711", "num_orders": "37"},
|
|
# ...
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
timestamp = self.parse8601(self.safe_string(result, 'event_time'))
|
|
marketId = self.safe_string(result, 'instrument')
|
|
return self.parse_order_book(result, self.safe_symbol(marketId), timestamp, 'bids', 'asks', 'price', 'size')
|
|
|
|
def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
|
|
https://api-docs.grvt.io/market_data_api/#trade_1
|
|
|
|
:param str symbol: unified symbol of the market
|
|
:param int [since]: timestamp in ms of the earliest item to fetch
|
|
:param int [limit]: the maximum amount of items to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms for the ending date filter, default is the current time
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request = {
|
|
'instrument': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.publicMarketPostFullV1TradeHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "next": "eyJ0cmFkZUlkIjo2NDc5MTAyMywidHJhZGVJbmRleCI6MX0",
|
|
# "result": [
|
|
# {
|
|
# "event_time": "1764779531332118705",
|
|
# "instrument": "ETH_USDT_Perp",
|
|
# "is_taker_buyer": False,
|
|
# "size": "23.73",
|
|
# "price": "3089.88",
|
|
# "mark_price": "3089.360002315",
|
|
# "index_price": "3090.443723246",
|
|
# "interest_rate": "0.0",
|
|
# "forward_price": "0.0",
|
|
# "trade_id": "64796657-1",
|
|
# "venue": "ORDERBOOK",
|
|
# "is_rpi": False
|
|
# },
|
|
# ...
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_trades(result, market, since, limit)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# fetchTrades
|
|
#
|
|
# {
|
|
# "event_time": "1764779531332118705",
|
|
# "instrument": "ETH_USDT_Perp",
|
|
# "size": "23.73",
|
|
# "price": "3089.88",
|
|
# "is_rpi": False,
|
|
# "mark_price": "3089.360002315",
|
|
# "index_price": "3090.443723246",
|
|
# "interest_rate": "0.0",
|
|
# "forward_price": "0.0",
|
|
# "trade_id": "64796657-1",
|
|
# "venue": "ORDERBOOK",
|
|
# "is_taker_buyer": False
|
|
# }
|
|
#
|
|
# fetchMyTrades
|
|
#
|
|
# {
|
|
# "event_time": "1764945709702747558",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "size": "0.001",
|
|
# "price": "90000.0",
|
|
# "is_rpi": False
|
|
# "mark_price": "90050.164063298",
|
|
# "index_price": "90089.803654938",
|
|
# "interest_rate": "0.0",
|
|
# "forward_price": "0.0",
|
|
# "trade_id": "65424692-2",
|
|
# "venue": "ORDERBOOK",
|
|
# "is_buyer": True,
|
|
# "is_taker": False,
|
|
# "broker": "UNSPECIFIED",
|
|
# "realized_pnl": "0.0",
|
|
# "fee": "-0.00009",
|
|
# "fee_rate": "0.0",
|
|
# "order_id": "0x01010105034cddc7000000006621285c",
|
|
# "client_order_id": "1375879248",
|
|
# "signer": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "sub_account_id": "2147050003876484",
|
|
# }
|
|
#
|
|
marketId = self.safe_string(trade, 'instrument')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_integer_product(trade, 'event_time', 0.000001)
|
|
takerOrMaker = None
|
|
isTakerBuyer = self.safe_bool(trade, 'is_taker_buyer')
|
|
side: Str = None
|
|
if isTakerBuyer is not None:
|
|
side = 'buy' if isTakerBuyer else 'sell'
|
|
takerOrMaker = 'taker'
|
|
else:
|
|
takerOrMaker = 'taker' if self.safe_bool(trade, 'is_taker') else 'maker'
|
|
side = 'buy' if self.safe_bool(trade, 'is_buyer') else 'sell'
|
|
fee = None
|
|
feeString = self.safe_string(trade, 'fee')
|
|
if feeString is not None:
|
|
fee = {
|
|
'cost': self.parse_number(feeString),
|
|
'currency': market['quote'],
|
|
'rate': self.safe_number(trade, 'fee_rate'),
|
|
}
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'id': self.safe_string(trade, 'trade_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': market['symbol'],
|
|
'side': side,
|
|
'takerOrMaker': takerOrMaker,
|
|
'price': self.safe_string(trade, 'price'),
|
|
'amount': self.safe_string(trade, 'size'),
|
|
'cost': None,
|
|
'fee': fee,
|
|
'order': self.safe_string(trade, 'order_id'),
|
|
}, market)
|
|
|
|
def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://api-docs.grvt.io/market_data_api/#candlestick_1
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest item to fetch
|
|
:param int [limit]: the maximum amount of items to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms for the ending date filter, default is the current time
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
maxLimit = 1000
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False)
|
|
if paginate:
|
|
return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, maxLimit)
|
|
market = self.market(symbol)
|
|
request = {
|
|
'instrument': market['id'],
|
|
'interval': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
}
|
|
priceTypeMap = {
|
|
'last': 'TRADE',
|
|
'mark': 'MARK',
|
|
'index': 'INDEX',
|
|
# 'median': 'MEDIAN',
|
|
}
|
|
selectedPriceType = self.safe_string(params, 'priceType', 'last')
|
|
request['type'] = self.safe_string(priceTypeMap, selectedPriceType)
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.publicMarketPostFullV1Kline(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "open_time": "1767288240000000000",
|
|
# "close_time": "1767288300000000000",
|
|
# "open": "88178.8",
|
|
# "close": "88176.7",
|
|
# "high": "88192.7",
|
|
# "low": "88176.6",
|
|
# "volume_b": "15.32",
|
|
# "volume_q": "1350962.4782",
|
|
# "trades": 38,
|
|
# "instrument": "BTC_USDT_Perp"
|
|
# },
|
|
# ],
|
|
# "next": "eyJvcGVuVGltZSI6MTc2NzI1ODMwMDAwMDAwMDAwMH0"
|
|
# }
|
|
#
|
|
candles = self.safe_list(response, 'result', [])
|
|
return self.parse_ohlcvs(candles, market, timeframe, since, limit)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# {
|
|
# "open_time": "1767288240000000000",
|
|
# "close_time": "1767288300000000000",
|
|
# "open": "88178.8",
|
|
# "close": "88176.7",
|
|
# "high": "88192.7",
|
|
# "low": "88176.6",
|
|
# "volume_b": "15.32",
|
|
# "volume_q": "1350962.4782",
|
|
# "trades": 38,
|
|
# "instrument": "BTC_USDT_Perp"
|
|
# }
|
|
#
|
|
return [
|
|
self.safe_integer_product(ohlcv, 'open_time', 0.000001),
|
|
self.safe_number(ohlcv, 'open'),
|
|
self.safe_number(ohlcv, 'high'),
|
|
self.safe_number(ohlcv, 'low'),
|
|
self.safe_number(ohlcv, 'close'),
|
|
self.safe_number(ohlcv, 'volume_b'),
|
|
]
|
|
|
|
def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetches historical funding rate prices
|
|
|
|
https://api-docs.grvt.io/market_data_api/#funding-rate
|
|
|
|
:param str symbol: unified symbol of the market to fetch the funding rate history for
|
|
:param int [since]: timestamp in ms of the earliest funding rate to fetch
|
|
:param int [limit]: the maximum amount of `funding rate structures <https://docs.ccxt.com/?id=funding-rate-history-structure>` to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest item
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/?id=funding-rate-history-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument')
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'instrument': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.publicMarketPostFullV1Funding(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "funding_rate": "-0.0034",
|
|
# "funding_time": "1760494260000000000",
|
|
# "mark_price": "112721.159060304",
|
|
# "funding_rate_8_h_avg": "-0.0038",
|
|
# "funding_interval_hours": "0"
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "next": "eyJmdW5kaW5nVGltZSI6MTc2MDQ5NDI2MDAwMDAwMDAwMH0"
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_funding_rate_histories(result, market)
|
|
|
|
def parse_funding_rate_history(self, rawItem, market: Market = None):
|
|
#
|
|
# {
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "funding_rate": "-0.0034",
|
|
# "funding_time": "1760494260000000000",
|
|
# "mark_price": "112721.159060304",
|
|
# "funding_rate_8_h_avg": "-0.0038",
|
|
# "funding_interval_hours": "0"
|
|
# },
|
|
#
|
|
marketId = self.safe_string(rawItem, 'instrument')
|
|
ts = self.safe_integer_product(rawItem, 'funding_time', 0.000001)
|
|
return {
|
|
'info': rawItem,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'fundingRate': self.safe_number(rawItem, 'funding_rate'),
|
|
'timestamp': ts,
|
|
'datetime': self.iso8601(ts),
|
|
}
|
|
|
|
def get_sub_account_id(self, params):
|
|
subAccountId = None
|
|
subAccountId, params = self.handle_option_and_params(params, 'getSubAccountId', 'accountId')
|
|
if subAccountId is None:
|
|
raise ArgumentsRequired(self.id + ' you should set "accountId" in options or params, which can be found in the grvt dashboard, under Api-Keys page')
|
|
return str(subAccountId)
|
|
|
|
def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
query for account info
|
|
|
|
https://api-docs.grvt.io/trading_api/#sub-account-summary
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/?id=balance-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
response = self.privateTradingPostFullV1AccountSummary(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "event_time": "1764863116142428457",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "margin_type": "SIMPLE_CROSS_MARGIN",
|
|
# "settle_currency": "USDT",
|
|
# "unrealized_pnl": "0.0",
|
|
# "total_equity": "15.0",
|
|
# "initial_margin": "0.0",
|
|
# "maintenance_margin": "0.0",
|
|
# "available_balance": "15.0",
|
|
# "spot_balances": [
|
|
# {
|
|
# "currency": "USDT",
|
|
# "balance": "15.0",
|
|
# "index_price": "1.000289735"
|
|
# }
|
|
# ],
|
|
# "positions": [],
|
|
# "settle_index_price": "1.000289735",
|
|
# "derisk_margin": "0.0",
|
|
# "derisk_to_maintenance_margin_ratio": "1.0",
|
|
# "total_cross_equity": "15.0",
|
|
# "cross_unrealized_pnl": "0.0"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_balance(result)
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
#
|
|
# {
|
|
# "event_time": "1764863116142428457",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "margin_type": "SIMPLE_CROSS_MARGIN",
|
|
# "settle_currency": "USDT",
|
|
# "unrealized_pnl": "0.0",
|
|
# "total_equity": "15.0",
|
|
# "initial_margin": "0.0",
|
|
# "maintenance_margin": "0.0",
|
|
# "available_balance": "15.0",
|
|
# "spot_balances": [
|
|
# {
|
|
# "currency": "USDT",
|
|
# "balance": "15.0",
|
|
# "index_price": "1.000289735"
|
|
# }
|
|
# ],
|
|
# "positions": [],
|
|
# "settle_index_price": "1.000289735",
|
|
# "derisk_margin": "0.0",
|
|
# "derisk_to_maintenance_margin_ratio": "1.0",
|
|
# "total_cross_equity": "15.0",
|
|
# "cross_unrealized_pnl": "0.0"
|
|
# }
|
|
#
|
|
timestamp = self.safe_integer_product(response, 'event_time', 0.000001)
|
|
result: dict = {
|
|
'info': response,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
}
|
|
spotBalances = self.safe_list(response, 'spot_balances', [])
|
|
availableBalance = self.safe_string(response, 'available_balance')
|
|
for i in range(0, len(spotBalances)):
|
|
balance = spotBalances[i]
|
|
currencyId = self.safe_string(balance, 'currency')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
account['total'] = self.safe_string(balance, 'balance')
|
|
account['free'] = availableBalance # todo: revise after API team clarification
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all deposits made to an account
|
|
|
|
https://api-docs.grvt.io/trading_api/#transfer
|
|
|
|
:param str [code]: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch deposits for
|
|
:param int [limit]: the maximum number of deposits structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest item
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request: dict = {}
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = [currency['code']]
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
useTransfersEndpoint = self.safe_bool(self.options, 'useTransfersEndpointForDepositsWithdrawals', True)
|
|
if useTransfersEndpoint:
|
|
transfers = self.internal_fetch_transfers(self.extend(request, params), currency, since, limit)
|
|
filteredResults = self.filter_transfers_by_type(transfers, 'deposit', True)
|
|
transactions = self.get_list_from_object_values(filteredResults[0], 'info')
|
|
return self.parse_transactions(transactions, currency, since, limit)
|
|
else:
|
|
response = self.privateTradingPostFullV1DepositHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [{
|
|
# "l_1_hash": "0x10000101000203040506",
|
|
# "l_2_hash": "0x10000101000203040506",
|
|
# "to_account_id": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "1500.0",
|
|
# "initiated_time": "1697788800000000000",
|
|
# "confirmed_time": "1697788800000000000",
|
|
# "from_address": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0"
|
|
# }],
|
|
# "next": "Qw0918="
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_transactions(result, currency, since, limit)
|
|
|
|
def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all withdrawals made from an account
|
|
|
|
https://docs.backpack.exchange/#tag/Capital/operation/get_withdrawals
|
|
|
|
:param str [code]: unified currency code of the currency transferred
|
|
:param int [since]: the earliest time in ms to fetch transfers for(default 24 hours ago)
|
|
:param int [limit]: the maximum number of transfer structures to retrieve(default 50, max 200)
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest item
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request: dict = {}
|
|
currency = None
|
|
if code is None:
|
|
request['currency'] = None
|
|
else:
|
|
currency = self.currency(code)
|
|
request['currency'] = [currency['code']]
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
useTransfersEndpoint = self.safe_bool(self.options, 'useTransfersEndpointForDepositsWithdrawals', True)
|
|
if useTransfersEndpoint:
|
|
transfers = self.internal_fetch_transfers(self.extend(request, params), currency, since, limit)
|
|
filteredResults = self.filter_transfers_by_type(transfers, 'withdrawal', True)
|
|
transactions = self.get_list_from_object_values(filteredResults[0], 'info')
|
|
return self.parse_transactions(transactions, currency, since, limit)
|
|
else:
|
|
response = self.privateTradingPostFullV1WithdrawalHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [{
|
|
# "tx_id": "1028403",
|
|
# "from_account_id": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "to_eth_address": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "1500.0",
|
|
# "signature": {
|
|
# "signer": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "r": "0xb788d96fee91c7cdc35918e0441b756d4000ec1d07d900c73347d9abbc20acc8",
|
|
# "s": "0x3d786193125f7c29c958647da64d0e2875ece2c3f845a591bdd7dae8c475e26d",
|
|
# "v": 28,
|
|
# "expiration": "1697788800000000000",
|
|
# "nonce": 1234567890,
|
|
# "chain_id": "325"
|
|
# },
|
|
# "event_time": "1697788800000000000",
|
|
# "l_1_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
# "l_2_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
# }],
|
|
# "next": "Qw0918="
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_transactions(result, currency, since, limit)
|
|
|
|
def internal_fetch_transfers(self, req, currency: Any = None, since: Int = None, limit: Int = None):
|
|
response = self.privateTradingPostFullV1TransferHistory(req)
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "tx_id": "65119836",
|
|
# "from_account_id": "0xc451b0191351ce308fdfd779d73814c910fc5ecb",
|
|
# "from_sub_account_id": "0",
|
|
# "to_account_id": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "to_sub_account_id": "0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "4.998",
|
|
# "signature": {
|
|
# "signer": "0xf4fdbaf9655bfd607098f4f887aaca58c9667203",
|
|
# "r": "0x5f780b99e5e8516f85e66af49b469eeeeeee724290d7f49f1e84b25ad038fa81",
|
|
# "s": "0x66c76fdb37a25db8c6b368625d96ee91ab1ffca1786d84dc806b08d1460e97bc",
|
|
# "v": "27",
|
|
# "expiration": "1767455807929000000",
|
|
# "nonce": "45905",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "event_time": "1764863808817370541",
|
|
# "transfer_type": "NON_NATIVE_BRIDGE_DEPOSIT",
|
|
# "transfer_metadata": "{\\"provider\\":\\"rhino\\",\\"direction\\":\\"deposit\\",\\"chainid\\":\\"8453\\",\\"endpoint\\":\\"0x01b89ac919ead1bd513b548962075137c683b9ab\\",\\"provider_tx_id\\":\\"0x1dff8c839f8e21b5af7e121a1ae926017e734aafe8c4ae9942756b3091793b4f\\",\\"provider_ref_id\\":\\"6931aefa5f1ab6fcf0d2f856\\"}"
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "next": ""
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'result', [])
|
|
transfers = self.parse_transfers(rows, currency, since, limit)
|
|
return transfers
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# fetchDeposits
|
|
#
|
|
# {
|
|
# "l_1_hash": "0x10000101000203040506",
|
|
# "l_2_hash": "0x10000101000203040506",
|
|
# "to_account_id": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "1500.0",
|
|
# "initiated_time": "1697788800000000000",
|
|
# "confirmed_time": "1697788800000000000",
|
|
# "from_address": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0"
|
|
# }
|
|
#
|
|
# fetchWithdrawals
|
|
#
|
|
# {
|
|
# "tx_id": "1028403",
|
|
# "from_account_id": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "to_eth_address": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "1500.0",
|
|
# "signature": {
|
|
# "signer": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "r": "0xb788d96fee91c7cdc35918e0441b756d4000ec1d07d900c73347d9abbc20acc8",
|
|
# "s": "0x3d786193125f7c29c958647da64d0e2875ece2c3f845a591bdd7dae8c475e26d",
|
|
# "v": 28,
|
|
# "expiration": "1697788800000000000",
|
|
# "nonce": 1234567890,
|
|
# "chain_id": "325"
|
|
# },
|
|
# "event_time": "1697788800000000000",
|
|
# "l_1_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
|
# "l_2_hash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
|
|
# }
|
|
#
|
|
# fetchTransfers
|
|
#
|
|
# {
|
|
# "tx_id": "65119836",
|
|
# "from_account_id": "0xc451b0191351ce308fdfd779d73814c910fc5ecb",
|
|
# "from_sub_account_id": "0",
|
|
# "to_account_id": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "to_sub_account_id": "0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "4.998",
|
|
# "signature": {
|
|
# "signer": "0xf4fdbaf9655bfd607098f4f887aaca58c9667203",
|
|
# "r": "0x5f780b99e5e8516f85e66af49b469eeeeeee724290d7f49f1e84b25ad038fa81",
|
|
# "s": "0x66c76fdb37a25db8c6b368625d96ee91ab1ffca1786d84dc806b08d1460e97bc",
|
|
# "v": "27",
|
|
# "expiration": "1767455807929000000",
|
|
# "nonce": "45905",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "event_time": "1764863808817370541",
|
|
# "transfer_type": "NON_NATIVE_BRIDGE_DEPOSIT",
|
|
# "transfer_metadata": "{\\"provider\\":\\"rhino\\",\\"direction\\":\\"deposit\\",\\"chainid\\":\\"8453\\",\\"endpoint\\":\\"0x01b89ac919ead1bd513b548962075137c683b9ab\\",\\"provider_tx_id\\":\\"0x1dff8c839f8e21b5af7e121a1ae926017e734aafe8c4ae9942756b3091793b4f\\",\\"provider_ref_id\\":\\"6931aefa5f1ab6fcf0d2f856\\"}"
|
|
# },
|
|
#
|
|
# withdraw
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "ack": "true"
|
|
# }
|
|
# }
|
|
#
|
|
direction: Str = None
|
|
txId: Str = None
|
|
networkCode: Str = None
|
|
addressFrom = self.safe_string(transaction, 'from_account_id')
|
|
addressTo = self.safe_string(transaction, 'to_account_id')
|
|
if 'transfer_metadata' in transaction:
|
|
metaData = self.omit_zero(self.safe_string(transaction, 'transfer_metadata'))
|
|
if metaData is not None:
|
|
parsedMeta = self.parse_json(metaData)
|
|
direction = self.safe_string_lower(parsedMeta, 'direction')
|
|
txId = self.safe_string(parsedMeta, 'provider_tx_id')
|
|
networkCode = self.network_id_to_code(self.safe_string(parsedMeta, 'chainid'))
|
|
if direction == 'withdrawal':
|
|
addressTo = self.safe_string(parsedMeta, 'endpoint')
|
|
elif direction == 'deposit':
|
|
addressFrom = self.safe_string(parsedMeta, 'endpoint')
|
|
timestamp = self.safe_integer_product_2(transaction, 'event_time', 'initiated_time', 0.000001)
|
|
currencyId = self.safe_string(transaction, 'currency')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
return {
|
|
'info': transaction,
|
|
'id': None,
|
|
'txid': txId,
|
|
'type': direction,
|
|
'currency': code,
|
|
'network': networkCode,
|
|
'amount': self.safe_number(transaction, 'num_tokens'),
|
|
'status': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'address': None,
|
|
'addressFrom': addressFrom,
|
|
'addressTo': addressTo,
|
|
'tag': None,
|
|
'tagFrom': None,
|
|
'tagTo': None,
|
|
'updated': None,
|
|
'comment': None,
|
|
'fee': None,
|
|
}
|
|
|
|
def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]:
|
|
"""
|
|
fetch a history of internal transfers made on an account
|
|
|
|
https://api-docs.grvt.io/trading_api/#transfer-history
|
|
|
|
:param str code: unified currency code of the currency transferred
|
|
:param int [since]: the earliest time in ms to fetch transfers for
|
|
:param int [limit]: the maximum number of transfers structures to retrieve(default 10, max 100)
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.paginate]: whether to paginate the results(default False)
|
|
:returns dict[]: a list of `transfer structures <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
if code is None:
|
|
raise ArgumentsRequired(self.id + ' fetchTransfers() requires a code argument')
|
|
self.load_markets_and_sign_in()
|
|
request: dict = {}
|
|
currency = self.currency(code)
|
|
maxLimit = 1000
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchTransfers', 'paginate', False)
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchTransfers', None, since, limit, params, maxLimit)
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.privateTradingPostFullV1TransferHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "tx_id": "65119836",
|
|
# "from_account_id": "0xc451b0191351ce308fdfd779d73814c910fc5ecb",
|
|
# "from_sub_account_id": "0",
|
|
# "to_account_id": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "to_sub_account_id": "0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "4.998",
|
|
# "signature": {
|
|
# "signer": "0xf4fdbaf9655bfd607098f4f887aaca58c9667203",
|
|
# "r": "0x5f780b99e5e8516f85e66af49b469eeeeeee724290d7f49f1e84b25ad038fa81",
|
|
# "s": "0x66c76fdb37a25db8c6b368625d96ee91ab1ffca1786d84dc806b08d1460e97bc",
|
|
# "v": "27",
|
|
# "expiration": "1767455807929000000",
|
|
# "nonce": "45905",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "event_time": "1764863808817370541",
|
|
# "transfer_type": "NON_NATIVE_BRIDGE_DEPOSIT",
|
|
# "transfer_metadata": "{\\"provider\\":\\"rhino\\",\\"direction\\":\\"deposit\\",\\"chainid\\":\\"8453\\",\\"endpoint\\":\\"0x01b89ac919ead1bd513b548962075137c683b9ab\\",\\"provider_tx_id\\":\\"0x1dff8c839f8e21b5af7e121a1ae926017e734aafe8c4ae9942756b3091793b4f\\",\\"provider_ref_id\\":\\"6931aefa5f1ab6fcf0d2f856\\"}"
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "next": ""
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'result', [])
|
|
transfers = self.parse_transfers(rows, currency, since, limit)
|
|
filteredResults = self.filter_transfers_by_type(transfers, 'internal', False)
|
|
return filteredResults[1]
|
|
|
|
def filter_transfers_by_type(self, transfers: Any, transferType: str, onlyMainAccount=True) -> Any:
|
|
matchedResults = []
|
|
nonMatchedResults = []
|
|
for i in range(0, len(transfers)):
|
|
transfer = transfers[i]
|
|
if (onlyMainAccount and transfer['fromAccount'] == '0' and transfer['toAccount'] == '0') or (not onlyMainAccount and (transfer['fromAccount'] != '0' or transfer['toAccount'] != '0')):
|
|
metadata = self.safe_string(transfer['info'], 'transfer_metadata')
|
|
parsedMetadata = self.parse_json(metadata)
|
|
direction = self.safe_string(parsedMetadata, 'direction')
|
|
if direction == transferType:
|
|
matchedResults.append(transfer)
|
|
else:
|
|
nonMatchedResults.append(transfer)
|
|
return [matchedResults, nonMatchedResults]
|
|
|
|
def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
"""
|
|
transfer currency internally between wallets on the same account
|
|
|
|
https://api-docs.grvt.io/trading_api/#transfer_1
|
|
|
|
:param str code: unified currency codeåå
|
|
:param float amount: amount to transfer
|
|
:param str fromAccount: account to transfer from
|
|
:param str toAccount: account to transfer to
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transfer structure <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
currency = self.currency(code)
|
|
defaultFromAccountId = self.safe_string(self.options, 'userMainAccountId')
|
|
if self.in_array(fromAccount, ['trading', 'funding']) and self.in_array(toAccount, ['trading', 'funding']):
|
|
tradingAccountId: Str = None
|
|
tradingAccountId, params = self.handle_option_and_params(params, 'transfer', 'tradingAccountId')
|
|
fundingAccountId: Str = None
|
|
fundingAccountId, params = self.handle_option_and_params(params, 'transfer', 'fundingAccountId')
|
|
if tradingAccountId is None or fundingAccountId is None:
|
|
raise ArgumentsRequired(self.id + ' transfer(): you should set(in the options or params) "tradingAccountId" and "fundingAccountId"(you can use "0" main funding account id)')
|
|
fromAccount = tradingAccountId if (fromAccount == 'trading') else fundingAccountId
|
|
toAccount = tradingAccountId if (toAccount == 'trading') else fundingAccountId
|
|
request: dict = {
|
|
'from_account_id': self.safe_string(params, 'from_account_id', defaultFromAccountId),
|
|
'from_sub_account_id': self.safe_string(params, 'from_sub_account_id', fromAccount),
|
|
'to_account_id': self.safe_string(params, 'to_account_id', defaultFromAccountId),
|
|
'to_sub_account_id': self.safe_string(params, 'to_sub_account_id', toAccount),
|
|
'currency': currency['id'],
|
|
'num_tokens': self.currency_to_precision(code, amount),
|
|
'signature': self.default_signature(),
|
|
'transfer_type': 'STANDARD',
|
|
'transfer_metadata': None,
|
|
}
|
|
request = self.create_signed_request(request, 'EIP712_TRANSFER_TYPE', currency)
|
|
response: dict = None
|
|
try:
|
|
response = self.privateTradingPostFullV1Transfer(self.extend(request, params))
|
|
except Exception as error:
|
|
msg = self.exception_message(error)
|
|
isFromFundingAccount = fromAccount == 'funding'
|
|
if isFromFundingAccount and msg.find('You are not authorized'):
|
|
raise PermissionDenied(self.id + ' transfer() failed. Ensure you use funding api-keys when trying to transfer from Funding accounts: ' + msg)
|
|
raise error
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "ack": "true",
|
|
# "tx_id": "1028403"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_transfer(result, currency)
|
|
|
|
def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
|
|
#
|
|
# transfer
|
|
#
|
|
# {
|
|
# "ack": "true",
|
|
# "tx_id": "1028403"
|
|
# }
|
|
#
|
|
# fetchTransfers
|
|
#
|
|
# {
|
|
# "tx_id": "65119836",
|
|
# "from_account_id": "0xc451b0191351ce308fdfd779d73814c910fc5ecb",
|
|
# "from_sub_account_id": "0",
|
|
# "to_account_id": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "to_sub_account_id": "0",
|
|
# "currency": "USDT",
|
|
# "num_tokens": "4.998",
|
|
# "signature": {
|
|
# "signer": "0xf4fdbaf9655bfd607098f4f887aaca58c9667203",
|
|
# "r": "0x5f780b99e5e8516f85e66af49b469eeeeeee724290d7f49f1e84b25ad038fa81",
|
|
# "s": "0x66c76fdb37a25db8c6b368625d96ee91ab1ffca1786d84dc806b08d1460e97bc",
|
|
# "v": "27",
|
|
# "expiration": "1767455807929000000",
|
|
# "nonce": "45905",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "event_time": "1764863808817370541",
|
|
# "transfer_type": "NON_NATIVE_BRIDGE_DEPOSIT",
|
|
# "transfer_metadata": "{\\"provider\\":\\"rhino\\",\\"direction\\":\\"deposit\\",\\"chainid\\":\\"8453\\",\\"endpoint\\":\\"0x01b89ac919ead1bd513b548962075137c683b9ab\\",\\"provider_tx_id\\":\\"0x1dff8c839f8e21b5af7e121a1ae926017e734aafe8c4ae9942756b3091793b4f\\",\\"provider_ref_id\\":\\"6931aefa5f1ab6fcf0d2f856\\"}"
|
|
# }
|
|
#
|
|
currencyId = self.safe_string(transfer, 'currency')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
timestamp = self.safe_integer_product(transfer, 'event_time', 0.000001)
|
|
return {
|
|
'info': transfer,
|
|
'id': self.safe_string(transfer, 'tx_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'currency': code,
|
|
'amount': self.safe_number(transfer, 'amount'),
|
|
'fromAccount': self.safe_string(transfer, 'from_sub_account_id'),
|
|
'toAccount': self.safe_string(transfer, 'to_sub_account_id'),
|
|
'status': None,
|
|
}
|
|
|
|
def load_account_infos(self):
|
|
if self.safe_string(self.options, 'userMainAccountId') is not None:
|
|
return False
|
|
promises = []
|
|
promises.append(self.privateTradingPostFullV1AggregatedAccountSummary())
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "main_account_id": "0xc73c0c2538fd9b833d20933ccc88fdaa74fcb0d0",
|
|
# "total_equity": "3945034.23",
|
|
# "spot_balances": [{
|
|
# "currency": "USDT",
|
|
# "balance": "123456.78",
|
|
# "index_price": "1.0000102"
|
|
# }],
|
|
# "vault_investments": [{
|
|
# "vault_id": 123456789,
|
|
# "num_lp_tokens": 1000000,
|
|
# "share_price": 1000000,
|
|
# "usd_notional_invested": 1000000
|
|
# }],
|
|
# "total_sub_account_balance": "3945034.23",
|
|
# "total_sub_account_equity": "3945034.23",
|
|
# "total_vault_investments_balance": "3945034.23",
|
|
# "total_sub_account_available_balance": "3945034.23",
|
|
# "total_usd_notional_invested": "3945034.23"
|
|
# }
|
|
# }
|
|
#
|
|
accountIsUndefined = self.safe_string(self.options, 'accountId') is None
|
|
if accountIsUndefined:
|
|
promises.append(self.privateTradingPostFullV1GetSubAccounts())
|
|
#
|
|
# {
|
|
# "sub_account_ids": ["4724219064482495","2095919380","1170592370"]
|
|
# }
|
|
#
|
|
responses = promises
|
|
result1 = self.safe_dict(responses[0], 'result', {})
|
|
mainAccountId = self.safe_string(result1, 'main_account_id')
|
|
self.options['userMainAccountId'] = mainAccountId
|
|
if accountIsUndefined:
|
|
subAccountIds = self.safe_list(responses[1], 'sub_account_ids', [])
|
|
length = len(subAccountIds)
|
|
if length < 1:
|
|
raise ArgumentsRequired(self.id + ' loadAccountInfos(): no sub accounts found, you might need to create an api-key in GRVT website')
|
|
if length > 1:
|
|
raise ArgumentsRequired(self.id + ' loadAccountInfos(): multiple sub accounts found, please set the exchange.options["accountId"] to your preferred sub_account_id from self list: ' + self.json(subAccountIds))
|
|
subAccountId = self.safe_string(subAccountIds, 0)
|
|
self.options['accountId'] = subAccountId
|
|
return True
|
|
|
|
def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
|
|
https://api-docs.grvt.io/trading_api/#withdrawal
|
|
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to
|
|
:param str tag:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str params['network']: the network to withdraw on(mandatory)
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.check_address(address)
|
|
self.load_markets_and_sign_in()
|
|
defaultFromAccountId = self.safe_string(self.options, 'userMainAccountId')
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'to_eth_address': address,
|
|
'from_account_id': defaultFromAccountId,
|
|
'currency': currency['id'],
|
|
'num_tokens': self.currency_to_precision(code, amount),
|
|
'signature': self.default_signature(),
|
|
}
|
|
networkCode, query = self.handle_network_code_and_params(params)
|
|
networkId = self.network_code_to_id(networkCode)
|
|
if networkId is None:
|
|
raise BadRequest(self.id + ' withdraw() requires a network parameter')
|
|
request['signature']['chain_id'] = networkId
|
|
request = self.create_signed_request(request, 'EIP712_WITHDRAWAL_TYPE', currency)
|
|
response = self.privateTradingPostFullV1Withdrawal(self.extend(request, query))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "ack": "true"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_transaction(result, currency)
|
|
|
|
def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
create a trade order
|
|
|
|
https://api-docs.grvt.io/trading_api/#create-order
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of currency you want to trade in units of base currency
|
|
:param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param float [params.triggerPrice]: The price a trigger order is triggered at
|
|
:param float [params.stopLossPrice]: The price a stop loss order is triggered at
|
|
:param float [params.takeProfitPrice]: The price a take profit order is triggered at
|
|
:param str [params.timeInForce]: "GTC", "IOC", or "POST_ONLY"
|
|
:param bool [params.postOnly]: True or False
|
|
:param bool [params.reduceOnly]: Ensures that the executed order does not flip the opened position.
|
|
:param str [params.clientOrderId]: a unique id for the order
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
market = self.market(symbol)
|
|
orderLeg = {
|
|
'instrument': market['id'],
|
|
'size': self.amount_to_precision(symbol, amount),
|
|
}
|
|
if price is not None:
|
|
orderLeg['limit_price'] = self.price_to_precision(symbol, price)
|
|
else:
|
|
orderLeg['limit_price'] = None
|
|
if side == 'sell':
|
|
orderLeg['is_buying_asset'] = False
|
|
elif side == 'buy':
|
|
orderLeg['is_buying_asset'] = True
|
|
else:
|
|
raise InvalidOrder(self.id + ' createOrder(): order side must be either "buy" or "sell"')
|
|
clientOrderId = self.safe_string(params, 'clientOrderId')
|
|
if clientOrderId is None:
|
|
clientOrderId = str(self.nonce()) + '000' + str(self.request_id())
|
|
params = self.omit(params, ['clientOrderId'])
|
|
isMarketOrder = (type == 'market')
|
|
subAccountId = self.get_sub_account_id(params)
|
|
isReduceOnly = self.safe_bool(params, 'reduceOnly', False)
|
|
orderRequest = {
|
|
'sub_account_id': subAccountId,
|
|
'time_in_force': None,
|
|
'legs': [orderLeg],
|
|
'signature': self.default_signature(),
|
|
'metadata': {
|
|
'client_order_id': clientOrderId,
|
|
},
|
|
'is_market': isMarketOrder,
|
|
'post_only': False,
|
|
'reduce_only': isReduceOnly,
|
|
# 'order_id': null,
|
|
# 'state': null,
|
|
}
|
|
timeInForce = self.safe_string_upper(params, 'timeInForce', 'GOOD_TILL_TIME')
|
|
postOnly = self.is_post_only(isMarketOrder, None, params)
|
|
if postOnly:
|
|
orderRequest['post_only'] = True
|
|
if timeInForce is None:
|
|
timeInForce = 'GOOD_TILL_TIME'
|
|
else:
|
|
tifMap = {
|
|
'GTC': 'GOOD_TILL_TIME',
|
|
'FOK': 'FILL_OR_KILL', # tbd: why not 'ALL_OR_NONE'
|
|
'IOC': 'IMMEDIATE_OR_CANCEL',
|
|
}
|
|
timeInForce = self.safe_string(tifMap, timeInForce, timeInForce)
|
|
orderRequest['time_in_force'] = timeInForce
|
|
if not isMarketOrder:
|
|
if postOnly:
|
|
timeInForce = 'POST_ONLY'
|
|
elif timeInForce == 'ioc':
|
|
timeInForce = 'IMMEDIATE_OR_CANCEL'
|
|
params = self.omit(params, ['reduceOnly', 'postOnly', 'timeInForce'])
|
|
# Trigger & SL & TP
|
|
triggerPrice: Str = None
|
|
stopLossPrice: Str = None
|
|
takeProfitPrice: Str = None
|
|
triggerPrice, stopLossPrice, takeProfitPrice, params = self.handle_trigger_prices_and_params(symbol, params)
|
|
if triggerPrice is not None or stopLossPrice is not None or takeProfitPrice is not None:
|
|
# trigger price
|
|
selectedPrice: Str = None
|
|
if triggerPrice is not None:
|
|
selectedPrice = triggerPrice
|
|
elif stopLossPrice is not None:
|
|
selectedPrice = stopLossPrice
|
|
elif takeProfitPrice is not None:
|
|
selectedPrice = takeProfitPrice
|
|
# trigger type
|
|
selectedType: Str = None
|
|
isBuy = (side == 'buy')
|
|
if stopLossPrice is not None:
|
|
selectedType = 'STOP_LOSS' if isBuy else 'TAKE_PROFIT'
|
|
elif takeProfitPrice is not None:
|
|
selectedType = 'TAKE_PROFIT' if isBuy else 'STOP_LOSS'
|
|
else:
|
|
triggerDirection = self.safe_string(params, 'triggerDirection')
|
|
if triggerDirection is None:
|
|
raise ArgumentsRequired(self.id + ' createOrder() requires a triggerDirection parameter when triggerPrice is specified, must be "ascending" or "descending"')
|
|
if triggerDirection is not None:
|
|
if triggerDirection == 'ascending':
|
|
selectedType = 'STOP_LOSS' if isBuy else 'TAKE_PROFIT'
|
|
elif triggerDirection == 'descending':
|
|
selectedType = 'TAKE_PROFIT' if isBuy else 'STOP_LOSS'
|
|
# trigger by
|
|
triggerPriceType = self.safe_string_upper(params, 'triggerPriceType', 'LAST')
|
|
orderRequest['metadata']['trigger'] = {
|
|
'trigger_type': selectedType,
|
|
'tpsl': {
|
|
'trigger_by': triggerPriceType,
|
|
'trigger_price': selectedPrice,
|
|
'close_position': self.safe_bool(params, 'closePosition', False),
|
|
},
|
|
}
|
|
params = self.omit(params, ['triggerDirection', 'triggerPriceType', 'closePosition'])
|
|
eipType = 'EIP712_ORDER_TYPE'
|
|
builderFee = self.safe_bool(params, 'builderFee', self.safe_bool(self.options, 'builderFee', True))
|
|
if builderFee:
|
|
eipType = 'EIP712_ORDER_WITH_BUILDER_TYPE'
|
|
orderRequest['builder'] = self.safe_string(self.options, 'builder')
|
|
orderRequest['builder_fee'] = self.safe_string(self.options, 'builderRate')
|
|
params = self.omit(params, ['builderFee'])
|
|
signedOrderRequest = self.create_signed_request(orderRequest, eipType)
|
|
request = {
|
|
'order': signedOrderRequest,
|
|
}
|
|
response = self.privateTradingPostFullV1CreateOrder(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "order_id": "0x00",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "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": "0xbf465e6083a43b170791ea29393f60...",
|
|
# "r": "0x161826bc2fc43e07b4c1e4aeb01b3e58901f936af10b399e...",
|
|
# "s": "0x1b6d09609430ef73cb53dd87dbe73939824409296b3673719...",
|
|
# "v": 27,
|
|
# "expiration": "1766076771082000000",
|
|
# "nonce": 1766076671,
|
|
# "chain_id": "0"
|
|
# },
|
|
# "metadata": {
|
|
# "client_order_id": "1766076671",
|
|
# "create_time": "1766076671243762741",
|
|
# "trigger": {
|
|
# "trigger_type": "UNSPECIFIED",
|
|
# "tpsl": {
|
|
# "trigger_by": "UNSPECIFIED",
|
|
# "trigger_price": "0.0",
|
|
# "close_position": False
|
|
# }
|
|
# },
|
|
# "broker": "UNSPECIFIED",
|
|
# "is_position_transfer": False,
|
|
# "allow_crossing": False
|
|
# },
|
|
# "state": {
|
|
# "status": "PENDING",
|
|
# "reject_reason": "UNSPECIFIED",
|
|
# "book_size": [
|
|
# "0.001"
|
|
# ],
|
|
# "traded_size": [
|
|
# "0.0"
|
|
# ],
|
|
# "update_time": "1766076671243762741",
|
|
# "avg_fill_price": [
|
|
# "0.0"
|
|
# ]
|
|
# },
|
|
# "builder": "0x00",
|
|
# "builder_fee": "0.0"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'result', {})
|
|
return self.parse_order(data, market)
|
|
|
|
def convert_to_big_int_custom(self, x):
|
|
return int(x)
|
|
|
|
def eip_message_for_order(self, order, structureType):
|
|
priceMultiplier = '1000000000'
|
|
orderLegs = self.safe_list(order, 'legs', [])
|
|
legs = []
|
|
for i in range(0, len(orderLegs)):
|
|
leg = orderLegs[i]
|
|
market = self.market(leg['instrument'])
|
|
bigInt10 = self.convert_to_big_int_custom('10')
|
|
precisionValue = self.precision_from_string(self.safe_string(market['precision'], 'base'))
|
|
precisionValueStr = str(precisionValue)
|
|
sizeMultiplier = math.pow(bigInt10, self.convert_to_big_int_custom(precisionValueStr))
|
|
size = leg['size']
|
|
sizeParts = size.split('.')
|
|
sizeDec = self.safe_string(sizeParts, 1, '')
|
|
sizeDecLength = len(sizeDec) + 0 # php tr
|
|
sizeDecLengthStr = str(sizeDecLength)
|
|
sizeInteger = self.convert_to_big_int_custom(size.replace('.', '')) * sizeMultiplier / (math.pow(bigInt10, self.convert_to_big_int_custom(sizeDecLengthStr)))
|
|
legOrder = {
|
|
'assetID': market['info']['instrument_hash'],
|
|
'contractSize': self.parse_to_int(sizeInteger),
|
|
'isBuyingContract': leg['is_buying_asset'],
|
|
}
|
|
limitPrice = self.safe_string(leg, 'limit_price')
|
|
if self.omit_zero(limitPrice) is not None:
|
|
price = leg['limit_price']
|
|
limitParts = price.split('.')
|
|
limitDec = self.safe_string(limitParts, 1, '')
|
|
limitDecLength = len(limitDec) + 0 # php tr
|
|
limitDecLengthStr = str(limitDecLength)
|
|
powerNum = limitDecLengthStr == 0 if '0' else self.convert_to_big_int_custom(limitDecLengthStr)
|
|
priceInteger = (self.convert_to_big_int_custom(price.replace('.', '')) * self.convert_to_big_int_custom(priceMultiplier) / (math.pow(bigInt10, powerNum)))
|
|
legOrder['limitPrice'] = self.parse_to_int(priceInteger)
|
|
else:
|
|
legOrder['limitPrice'] = 0 # should be zero to validate type-check
|
|
legs.append(legOrder)
|
|
returnValue = {
|
|
'subAccountID': order['sub_account_id'],
|
|
'isMarket': order['is_market'],
|
|
'timeInForce': self.time_in_force_to_int(order['time_in_force']),
|
|
'postOnly': order['post_only'],
|
|
'reduceOnly': order['reduce_only'],
|
|
'legs': legs,
|
|
'nonce': order['signature']['nonce'],
|
|
'expiration': order['signature']['expiration'],
|
|
}
|
|
if structureType == 'EIP712_ORDER_WITH_BUILDER_TYPE' and self.safe_bool(self.options, 'builderFee', True):
|
|
returnValue['builder'] = order['builder']
|
|
returnValue['builderFee'] = self.parse_to_int(self.convert_to_big_int_custom(self.fee_amount_multiplier()) * float(order['builder_fee'])) # the order is matter for Multiply in go, b must be float64 otherwise the value would be 0
|
|
return returnValue
|
|
|
|
def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://api-docs.grvt.io/trading_api/#fill-history
|
|
|
|
:param str [symbol]: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest item
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params)
|
|
request = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['base'] = []
|
|
request['base'].append(market['baseId'])
|
|
request['quote'] = []
|
|
request['quote'].append(market['quoteId'])
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.privateTradingPostFullV1FillHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "event_time": "1764945709702747558",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "is_buyer": True,
|
|
# "is_taker": False,
|
|
# "size": "0.001",
|
|
# "price": "90000.0",
|
|
# "mark_price": "90050.164063298",
|
|
# "index_price": "90089.803654938",
|
|
# "interest_rate": "0.0",
|
|
# "forward_price": "0.0",
|
|
# "realized_pnl": "0.0",
|
|
# "fee": "-0.00009",
|
|
# "fee_rate": "0.0",
|
|
# "trade_id": "65424692-2",
|
|
# "order_id": "0x01010105034cddc7000000006621285c",
|
|
# "venue": "ORDERBOOK",
|
|
# "client_order_id": "1375879248",
|
|
# "signer": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "broker": "UNSPECIFIED",
|
|
# "is_rpi": False
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "next": ""
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_trades(result, None, since, limit)
|
|
|
|
def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://api-docs.grvt.io/trading_api/#positions-request
|
|
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `position structures <https://docs.ccxt.com/?id=position-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
if symbols is not None:
|
|
symbols = self.market_symbols(symbols)
|
|
request['base'] = []
|
|
request['quote'] = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
if market['contract'] is not True:
|
|
raise BadRequest(self.id + ' fetchPositions() supports contract markets only')
|
|
request['base'].append(market['baseId'])
|
|
request['quote'].append(market['quoteId'])
|
|
response = self.privateTradingPostFullV1Positions(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "event_time": "1765258069092857642",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "size": "0.001",
|
|
# "notional": "89.8169",
|
|
# "entry_price": "90000.0",
|
|
# "exit_price": "0.0",
|
|
# "mark_price": "89816.900008979",
|
|
# "unrealized_pnl": "-0.183099",
|
|
# "realized_pnl": "0.0",
|
|
# "total_pnl": "-0.183099",
|
|
# "roi": "-0.2034",
|
|
# "quote_index_price": "1.00017885",
|
|
# "est_liquidation_price": "77951.450008979",
|
|
# "leverage": "28.0",
|
|
# "cumulative_fee": "-0.00009",
|
|
# "cumulative_realized_funding_payment": "0.033862"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_positions(result, symbols)
|
|
|
|
def parse_position(self, position: dict, market: Market = None):
|
|
#
|
|
# {
|
|
# "event_time": "1765258069092857642",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "size": "0.001",
|
|
# "notional": "89.8169",
|
|
# "entry_price": "90000.0",
|
|
# "exit_price": "0.0",
|
|
# "mark_price": "89816.900008979",
|
|
# "unrealized_pnl": "-0.183099",
|
|
# "realized_pnl": "0.0",
|
|
# "total_pnl": "-0.183099",
|
|
# "roi": "-0.2034",
|
|
# "quote_index_price": "1.00017885",
|
|
# "est_liquidation_price": "77951.450008979",
|
|
# "leverage": "28.0",
|
|
# "cumulative_fee": "-0.00009",
|
|
# "cumulative_realized_funding_payment": "0.033862"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(position, 'instrument')
|
|
timestamp = self.safe_integer_product(position, 'event_time', 0.000001)
|
|
sizeRaw = self.safe_string(position, 'size')
|
|
isLong = (Precise.string_ge(sizeRaw, '0'))
|
|
side = 'long' if isLong else 'short'
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'notional': self.parse_number(Precise.string_abs(self.safe_string(position, 'notional'))),
|
|
'marginMode': None,
|
|
'liquidationPrice': self.safe_number(position, 'est_liquidation_price'),
|
|
'entryPrice': self.safe_number(position, 'entry_price'),
|
|
'unrealizedPnl': self.safe_number(position, 'unrealized_pnl'),
|
|
'realizedPnl': self.safe_number(position, 'realized_pnl'),
|
|
'percentage': None,
|
|
'contracts': self.parse_number(Precise.string_abs(sizeRaw)),
|
|
'markPrice': self.safe_number(position, 'mark_price'),
|
|
'lastPrice': None,
|
|
'side': side,
|
|
'hedged': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastUpdateTimestamp': self.safe_integer(position, 'lastUpdateTime'),
|
|
'maintenanceMargin': self.safe_number(position, 'maintenanceMargin'),
|
|
'maintenanceMarginPercentage': None,
|
|
'collateral': None,
|
|
'initialMargin': self.safe_number(position, 'initialMargin'),
|
|
'initialMarginPercentage': None,
|
|
'leverage': self.safe_number(position, 'leverage'),
|
|
'marginRatio': None,
|
|
'stopLossPrice': None,
|
|
'takeProfitPrice': None,
|
|
})
|
|
|
|
def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages:
|
|
"""
|
|
fetch the set leverage for all contract markets
|
|
|
|
https://api-docs.grvt.io/trading_api/#get-all-initial-leverage
|
|
|
|
:param str[] [symbols]: a list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `leverage structures <https://docs.ccxt.com/?id=leverage-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request: dict = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
response = self.privateTradingPostFullV1GetAllInitialLeverage(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "results": [
|
|
# {
|
|
# "instrument": "AAVE_USDT_Perp",
|
|
# "leverage": "10.0",
|
|
# "min_leverage": "1.0",
|
|
# "max_leverage": "50.0",
|
|
# "margin_type": "CROSS"
|
|
# },
|
|
#
|
|
results = self.safe_list(response, 'results', [])
|
|
return self.parse_leverages(results, symbols)
|
|
|
|
def set_leverage(self, leverage: int, symbol: Str = None, params={}):
|
|
"""
|
|
set the level of leverage for a market
|
|
|
|
https://api-docs.grvt.io/trading_api/#set-initial-leverage
|
|
|
|
:param float leverage: the rate of leverage
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: response from the exchange
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument')
|
|
self.load_markets_and_sign_in()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
'instrument': market['id'],
|
|
'leverage': self.number_to_string(leverage),
|
|
}
|
|
response = self.privateTradingPostFullV1SetInitialLeverage(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "success": True
|
|
# }
|
|
#
|
|
return self.parse_leverage(response, market)
|
|
|
|
def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage:
|
|
#
|
|
# setLeverage
|
|
#
|
|
# {
|
|
# "success": True
|
|
# }
|
|
#
|
|
# fetchLeverages
|
|
#
|
|
# {
|
|
# "instrument": "AAVE_USDT_Perp",
|
|
# "leverage": "10.0",
|
|
# "min_leverage": "1.0",
|
|
# "max_leverage": "50.0",
|
|
# "margin_type": "CROSS"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(leverage, 'instrument')
|
|
leverageValue = self.safe_number(leverage, 'leverage')
|
|
marginType = self.safe_string_lower(leverage, 'margin_type')
|
|
return {
|
|
'info': leverage,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'marginMode': marginType,
|
|
'longLeverage': leverageValue,
|
|
'shortLeverage': leverageValue,
|
|
}
|
|
|
|
def fetch_margin_modes(self, symbols: List[Str] = None, params={}) -> MarginModes:
|
|
"""
|
|
fetches margin mode of the user
|
|
|
|
https://api-docs.grvt.io/trading_api/#get-all-initial-leverage
|
|
|
|
:param str[] symbols: unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `margin mode structures <https://docs.ccxt.com/?id=margin-mode-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request: dict = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
response = self.privateTradingPostFullV1GetAllInitialLeverage(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "results": [
|
|
# {
|
|
# "instrument": "AAVE_USDT_Perp",
|
|
# "leverage": "10.0",
|
|
# "min_leverage": "1.0",
|
|
# "max_leverage": "50.0",
|
|
# "margin_type": "CROSS"
|
|
# },
|
|
#
|
|
results = self.safe_list(response, 'results', [])
|
|
return self.parse_leverages(results, symbols)
|
|
|
|
def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode:
|
|
#
|
|
# fetchMarginModes
|
|
#
|
|
# {
|
|
# "instrument": "AAVE_USDT_Perp",
|
|
# "leverage": "10.0",
|
|
# "min_leverage": "1.0",
|
|
# "max_leverage": "50.0",
|
|
# "margin_type": "CROSS"
|
|
# },
|
|
#
|
|
marketId = self.safe_string(marginMode, 'symbol')
|
|
return {
|
|
'info': marginMode,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'marginMode': self.safe_string_lower(marginMode, 'margin_type'),
|
|
}
|
|
|
|
def fetch_funding_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch the history of funding payments paid and received on self account
|
|
|
|
https://api-docs.grvt.io/trading_api/#funding-payment-history
|
|
|
|
:param str [symbol]: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch funding history for
|
|
:param int [limit]: the maximum number of funding history structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest item
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns dict: a `funding history structure <https://docs.ccxt.com/?id=funding-history-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchFundingHistory', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchFundingHistory', symbol, since, limit, params, 1000)
|
|
request = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['base'] = []
|
|
request['base'].append(market['baseId'])
|
|
request['quote'] = []
|
|
request['quote'].append(market['quoteId'])
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.privateTradingPostFullV1FundingPaymentHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "event_time": "1765267200004987902",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "currency": "USDT",
|
|
# "amount": "-0.004522",
|
|
# "tx_id": "66625184"
|
|
# },
|
|
# ..
|
|
# ],
|
|
# "next": ""
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_incomes(result, market, since, limit)
|
|
|
|
def parse_income(self, income, market: Market = None):
|
|
#
|
|
# {
|
|
# "event_time": "1765267200004987902",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "currency": "USDT",
|
|
# "amount": "-0.004522",
|
|
# "tx_id": "66625184"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(income, 'instrument')
|
|
currencyId = self.safe_string(income, 'currency')
|
|
timestamp = self.safe_integer_product(income, 'event_time', 0.000001)
|
|
return {
|
|
'info': income,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'code': self.safe_currency_code(currencyId),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'id': self.safe_string(income, 'tx_id'),
|
|
'amount': self.safe_number(income, 'amount'),
|
|
}
|
|
|
|
def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple orders made by the user
|
|
|
|
https://api-docs.grvt.io/trading_api/#order-history
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest item
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
subAccountId = self.get_sub_account_id(params)
|
|
request = {
|
|
'sub_account_id': subAccountId,
|
|
}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['base'] = []
|
|
request['base'].append(market['baseId'])
|
|
request['quote'] = []
|
|
request['quote'].append(market['quoteId'])
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
request, params = self.handle_until_option_string('end_time', request, params, 1000000)
|
|
if since is not None:
|
|
request['start_time'] = self.number_to_string(since * 1000000)
|
|
response = self.privateTradingPostFullV1OrderHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "order_id": "0x01010105034cddc7000000006621285c",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "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": "90000.0",
|
|
# "is_buying_asset": True
|
|
# }
|
|
# ],
|
|
# "signature": {
|
|
# "signer": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "r": "0x2d567b0a04525baf0bbd792db3bb3a28c1bcc5e95936f6dc2515a28ad8529313",
|
|
# "s": "0x0bc2468d96c819c8de005aa7bebfb58eecb34dd7a1bae1e81e74c7b8bc4cddc7",
|
|
# "v": "27",
|
|
# "expiration": "1767455222801000000",
|
|
# "nonce": "1375879248",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "metadata": {
|
|
# "client_order_id": "1375879248",
|
|
# "create_time": "1764863234474424590",
|
|
# "trigger": {
|
|
# "trigger_type": "UNSPECIFIED",
|
|
# "tpsl": {
|
|
# "trigger_by": "UNSPECIFIED",
|
|
# "trigger_price": "0.0",
|
|
# "close_position": False
|
|
# }
|
|
# },
|
|
# "broker": "UNSPECIFIED",
|
|
# "is_position_transfer": False,
|
|
# "allow_crossing": False
|
|
# },
|
|
# "state": {
|
|
# "status": "FILLED",
|
|
# "reject_reason": "UNSPECIFIED",
|
|
# "book_size": [
|
|
# "0.0"
|
|
# ],
|
|
# "traded_size": [
|
|
# "0.001"
|
|
# ],
|
|
# "update_time": "1764945709704912003",
|
|
# "avg_fill_price": [
|
|
# "90000.0"
|
|
# ]
|
|
# }
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "next": ""
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_orders(result, market, since, limit)
|
|
|
|
def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://api-docs.grvt.io/trading_api/#open-orders
|
|
|
|
:param str [symbol]: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
response = self.privateTradingPostFullV1OpenOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": [
|
|
# {
|
|
# "order_id": "0x0101010503e693410000000069530a7d",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "is_market": False,
|
|
# "time_in_force": "GOOD_TILL_TIME",
|
|
# "post_only": False,
|
|
# "reduce_only": False,
|
|
# "legs": [
|
|
# {
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "size": "0.002",
|
|
# "limit_price": "88123.0",
|
|
# "is_buying_asset": True
|
|
# }
|
|
# ],
|
|
# "signature": {
|
|
# "signer": "0x0982ebb82523fd20d1347d59f5a989ed84caa4b5",
|
|
# "r": "0x22b13e5bc7c8d6793db9d0adf6a51340437292baf83aa4f89a01a3c0c1fef4a8",
|
|
# "s": "0x46ecd483126c388cc933022979a9636670f64af3773d04a84ecbeac423e69341",
|
|
# "v": "28",
|
|
# "expiration": "1767871961406000000",
|
|
# "nonce": "588129369",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "metadata": {
|
|
# "client_order_id": "588129369",
|
|
# "create_time": "1765279966899943792",
|
|
# "trigger": {
|
|
# "trigger_type": "UNSPECIFIED",
|
|
# "tpsl": {
|
|
# "trigger_by": "UNSPECIFIED",
|
|
# "trigger_price": "0.0",
|
|
# "close_position": False
|
|
# }
|
|
# },
|
|
# "broker": "UNSPECIFIED",
|
|
# "is_position_transfer": False,
|
|
# "allow_crossing": False
|
|
# },
|
|
# "state": {
|
|
# "status": "OPEN",
|
|
# "reject_reason": "UNSPECIFIED",
|
|
# "book_size": [
|
|
# "0.002"
|
|
# ],
|
|
# "traded_size": [
|
|
# "0.0"
|
|
# ],
|
|
# "update_time": "1765279966899943792",
|
|
# "avg_fill_price": [
|
|
# "0.0"
|
|
# ]
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_list(response, 'result', [])
|
|
return self.parse_orders(result, None, since, limit)
|
|
|
|
def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://api-docs.grvt.io/trading_api/#get-order
|
|
|
|
:param str id: the order id
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.clientOrderId]: client order id
|
|
:returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
subAccountId = self.get_sub_account_id(params)
|
|
request = {
|
|
'sub_account_id': subAccountId,
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id')
|
|
if clientOrderId is not None:
|
|
params = self.omit(params, 'clientOrderId', 'client_order_id')
|
|
request['client_order_id'] = clientOrderId
|
|
else:
|
|
request['order_id'] = id
|
|
response = self.privateTradingPostFullV1Order(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "order_id": "0x01010105034cddc7000000006621285c",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "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": "90000.0",
|
|
# "is_buying_asset": True
|
|
# }
|
|
# ],
|
|
# "signature": {
|
|
# "signer": "0x42c9f56f2c9da534f64b8806d64813b29c62a01d",
|
|
# "r": "0x2d567b0a04525baf0bbd792db3bb3a28c1bcc5e95936f6dc2515a28ad8529313",
|
|
# "s": "0x0bc2468d96c819c8de005aa7bebfb58eecb34dd7a1bae1e81e74c7b8bc4cddc7",
|
|
# "v": "27",
|
|
# "expiration": "1767455222801000000",
|
|
# "nonce": "1375879248",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "metadata": {
|
|
# "client_order_id": "1375879248",
|
|
# "create_time": "1764863234474424590",
|
|
# "trigger": {
|
|
# "trigger_type": "UNSPECIFIED",
|
|
# "tpsl": {
|
|
# "trigger_by": "UNSPECIFIED",
|
|
# "trigger_price": "0.0",
|
|
# "close_position": False
|
|
# }
|
|
# },
|
|
# "broker": "UNSPECIFIED",
|
|
# "is_position_transfer": False,
|
|
# "allow_crossing": False
|
|
# },
|
|
# "state": {
|
|
# "status": "FILLED",
|
|
# "reject_reason": "UNSPECIFIED",
|
|
# "book_size": [
|
|
# "0.0"
|
|
# ],
|
|
# "traded_size": [
|
|
# "0.001"
|
|
# ],
|
|
# "update_time": "1764945709704912003",
|
|
# "avg_fill_price": [
|
|
# "90000.0"
|
|
# ]
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_order(result)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# fetchOrders, fetchOpenOrders, fetchOrder, createOrder
|
|
#
|
|
# {
|
|
# "order_id": "0x0101010503e693410000000069530a7d",
|
|
# "sub_account_id": "2147050003876484",
|
|
# "is_market": False,
|
|
# "time_in_force": "GOOD_TILL_TIME",
|
|
# "post_only": False,
|
|
# "reduce_only": False,
|
|
# "legs": [
|
|
# {
|
|
# "instrument": "BTC_USDT_Perp",
|
|
# "size": "0.002",
|
|
# "limit_price": "88123.0",
|
|
# "is_buying_asset": True
|
|
# }
|
|
# ],
|
|
# "signature": {
|
|
# "signer": "0x0982ebb82523fd20d1347d59f5a989ed84caa4b5",
|
|
# "r": "0x22b13e5bc7c8d6793db9d0adf6a51340437292baf83aa4f89a01a3c0c1fef4a8",
|
|
# "s": "0x46ecd483126c388cc933022979a9636670f64af3773d04a84ecbeac423e69341",
|
|
# "v": "28",
|
|
# "expiration": "1767871961406000000",
|
|
# "nonce": "588129369",
|
|
# "chain_id": "0"
|
|
# },
|
|
# "metadata": {
|
|
# "client_order_id": "588129369",
|
|
# "create_time": "1765279966899943792",
|
|
# "trigger": {
|
|
# "trigger_type": "UNSPECIFIED",
|
|
# "tpsl": {
|
|
# "trigger_by": "UNSPECIFIED",
|
|
# "trigger_price": "0.0",
|
|
# "close_position": False
|
|
# }
|
|
# },
|
|
# "broker": "UNSPECIFIED",
|
|
# "is_position_transfer": False,
|
|
# "allow_crossing": False
|
|
# },
|
|
# "state": {
|
|
# "status": "OPEN",
|
|
# "reject_reason": "UNSPECIFIED",
|
|
# "book_size": [
|
|
# "0.002"
|
|
# ],
|
|
# "traded_size": [
|
|
# "0.0"
|
|
# ],
|
|
# "update_time": "1765279966899943792",
|
|
# "avg_fill_price": [
|
|
# "0.0"
|
|
# ]
|
|
# },
|
|
# "builder": "0x00",
|
|
# "builder_fee": "0.0"
|
|
# }
|
|
#
|
|
# cancelOrder, cancelAllOrders
|
|
#
|
|
# {
|
|
# "ack": True
|
|
# }
|
|
#
|
|
if 'ack' in order:
|
|
return self.safe_order({
|
|
'info': order,
|
|
'id': None,
|
|
})
|
|
isMarket = self.safe_bool(order, 'is_market')
|
|
orderType = 'market' if isMarket else 'limit'
|
|
isPostOnly = self.safe_bool(order, 'post_only')
|
|
isReduceOnly = self.safe_bool(order, 'reduce_only')
|
|
timeInForceRaw = self.safe_string(order, 'time_in_force')
|
|
timeInForce = 'PO' if isPostOnly else self.parse_time_in_force(timeInForceRaw)
|
|
size = None
|
|
side = None
|
|
price = None
|
|
filled = None
|
|
avgPrice = None
|
|
legs = self.safe_list(order, 'legs')
|
|
metadata = self.safe_dict(order, 'metadata', {})
|
|
stateObj = self.safe_dict(order, 'state', {})
|
|
filledAmounts = self.safe_list(stateObj, 'traded_size', [])
|
|
avgPrices = self.safe_list(stateObj, 'avg_fill_price', [])
|
|
primaryOrderIndex = 0
|
|
firstLeg = self.safe_dict(legs, primaryOrderIndex)
|
|
if firstLeg is not None:
|
|
marketId = self.safe_string(firstLeg, 'instrument')
|
|
market = self.safe_market(marketId, market)
|
|
size = self.safe_string(firstLeg, 'size')
|
|
side = 'buy' if self.safe_bool(firstLeg, 'is_buying_asset') else 'sell'
|
|
price = self.safe_string(firstLeg, 'limit_price')
|
|
filled = self.safe_string(filledAmounts, primaryOrderIndex)
|
|
avgPrice = self.safe_string(avgPrices, primaryOrderIndex)
|
|
timestamp = self.safe_integer_product(metadata, 'create_time', 0.000001)
|
|
# triggerDetails = self.safe_dict(metadata, 'trigger', {})
|
|
legsLength = len(legs)
|
|
return self.safe_order({
|
|
'isMultiLeg': (legsLength > 1),
|
|
'id': self.safe_string(order, 'order_id'),
|
|
'clientOrderId': self.safe_string(metadata, 'client_order_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimeStamp': None,
|
|
'lastUpdateTimestamp': self.safe_integer_product(stateObj, 'update_time', 0.000001),
|
|
'status': self.parse_order_status(self.safe_string(stateObj, 'status')),
|
|
'symbol': market['symbol'],
|
|
'type': orderType,
|
|
'timeInForce': timeInForce,
|
|
'postOnly': isPostOnly,
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': None,
|
|
'cost': None,
|
|
'average': avgPrice,
|
|
'amount': size,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'trades': None,
|
|
'fees': None,
|
|
'reduceOnly': isReduceOnly,
|
|
'info': order,
|
|
}, market)
|
|
|
|
def parse_time_in_force(self, type: Str) -> Str:
|
|
types: dict = {
|
|
'GOOD_TILL_TIME': 'GTC', # yeah, not GTD
|
|
'IMMEDIATE_OR_CANCEL': 'IOC',
|
|
'FILL_OR_KILL': 'FOK',
|
|
# exchange specific types
|
|
'ALL_OR_NONE': 'ALL_OR_NONE',
|
|
'RETAIL_PRICE_IMPROVEMENT': 'RETAIL_PRICE_IMPROVEMENT',
|
|
}
|
|
return self.safe_string_upper(types, type, type)
|
|
|
|
def time_in_force_to_int(self, timeInForce: Str) -> Int:
|
|
timeInForces: dict = {
|
|
'GOOD_TILL_TIME': 1,
|
|
'ALL_OR_NONE': 2,
|
|
'IMMEDIATE_OR_CANCEL': 3,
|
|
'FILL_OR_KILL': 4,
|
|
'RETAIL_PRICE_IMPROVEMENT': 5,
|
|
}
|
|
return self.safe_integer(timeInForces, timeInForce, 0)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'PENDING': 'pending',
|
|
'OPEN': 'open',
|
|
'FILLED': 'closed',
|
|
'REJECTED': 'rejected',
|
|
'CANCELLED': 'canceled',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders in a market
|
|
|
|
https://api-docs.grvt.io/trading_api/#cancel-all-orders
|
|
|
|
:param str symbol: cancel alls open orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
request = {
|
|
'sub_account_id': self.get_sub_account_id(params),
|
|
}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['base'] = []
|
|
request['base'].append(market['baseId'])
|
|
request['quote'] = []
|
|
request['quote'].append(market['quoteId'])
|
|
response = self.privateTradingPostFullV1CancelAllOrders(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "ack": True
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_orders([result])
|
|
|
|
def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
|
|
https://api-docs.grvt.io/trading_api/#cancel-order
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.clientOrderId]: client order id
|
|
:returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets_and_sign_in()
|
|
subAccoubntId = self.get_sub_account_id(params)
|
|
request = {
|
|
'sub_account_id': subAccoubntId,
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id')
|
|
if clientOrderId is not None:
|
|
params = self.omit(params, 'clientOrderId')
|
|
request['client_order_id'] = clientOrderId
|
|
else:
|
|
request['order_id'] = id
|
|
response = self.privateTradingPostFullV1CancelOrder(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": {
|
|
# "ack": True
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result', {})
|
|
return self.parse_order(result)
|
|
|
|
def eip_domain_data(self):
|
|
# GrvtEnv.DEV.value: 327,
|
|
# GrvtEnv.STAGING.value: 327,
|
|
# GrvtEnv.TESTNET.value: 326,
|
|
# GrvtEnv.PROD.value: 325,
|
|
return {
|
|
'name': 'GRVT Exchange',
|
|
'version': '0',
|
|
'chainId': 326 if self.isSandboxModeEnabled else 325,
|
|
}
|
|
|
|
def fee_amount_multiplier(self):
|
|
return self.convert_to_big_int_custom('10000') # multiply needed https://t.me/c/3396937126/88
|
|
|
|
def create_signed_request(self, request: Any, structureType: str, currencyObj=None, signerAddress: Str = None) -> dict:
|
|
messageData = None
|
|
if structureType == 'EIP712_TRANSFER_TYPE':
|
|
amountMultiplier = self.convert_to_big_int_custom('1000000')
|
|
amountInt = request['num_tokens'] * amountMultiplier
|
|
messageData = {
|
|
'fromAccount': request['from_account_id'],
|
|
'fromSubAccount': request['from_sub_account_id'],
|
|
'toAccount': request['to_account_id'],
|
|
'toSubAccount': request['to_sub_account_id'],
|
|
'tokenCurrency': currencyObj['numericId'],
|
|
'numTokens': self.parse_to_int(amountInt),
|
|
'nonce': request['signature']['nonce'],
|
|
'expiration': request['signature']['expiration'],
|
|
}
|
|
elif structureType == 'EIP712_WITHDRAWAL_TYPE':
|
|
amountMultiplier = self.convert_to_big_int_custom('1000000')
|
|
messageData = {
|
|
'fromAccount': request['from_account_id'],
|
|
'toEthAddress': request['to_eth_address'],
|
|
'tokenCurrency': currencyObj['numericId'],
|
|
'numTokens': self.parse_to_int(request['num_tokens'] * amountMultiplier),
|
|
'nonce': request['signature']['nonce'],
|
|
'expiration': request['signature']['expiration'],
|
|
}
|
|
elif structureType == 'EIP712_ORDER_TYPE' or structureType == 'EIP712_ORDER_WITH_BUILDER_TYPE':
|
|
messageData = self.eip_message_for_order(request, structureType)
|
|
elif structureType == 'EIP712_BUILDER_APPROVAL_TYPE':
|
|
amountMultiplier = self.convert_to_big_int_custom(self.fee_amount_multiplier())
|
|
messageData = {
|
|
'mainAccountID': request['main_account_id'],
|
|
'builderAccountID': request['builder_account_id'],
|
|
'maxFutureFeeRate': self.parse_to_int(float(request['max_futures_fee_rate']) * amountMultiplier),
|
|
'maxSpotFeeRate': self.parse_to_int(float(request['max_spot_fee_rate']) * amountMultiplier),
|
|
'nonce': request['signature']['nonce'],
|
|
'expiration': request['signature']['expiration'],
|
|
}
|
|
elif structureType == 'EIP712_WALLETLOGIN_TYPE':
|
|
messageData = {
|
|
'signer': request['address'],
|
|
'nonce': request['signature']['nonce'],
|
|
'expiration': request['signature']['expiration'],
|
|
}
|
|
domainData = self.eip_domain_data()
|
|
definitions = self.eip_definitions()
|
|
ethEncodedMessage = self.eth_encode_structured_data(domainData, definitions[structureType], messageData)
|
|
ethEncodedMessageHashed = '0x' + self.hash(ethEncodedMessage, 'keccak', 'hex')
|
|
usesPrivKey = self.uses_private_key() # py transpiler needs self line separated
|
|
secretOrPrivkey = self.privateKey if usesPrivKey else self.secret
|
|
privateKeyWithoutZero = self.remove0x_prefix(secretOrPrivkey)
|
|
signature = self.ecdsa(self.remove0x_prefix(ethEncodedMessageHashed), privateKeyWithoutZero, 'secp256k1', None)
|
|
request['signature']['r'] = self.format_signature_rs(signature['r'])
|
|
request['signature']['s'] = self.format_signature_rs(signature['s'])
|
|
request['signature']['v'] = self.sum(27, signature['v'])
|
|
request['signature']['signer'] = self.eth_get_address_from_private_key('0x' + privateKeyWithoutZero) if (signerAddress is None) else signerAddress
|
|
return request
|
|
|
|
def format_signature_rs(self, value: str):
|
|
padded = value.rjust(64, '0')
|
|
if padded.startswith('0x'):
|
|
return padded
|
|
else:
|
|
return '0x' + padded
|
|
|
|
def default_signature(self):
|
|
expiration = self.milliseconds() * 1000000 + 1000000 * self.safe_integer(self.options, 'expirationSeconds', 30) * 1000
|
|
return {
|
|
'signer': '',
|
|
'r': '',
|
|
's': '',
|
|
'v': 0,
|
|
'expiration': str(expiration),
|
|
'nonce': self.nonce(),
|
|
'chain_id': '326' if self.isSandboxModeEnabled else '325',
|
|
}
|
|
|
|
def handle_until_option_string(self, key: str, request, params, multiplier=1):
|
|
until = self.safe_integer_2(params, 'until', 'till')
|
|
if until is not None:
|
|
request[key] = self.number_to_string(self.parse_to_int(until * multiplier))
|
|
params = self.omit(params, ['until', 'till'])
|
|
return [request, params]
|
|
|
|
def request_id(self):
|
|
requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1)
|
|
self.options['requestId'] = requestId
|
|
return requestId
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
query = self.omit(params, self.extract_params(path))
|
|
url = self.urls['api'][api] + path
|
|
queryString = ''
|
|
if method == 'GET':
|
|
if query:
|
|
queryString = self.urlencode(query)
|
|
url += '?' + queryString
|
|
elif method == 'POST':
|
|
body = self.json(params)
|
|
isPrivate = api.startswith('private')
|
|
if isPrivate:
|
|
self.check_required_credentials()
|
|
if queryString != '':
|
|
path = path + '?' + queryString
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
if path.endswith('auth/api_key/login') or path.endswith('auth/wallet/login'):
|
|
headers['Cookie'] = 'rm=true;'
|
|
else:
|
|
accountId = self.safe_string(self.options, 'AuthAccountId')
|
|
cookieValue = self.safe_string(self.options, 'AuthCookieValue')
|
|
if cookieValue is None or accountId is None:
|
|
raise AuthenticationError(self.id + ' : at first, you need to authenticate with exchange using signIn() method.')
|
|
headers['Cookie'] = cookieValue
|
|
headers['X-Grvt-Account-Id'] = accountId
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if url.endswith('auth/api_key/login') or url.endswith('auth/wallet/login'):
|
|
accountId = self.safe_string_2(headers, 'X-Grvt-Account-Id', 'x-grvt-account-id')
|
|
self.options['AuthAccountId'] = accountId
|
|
cookie = self.safe_string_2(headers, 'Set-Cookie', 'set-cookie')
|
|
if cookie is not None:
|
|
cookieValue = cookie.split(';')[0]
|
|
self.options['AuthCookieValue'] = cookieValue
|
|
if self.options['AuthCookieValue'] is None or self.options['AuthAccountId'] is None:
|
|
raise AuthenticationError(self.id + ' signIn() failed to receive auth-cookie or account-id')
|
|
else:
|
|
errorCode = self.safe_string(response, 'code')
|
|
if errorCode is not None:
|
|
feedback = self.id + ' ' + body
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
|
|
raise ExchangeError(feedback)
|
|
else:
|
|
message = self.safe_string(response, 'message')
|
|
if message is not None:
|
|
feedback = self.id + ' ' + body
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
raise ExchangeError(feedback)
|
|
else:
|
|
status = self.safe_string(response, 'status')
|
|
if status is not None and status != 'success':
|
|
feedback = self.id + ' ' + body
|
|
raise ExchangeError(feedback)
|
|
return None
|