2366 lines
106 KiB
Python
2366 lines
106 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.dydx import ImplicitAPI
|
|
import math
|
|
from ccxt.base.types import Account, Any, Balances, Currency, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Trade, Transaction, TransferEntry
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import NotSupported
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class dydx(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(dydx, self).describe(), {
|
|
'id': 'dydx',
|
|
'name': 'dYdX',
|
|
'countries': ['US'],
|
|
'rateLimit': 100,
|
|
'version': 'v4',
|
|
'certified': False,
|
|
'dex': True,
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': False,
|
|
'margin': False,
|
|
'swap': True,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': False,
|
|
'cancelAllOrders': False,
|
|
'cancelAllOrdersAfter': False,
|
|
'cancelOrder': True,
|
|
'cancelOrders': True,
|
|
'cancelWithdraw': False,
|
|
'closeAllPositions': False,
|
|
'closePosition': False,
|
|
'createConvertTrade': False,
|
|
'createDepositAddress': False,
|
|
'createMarketBuyOrderWithCost': False,
|
|
'createMarketOrder': False,
|
|
'createMarketOrderWithCost': False,
|
|
'createMarketSellOrderWithCost': False,
|
|
'createOrder': True,
|
|
'createOrderWithTakeProfitAndStopLoss': False,
|
|
'createReduceOnlyOrder': False,
|
|
'createStopLimitOrder': False,
|
|
'createStopLossOrder': False,
|
|
'createStopMarketOrder': False,
|
|
'createStopOrder': False,
|
|
'createTakeProfitOrder': False,
|
|
'createTrailingAmountOrder': False,
|
|
'createTrailingPercentOrder': False,
|
|
'createTriggerOrder': False,
|
|
'fetchAccounts': True,
|
|
'fetchBalance': True,
|
|
'fetchCanceledOrders': False,
|
|
'fetchClosedOrder': False,
|
|
'fetchClosedOrders': True,
|
|
'fetchConvertCurrencies': False,
|
|
'fetchConvertQuote': False,
|
|
'fetchConvertTrade': False,
|
|
'fetchConvertTradeHistory': False,
|
|
'fetchCurrencies': False,
|
|
'fetchDepositAddress': False,
|
|
'fetchDepositAddresses': False,
|
|
'fetchDepositAddressesByNetwork': False,
|
|
'fetchDeposits': True,
|
|
'fetchDepositsWithdrawals': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingInterval': False,
|
|
'fetchFundingIntervals': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': True,
|
|
'fetchFundingRates': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchLedger': True,
|
|
'fetchLeverage': False,
|
|
'fetchMarginAdjustmentHistory': False,
|
|
'fetchMarginMode': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMyTrades': False,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenOrder': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrders': True,
|
|
'fetchOrderTrades': False,
|
|
'fetchPosition': True,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': True,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchStatus': False,
|
|
'fetchTicker': False,
|
|
'fetchTickers': False,
|
|
'fetchTime': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFee': False,
|
|
'fetchTradingFees': False,
|
|
'fetchTransactions': False,
|
|
'fetchTransfers': True,
|
|
'fetchWithdrawals': True,
|
|
'reduceMargin': False,
|
|
'sandbox': False,
|
|
'setLeverage': False,
|
|
'setMargin': False,
|
|
'setPositionMode': False,
|
|
'transfer': True,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1MIN',
|
|
'5m': '5MINS',
|
|
'15m': '15MINS',
|
|
'30m': '30MINS',
|
|
'1h': '1HOUR',
|
|
'4h': '4HOURS',
|
|
'1d': '1DAY',
|
|
},
|
|
'urls': {
|
|
'logo': 'https://github.com/user-attachments/assets/617ea0c1-f05a-4d26-9fcb-a0d1d4091ae1',
|
|
'api': {
|
|
'indexer': 'https://indexer.dydx.trade/v4',
|
|
'nodeRpc': 'https://dydx-ops-rpc.kingnodes.com',
|
|
'nodeRest': 'https://dydx-rest.publicnode.com',
|
|
},
|
|
'test': {
|
|
'indexer': 'https://indexer.v4testnet.dydx.exchange/v4',
|
|
'nodeRpc': 'https://test-dydx-rpc.kingnodes.com',
|
|
'nodeRest': 'https://test-dydx-rest.kingnodes.com',
|
|
},
|
|
'www': 'https://www.dydx.xyz',
|
|
'doc': [
|
|
'https://docs.dydx.xyz',
|
|
],
|
|
'fees': [
|
|
'https://docs.dydx.exchange/introduction-trading_fees',
|
|
],
|
|
'referral': 'dydx.trade?ref=ccxt',
|
|
},
|
|
'api': {
|
|
'indexer': {
|
|
'get': {
|
|
'addresses/{address}': 1,
|
|
'addresses/{address}/parentSubaccountNumber/{number}': 1,
|
|
'addresses/{address}/subaccountNumber/{subaccountNumber}': 1,
|
|
'assetPositions': 1,
|
|
'assetPositions/parentSubaccountNumber': 1,
|
|
'candles/perpetualMarkets/{market}': 1,
|
|
'compliance/screen/{address}': 1,
|
|
'fills': 1,
|
|
'fills/parentSubaccountNumber': 1,
|
|
'fundingPayments': 1,
|
|
'fundingPayments/parentSubaccount': 1,
|
|
'height': 0.1,
|
|
'historical-pnl': 1,
|
|
'historical-pnl/parentSubaccountNumber': 1,
|
|
'historicalBlockTradingRewards/{address}': 1,
|
|
'historicalFunding/{market}': 1,
|
|
'historicalTradingRewardAggregations/{address}': 1,
|
|
'orderbooks/perpetualMarket/{market}': 1,
|
|
'orders': 1,
|
|
'orders/parentSubaccountNumber': 1,
|
|
'orders/{orderId}': 1,
|
|
'perpetualMarkets': 1,
|
|
'perpetualPositions': 1,
|
|
'perpetualPositions/parentSubaccountNumber': 1,
|
|
'screen': 1,
|
|
'sparklines': 1,
|
|
'time': 1,
|
|
'trades/perpetualMarket/{market}': 1,
|
|
'transfers': 1,
|
|
'transfers/between': 1,
|
|
'transfers/parentSubaccountNumber': 1,
|
|
'vault/v1/megavault/historicalPnl': 1,
|
|
'vault/v1/megavault/positions': 1,
|
|
'vault/v1/vaults/historicalPnl': 1,
|
|
#
|
|
'perpetualMarketSparklines': 1,
|
|
'perpetualMarkets/{ticker}': 1,
|
|
'perpetualMarkets/{ticker}/orderbook': 1,
|
|
'trades/perpetualMarket/{ticker}': 1,
|
|
'historicalFunding/{ticker}': 1,
|
|
'candles/{ticker}/{resolution}': 1,
|
|
'addresses/{address}/subaccounts': 1,
|
|
'addresses/{address}/subaccountNumber/{subaccountNumber}/assetPositions': 1,
|
|
'addresses/{address}/subaccountNumber/{subaccountNumber}/perpetualPositions': 1,
|
|
'addresses/{address}/subaccountNumber/{subaccountNumber}/orders': 1,
|
|
'fills/parentSubaccount': 1,
|
|
'historical-pnl/parentSubaccount': 1,
|
|
},
|
|
},
|
|
'nodeRpc': {
|
|
'get': {
|
|
'abci_info': 1,
|
|
'block': 1,
|
|
'broadcast_tx_async': 1,
|
|
'broadcast_tx_sync': 1,
|
|
'tx': 1,
|
|
},
|
|
},
|
|
'nodeRest': {
|
|
'get': {
|
|
'cosmos/auth/v1beta1/account_info/{dydxAddress}': 1,
|
|
},
|
|
'post': {
|
|
'cosmos/tx/v1beta1/encode': 1,
|
|
'cosmos/tx/v1beta1/simulate': 1,
|
|
},
|
|
},
|
|
},
|
|
'fees': {
|
|
'trading': {
|
|
'tierBased': True,
|
|
'percentage': True,
|
|
'maker': self.parse_number('0.0001'),
|
|
'taker': self.parse_number('0.0005'),
|
|
},
|
|
},
|
|
'requiredCredentials': {
|
|
'apiKey': False,
|
|
'secret': False,
|
|
'privateKey': False,
|
|
},
|
|
'options': {
|
|
'mnemonic': None, # specify mnemonic, copy secret phrase from UI
|
|
'chainName': 'dydx-mainnet-1',
|
|
'chainId': 1,
|
|
'sandboxMode': False,
|
|
'defaultFeeDenom': 'uusdc',
|
|
'defaultFeeMultiplier': '1.6',
|
|
'feeDenom': {
|
|
'USDC_DENOM': 'ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5',
|
|
'USDC_GAS_DENOM': 'uusdc',
|
|
'USDC_DECIMALS': 6,
|
|
'USDC_GAS_PRICE': '0.025',
|
|
'CHAINTOKEN_DENOM': 'adydx',
|
|
'CHAINTOKEN_DECIMALS': 18,
|
|
'CHAINTOKEN_GAS_PRICE': '25000000000',
|
|
},
|
|
},
|
|
'features': {
|
|
'default': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': {
|
|
'last': True,
|
|
'mark': True,
|
|
'index': False,
|
|
},
|
|
'triggerDirection': False,
|
|
'stopLossPrice': False, # todo by triggerPrice
|
|
'takeProfitPrice': False, # todo by triggerPrice
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': True,
|
|
},
|
|
'hedged': False,
|
|
'trailing': False,
|
|
'leverage': False,
|
|
'marketBuyByCost': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'selfTradePrevention': False,
|
|
'iceberg': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'daysBack': 90,
|
|
'untilDays': 10000,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': True,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'trigger': True,
|
|
'trailing': True,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'daysBack': None,
|
|
'untilDays': 100000,
|
|
'trigger': True,
|
|
'trailing': True,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'daysBack': None,
|
|
'daysBackCanceled': None,
|
|
'untilDays': 100000,
|
|
'trigger': True,
|
|
'trailing': True,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 1000,
|
|
},
|
|
},
|
|
'forSwap': {
|
|
'extends': 'default',
|
|
'createOrder': {
|
|
'hedged': True,
|
|
},
|
|
},
|
|
'swap': {
|
|
'linear': {
|
|
'extends': 'forSwap',
|
|
},
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'commonCurrencies': {},
|
|
'exceptions': {
|
|
'exact': {
|
|
# error collision for clob and sending modules from 2 - 8
|
|
# https://github.com/dydxprotocol/v4-chain/blob/5f9f6c9b95cc87d732e23de764909703b81a6e8b/protocol/x/clob/types/errors.go#L320
|
|
# https://github.com/dydxprotocol/v4-chain/blob/5f9f6c9b95cc87d732e23de764909703b81a6e8b/protocol/x/sending/types/errors.go
|
|
'9': InvalidOrder, # A cancel already exists in the memclob for self order with a greater than or equal GoodTilBlock
|
|
'10': InvalidOrder, # The next block height is greater than the GoodTilBlock of the message
|
|
'11': InvalidOrder, # The GoodTilBlock of the message is further than ShortBlockWindow blocks into the future
|
|
'12': InvalidOrder, # MsgPlaceOrder is invalid
|
|
'13': InvalidOrder, # MsgProposedMatchOrders is invalid
|
|
'14': InvalidOrder, # State filled amount cannot be unchanged
|
|
'15': InvalidOrder, # State filled amount cannot decrease
|
|
'16': InvalidOrder, # Cannot prune state fill amount that does not exist
|
|
'17': InvalidOrder, # Subaccount cannot open more than 20 orders on a given CLOB and side
|
|
'18': InvalidOrder, # `FillAmount` is not divisible by `StepBaseQuantums` of the specified `ClobPairId`
|
|
'19': InvalidOrder, # The provided perpetual ID does not have any associated CLOB pairs
|
|
'20': InvalidOrder, # Replacing an existing order failed
|
|
'21': InvalidOrder, # Clob pair and perpetual ids do not match
|
|
'22': InvalidOrder, # Matched order has negative fee
|
|
'23': InvalidOrder, # Subaccounts updated for a matched order, but fee transfer to fee-collector failed
|
|
'24': InvalidOrder, # Order is fully filled
|
|
'25': InvalidOrder, # Attempting to get price premium with a non-perpetual CLOB pair
|
|
'26': InvalidOrder, # Index price is zero when calculating price premium
|
|
'27': InvalidOrder, # Invalid ClobPair parameter
|
|
'28': InvalidOrder, # Oracle price must be > 0.
|
|
'29': InvalidOrder, # Invalid stateful order cancellation
|
|
'30': InvalidOrder, # An order with the same `OrderId` and `OrderHash` has already been processed for self CLOB
|
|
'31': InvalidOrder, # Missing mid price for ClobPair
|
|
'32': InvalidOrder, # Existing stateful order cancellation has higher-or-equal priority than the new one
|
|
'33': InvalidOrder, # ClobPair with id already exists
|
|
'34': InvalidOrder, # Order conflicts with ClobPair status
|
|
'35': InvalidOrder, # Invalid ClobPair status transition
|
|
'36': InvalidOrder, # Operation conflicts with ClobPair status
|
|
'37': InvalidOrder, # Perpetual does not exist in state
|
|
'39': InvalidOrder, # ClobPair update is invalid
|
|
'40': InvalidOrder, # Authority is invalid
|
|
'41': InvalidOrder, # perpetual ID is already associated with an existing CLOB pair
|
|
'42': InvalidOrder, # Unexpected time in force
|
|
'43': InvalidOrder, # Order has remaining size
|
|
'44': InvalidOrder, # invalid time in force
|
|
'45': InvalidOrder, # Invalid batch cancel message
|
|
'46': InvalidOrder, # Batch cancel has failed
|
|
'47': InvalidOrder, # CLOB has not been initialized
|
|
'48': InvalidOrder, # This field has been deprecated
|
|
'49': InvalidOrder, # Invalid TWAP order placement
|
|
'50': InvalidOrder, # Invalid builder code
|
|
'1000': BadRequest, # Proposed LiquidationsConfig is invalid
|
|
'1001': BadRequest, # Subaccount has no perpetual positions to liquidate
|
|
'1002': BadRequest, # Subaccount is not liquidatable
|
|
'1003': InvalidOrder, # Subaccount does not have an open position for perpetual
|
|
'1004': InvalidOrder, # Liquidation order has invalid size
|
|
'1005': InvalidOrder, # Liquidation order is on the wrong side
|
|
'1006': InvalidOrder, # Total fills amount exceeds size of liquidation order
|
|
'1007': InvalidOrder, # Liquidation order does not contain any fills
|
|
'1008': InvalidOrder, # Subaccount has previously liquidated self perpetual in the current block
|
|
'1009': InvalidOrder, # Liquidation order has size smaller than min position notional specified in the liquidation config
|
|
'1010': InvalidOrder, # Liquidation order has size greater than max position notional specified in the liquidation config
|
|
'1011': InvalidOrder, # Liquidation exceeds the maximum notional amount that a single subaccount can have liquidated per block
|
|
'1012': InvalidOrder, # Liquidation exceeds the maximum insurance fund payout amount for a given subaccount per block
|
|
'1013': InvalidOrder, # Insurance fund does not have sufficient funds to cover liquidation losses
|
|
'1014': InvalidOrder, # Invalid perpetual position size delta
|
|
'1015': InvalidOrder, # Invalid delta base and/or quote quantums for insurance fund delta calculation
|
|
'1017': InvalidOrder, # Cannot deleverage subaccount against itself
|
|
'1018': InvalidOrder, # Deleveraging match cannot have fills with same id
|
|
'1019': InvalidOrder, # Deleveraging match cannot have fills with zero amount
|
|
'1020': InvalidOrder, # Position cannot be fully offset
|
|
'1021': InvalidOrder, # Deleveraging match has incorrect value for isFinalSettlement flag
|
|
'1022': InvalidOrder, # Liquidation conflicts with ClobPair status
|
|
'2000': InvalidOrder, # FillOrKill order could not be fully filled
|
|
'2001': InvalidOrder, # Reduce-only orders cannot increase the position size
|
|
'2002': InvalidOrder, # Reduce-only orders cannot change the position side
|
|
'2003': InvalidOrder, # Post-only order would cross one or more maker orders
|
|
'2004': InvalidOrder, # IOC order is already filled, remaining size is cancelled.
|
|
'2005': InvalidOrder, # Order would violate isolated subaccount constraints.
|
|
'3000': InvalidOrder, # Invalid order flags
|
|
'3001': InvalidOrder, # Invalid order goodTilBlockTime
|
|
'3002': InvalidOrder, # Stateful orders cannot require immediate execution
|
|
'3003': InvalidOrder, # The block time is greater than the GoodTilBlockTime of the message
|
|
'3004': InvalidOrder, # The GoodTilBlockTime of the message is further than StatefulOrderTimeWindow into the future
|
|
'3005': InvalidOrder, # Existing stateful order has higher-or-equal priority than the new one
|
|
'3006': InvalidOrder, # Stateful order does not exist
|
|
'3007': InvalidOrder, # Stateful order collateralization check failed
|
|
'3008': InvalidOrder, # Stateful order was previously cancelled and therefore cannot be placed
|
|
'3009': InvalidOrder, # Stateful order was previously removed and therefore cannot be placed
|
|
'3010': InvalidOrder, # Stateful order cancellation failed because the order was already removed from state
|
|
'4000': InvalidOrder, # MsgProposedOperations is invalid
|
|
'4001': InvalidOrder, # Match Order is invalid
|
|
'4002': InvalidOrder, # Order was not previously placed in operations queue
|
|
'4003': InvalidOrder, # Fill amount cannot be zero
|
|
'4004': InvalidOrder, # Deleveraging fill is invalid
|
|
'4005': InvalidOrder, # Deleveraged subaccount in proposed deleveraged operation failed deleveraging validation
|
|
'4006': InvalidOrder, # Order Removal is invalid
|
|
'4007': InvalidOrder, # Order Removal reason is invalid
|
|
'4008': InvalidOrder, # Zero-fill deleveraging operation included in block for non-negative TNC subaccount
|
|
'5000': InvalidOrder, # Proposed BlockRateLimitConfig is invalid
|
|
'5001': InvalidOrder, # Block rate limit exceeded
|
|
'6000': InvalidOrder, # Conditional type is invalid
|
|
'6001': InvalidOrder, # Conditional order trigger subticks is invalid
|
|
'6002': InvalidOrder, # Conditional order is untriggered
|
|
'9000': InvalidOrder, # Asset orders are not implemented
|
|
'9001': InvalidOrder, # Updates for assets other than USDC are not implemented
|
|
'9002': InvalidOrder, # This function is not implemented
|
|
'9003': InvalidOrder, # Reduce-only is currently disabled for non-IOC orders
|
|
'10000': InvalidOrder, # Proposed EquityTierLimitConfig is invalid
|
|
'10001': InvalidOrder, # Subaccount cannot open more orders due to equity tier limit.
|
|
'11000': InvalidOrder, # Invalid order router address
|
|
},
|
|
'broad': {
|
|
'insufficient funds': InsufficientFunds,
|
|
},
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
})
|
|
|
|
def fetch_time(self, params={}) -> Int:
|
|
"""
|
|
fetches the current integer timestamp in milliseconds from the exchange server
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-time
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int: the current integer timestamp in milliseconds from the exchange server
|
|
"""
|
|
response = self.indexerGetTime(params)
|
|
#
|
|
# {
|
|
# "iso": "2025-07-20T15:12:13.466Z",
|
|
# "epoch": 1753024333.466
|
|
# }
|
|
#
|
|
return self.safe_integer(response, 'epoch')
|
|
|
|
def parse_market(self, market: dict) -> Market:
|
|
#
|
|
# {
|
|
# "clobPairId": "0",
|
|
# "ticker": "BTC-USD",
|
|
# "status": "ACTIVE",
|
|
# "oraclePrice": "118976.5376",
|
|
# "priceChange24H": "659.9736",
|
|
# "volume24H": "1292729.3605",
|
|
# "trades24H": 9387,
|
|
# "nextFundingRate": "0",
|
|
# "initialMarginFraction": "0.02",
|
|
# "maintenanceMarginFraction": "0.012",
|
|
# "openInterest": "52.0691",
|
|
# "atomicResolution": -10,
|
|
# "quantumConversionExponent": -9,
|
|
# "tickSize": "1",
|
|
# "stepSize": "0.0001",
|
|
# "stepBaseQuantums": 1000000,
|
|
# "subticksPerTick": 100000,
|
|
# "marketType": "CROSS",
|
|
# "openInterestLowerCap": "0",
|
|
# "openInterestUpperCap": "0",
|
|
# "baseOpenInterest": "50.3776",
|
|
# "defaultFundingRate1H": "0"
|
|
# }
|
|
#
|
|
quoteId = 'USDC'
|
|
marketId = self.safe_string(market, 'ticker')
|
|
parts = marketId.split('-')
|
|
baseName = self.safe_string(parts, 0)
|
|
baseId = self.safe_string(market, 'baseId', baseName) # idk where 'baseId' comes from, but leaving
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
settleId = 'USDC'
|
|
settle = self.safe_currency_code(settleId)
|
|
symbol = base + '/' + quote + ':' + settle
|
|
contract = True
|
|
swap = True
|
|
amountPrecisionStr = self.safe_string(market, 'stepSize')
|
|
pricePrecisionStr = self.safe_string(market, 'tickSize')
|
|
status = self.safe_string(market, 'status')
|
|
active = True
|
|
if status != 'ACTIVE':
|
|
active = False
|
|
return self.safe_market_structure({
|
|
'id': self.safe_string(market, 'ticker'),
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': settle,
|
|
'baseId': baseId,
|
|
'baseName': baseName,
|
|
'quoteId': quoteId,
|
|
'settleId': settleId,
|
|
'type': 'swap',
|
|
'spot': False,
|
|
'margin': None,
|
|
'swap': swap,
|
|
'future': False,
|
|
'option': False,
|
|
'active': active,
|
|
'contract': contract,
|
|
'contractSize': self.parse_number('1'), # trades seem in absolute size
|
|
'linear': True,
|
|
'inverse': False,
|
|
'taker': None,
|
|
'maker': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': self.parse_number(amountPrecisionStr),
|
|
'price': self.parse_number(pricePrecisionStr),
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'cost': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'created': None,
|
|
'info': market,
|
|
})
|
|
|
|
def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for hyperliquid
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-perpetual-markets
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
request: dict = {
|
|
# 'limit': 1000,
|
|
}
|
|
response = self.indexerGetPerpetualMarkets(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "markets": {
|
|
# "BTC-USD": {
|
|
# "clobPairId": "0",
|
|
# "ticker": "BTC-USD",
|
|
# "status": "ACTIVE",
|
|
# "oraclePrice": "118976.5376",
|
|
# "priceChange24H": "659.9736",
|
|
# "volume24H": "1292729.3605",
|
|
# "trades24H": 9387,
|
|
# "nextFundingRate": "0",
|
|
# "initialMarginFraction": "0.02",
|
|
# "maintenanceMarginFraction": "0.012",
|
|
# "openInterest": "52.0691",
|
|
# "atomicResolution": -10,
|
|
# "quantumConversionExponent": -9,
|
|
# "tickSize": "1",
|
|
# "stepSize": "0.0001",
|
|
# "stepBaseQuantums": 1000000,
|
|
# "subticksPerTick": 100000,
|
|
# "marketType": "CROSS",
|
|
# "openInterestLowerCap": "0",
|
|
# "openInterestUpperCap": "0",
|
|
# "baseOpenInterest": "50.3776",
|
|
# "defaultFundingRate1H": "0"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'markets', {})
|
|
markets = list(data.values())
|
|
return self.parse_markets(markets)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# {
|
|
# "id": "02ac5b1f0000000200000002",
|
|
# "side": "BUY",
|
|
# "size": "0.0501",
|
|
# "price": "115732",
|
|
# "type": "LIMIT",
|
|
# "createdAt": "2025-07-25T05:11:09.800Z",
|
|
# "createdAtHeight": "44849951"
|
|
# }
|
|
#
|
|
timestamp = self.parse8601(self.safe_string(trade, 'createdAt'))
|
|
symbol = market['symbol']
|
|
price = self.safe_string(trade, 'price')
|
|
amount = self.safe_string(trade, 'size')
|
|
side = self.safe_string_lower(trade, 'side')
|
|
id = self.safe_string(trade, 'id')
|
|
return self.safe_trade({
|
|
'id': id,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': symbol,
|
|
'side': side,
|
|
'price': price,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'order': None,
|
|
'takerOrMaker': None,
|
|
'type': None,
|
|
'fee': None,
|
|
'info': trade,
|
|
}, market)
|
|
|
|
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://developer.woox.io/api-reference/endpoint/public_data/marketTrades
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
response = self.indexerGetTradesPerpetualMarketMarket(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "trades": [
|
|
# {
|
|
# "id": "02ac5b1f0000000200000002",
|
|
# "side": "BUY",
|
|
# "size": "0.0501",
|
|
# "price": "115732",
|
|
# "type": "LIMIT",
|
|
# "createdAt": "2025-07-25T05:11:09.800Z",
|
|
# "createdAtHeight": "44849951"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'trades', [])
|
|
return self.parse_trades(rows, market, since, limit)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# {
|
|
# "startedAt": "2025-07-25T09:47:00.000Z",
|
|
# "ticker": "BTC-USD",
|
|
# "resolution": "1MIN",
|
|
# "low": "116099",
|
|
# "high": "116099",
|
|
# "open": "116099",
|
|
# "close": "116099",
|
|
# "baseTokenVolume": "0",
|
|
# "usdVolume": "0",
|
|
# "trades": 0,
|
|
# "startingOpenInterest": "54.0594",
|
|
# "orderbookMidPriceOpen": "115845.5",
|
|
# "orderbookMidPriceClose": "115845.5"
|
|
# }
|
|
#
|
|
return [
|
|
self.parse8601(self.safe_string(ohlcv, 'startedAt')),
|
|
self.safe_number(ohlcv, 'open'),
|
|
self.safe_number(ohlcv, 'high'),
|
|
self.safe_number(ohlcv, 'low'),
|
|
self.safe_number(ohlcv, 'close'),
|
|
self.safe_number(ohlcv, 'baseTokenVolume'),
|
|
]
|
|
|
|
def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-candles
|
|
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
: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 candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: the latest time in ms to fetch entries for
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
'resolution': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = min(limit, 1000)
|
|
if since is not None:
|
|
request['fromIso'] = self.iso8601(since)
|
|
until = self.safe_integer(params, 'until')
|
|
params = self.omit(params, 'until')
|
|
if until is not None:
|
|
request['toIso'] = self.iso8601(until)
|
|
response = self.indexerGetCandlesPerpetualMarketsMarket(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "candles": [
|
|
# {
|
|
# "startedAt": "2025-07-25T09:47:00.000Z",
|
|
# "ticker": "BTC-USD",
|
|
# "resolution": "1MIN",
|
|
# "low": "116099",
|
|
# "high": "116099",
|
|
# "open": "116099",
|
|
# "close": "116099",
|
|
# "baseTokenVolume": "0",
|
|
# "usdVolume": "0",
|
|
# "trades": 0,
|
|
# "startingOpenInterest": "54.0594",
|
|
# "orderbookMidPriceOpen": "115845.5",
|
|
# "orderbookMidPriceClose": "115845.5"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'candles', [])
|
|
return self.parse_ohlcvs(rows, market, timeframe, since, limit)
|
|
|
|
def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetches historical funding rate prices
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-historical-funding
|
|
|
|
: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 funding rate
|
|
: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()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
until = self.safe_integer(params, 'until')
|
|
if until is not None:
|
|
request['effectiveBeforeOrAt'] = self.iso8601(until)
|
|
response = self.indexerGetHistoricalFundingMarket(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "historicalFunding": [
|
|
# {
|
|
# "ticker": "BTC-USD",
|
|
# "rate": "0",
|
|
# "price": "116302.62419",
|
|
# "effectiveAtHeight": "44865196",
|
|
# "effectiveAt": "2025-07-25T11:00:00.013Z"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rates = []
|
|
rows = self.safe_list(response, 'historicalFunding', [])
|
|
for i in range(0, len(rows)):
|
|
entry = rows[i]
|
|
timestamp = self.parse8601(self.safe_string(entry, 'effectiveAt'))
|
|
marketId = self.safe_string(entry, 'ticker')
|
|
rates.append({
|
|
'info': entry,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'fundingRate': self.safe_number(entry, 'rate'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
})
|
|
sorted = self.sort_by(rates, 'timestamp')
|
|
return self.filter_by_symbol_since_limit(sorted, symbol, since, limit)
|
|
|
|
def handle_public_address(self, methodName: str, params: dict):
|
|
userAux = None
|
|
userAux, params = self.handle_option_and_params(params, methodName, 'user')
|
|
user = userAux
|
|
user, params = self.handle_option_and_params(params, methodName, 'address', userAux)
|
|
if (user is not None) and (user != ''):
|
|
return [user, params]
|
|
if (self.walletAddress is not None) and (self.walletAddress != ''):
|
|
return [self.walletAddress, params]
|
|
raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a user parameter inside \'params\' or the walletAddress set')
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# {
|
|
# "id": "dad46410-3444-5566-a129-19a619300fb7",
|
|
# "subaccountId": "8586bcf6-1f58-5ec9-a0bc-e53db273e7b0",
|
|
# "clientId": "716238006",
|
|
# "clobPairId": "0",
|
|
# "side": "BUY",
|
|
# "size": "0.001",
|
|
# "totalFilled": "0.001",
|
|
# "price": "400000",
|
|
# "type": "LIMIT",
|
|
# "status": "FILLED",
|
|
# "timeInForce": "GTT",
|
|
# "reduceOnly": False,
|
|
# "orderFlags": "64",
|
|
# "goodTilBlockTime": "2025-07-28T12:07:33.000Z",
|
|
# "createdAtHeight": "45058325",
|
|
# "clientMetadata": "2",
|
|
# "updatedAt": "2025-07-28T12:06:35.330Z",
|
|
# "updatedAtHeight": "45058326",
|
|
# "postOnly": False,
|
|
# "ticker": "BTC-USD",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
#
|
|
status = self.parse_order_status(self.safe_string_upper(order, 'status'))
|
|
marketId = self.safe_string(order, 'ticker')
|
|
symbol = self.safe_symbol(marketId, market)
|
|
filled = self.safe_string(order, 'totalFilled')
|
|
timestamp = self.parse8601(self.safe_string(order, 'updatedAt'))
|
|
price = self.safe_string(order, 'price')
|
|
amount = self.safe_string(order, 'size')
|
|
type = self.parse_order_type(self.safe_string_upper(order, 'type'))
|
|
side = self.safe_string_lower(order, 'side')
|
|
timeInForce = self.safe_string_upper(order, 'timeInForce')
|
|
return self.safe_order({
|
|
'info': order,
|
|
'id': self.safe_string(order, 'id'),
|
|
'clientOrderId': self.safe_string(order, 'clientId'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': None,
|
|
'lastUpdateTimestamp': timestamp,
|
|
'symbol': symbol,
|
|
'type': type,
|
|
'timeInForce': timeInForce,
|
|
'postOnly': self.safe_bool(order, 'postOnly'),
|
|
'reduceOnly': self.safe_bool(order, 'reduceOnly'),
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': None,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'average': None,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'status': status,
|
|
'fee': None,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'UNTRIGGERED': 'open',
|
|
'OPEN': 'open',
|
|
'FILLED': 'closed',
|
|
'CANCELED': 'canceled',
|
|
'BEST_EFFORT_CANCELED': 'canceling',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order_type(self, type: Str):
|
|
types: dict = {
|
|
'LIMIT': 'LIMIT',
|
|
'STOP_LIMIT': 'LIMIT',
|
|
'TAKE_PROFIT_LIMIT': 'LIMIT',
|
|
'MARKET': 'MARKET',
|
|
'STOP_MARKET': 'MARKET',
|
|
'TAKE_PROFIT_MARKET': 'MARKET',
|
|
'TRAILING_STOP': 'MARKET',
|
|
}
|
|
return self.safe_string_upper(types, type, type)
|
|
|
|
def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#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
|
|
:returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
'orderId': id,
|
|
}
|
|
order = self.indexerGetOrdersOrderId(self.extend(request, params))
|
|
return self.parse_order(order)
|
|
|
|
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://docs.dydx.xyz/indexer-client/http#list-orders
|
|
|
|
: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 str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
userAddress = None
|
|
subAccountNumber = None
|
|
userAddress, params = self.handle_public_address('fetchOrders', params)
|
|
subAccountNumber, params = self.handle_option_and_params(params, 'fetchOrders', 'subAccountNumber', '0')
|
|
self.load_markets()
|
|
request: dict = {
|
|
'address': userAddress,
|
|
'subaccountNumber': subAccountNumber,
|
|
}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['ticker'] = market['id']
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
response = self.indexerGetOrders(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "id": "dad46410-3444-5566-a129-19a619300fb7",
|
|
# "subaccountId": "8586bcf6-1f58-5ec9-a0bc-e53db273e7b0",
|
|
# "clientId": "716238006",
|
|
# "clobPairId": "0",
|
|
# "side": "BUY",
|
|
# "size": "0.001",
|
|
# "totalFilled": "0.001",
|
|
# "price": "400000",
|
|
# "type": "LIMIT",
|
|
# "status": "FILLED",
|
|
# "timeInForce": "GTT",
|
|
# "reduceOnly": False,
|
|
# "orderFlags": "64",
|
|
# "goodTilBlockTime": "2025-07-28T12:07:33.000Z",
|
|
# "createdAtHeight": "45058325",
|
|
# "clientMetadata": "2",
|
|
# "updatedAt": "2025-07-28T12:06:35.330Z",
|
|
# "updatedAtHeight": "45058326",
|
|
# "postOnly": False,
|
|
# "ticker": "BTC-USD",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_orders(response, 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://docs.dydx.xyz/indexer-client/http#list-orders
|
|
|
|
: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 str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'OPEN', # ['OPEN', 'FILLED', 'CANCELED', 'BEST_EFFORT_CANCELED', 'UNTRIGGERED', 'BEST_EFFORT_OPENED']
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple closed orders made by the user
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#list-orders
|
|
|
|
: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 str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'status': 'FILLED', # ['OPEN', 'FILLED', 'CANCELED', 'BEST_EFFORT_CANCELED', 'UNTRIGGERED', 'BEST_EFFORT_OPENED']
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.extend(request, params))
|
|
|
|
def parse_position(self, position: dict, market: Market = None):
|
|
#
|
|
# {
|
|
# "market": "BTC-USD",
|
|
# "status": "OPEN",
|
|
# "side": "SHORT",
|
|
# "size": "-0.407",
|
|
# "maxSize": "-0.009",
|
|
# "entryPrice": "118692.04840909090909090909",
|
|
# "exitPrice": "119526.565625",
|
|
# "realizedPnl": "476.42665909090909090909088",
|
|
# "unrealizedPnl": "-57.26681734000000000000037",
|
|
# "createdAt": "2025-07-14T07:53:55.631Z",
|
|
# "createdAtHeight": "44140908",
|
|
# "closedAt": null,
|
|
# "sumOpen": "0.44",
|
|
# "sumClose": "0.032",
|
|
# "netFunding": "503.13121",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
#
|
|
marketId = self.safe_string(position, 'market')
|
|
market = self.safe_market(marketId, market)
|
|
symbol = market['symbol']
|
|
side = self.safe_string_lower(position, 'side')
|
|
quantity = self.safe_string(position, 'size')
|
|
if side != 'long':
|
|
quantity = Precise.string_mul('-1', quantity)
|
|
timestamp = self.parse8601(self.safe_string(position, 'createdAt'))
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': symbol,
|
|
'entryPrice': self.safe_number(position, 'entryPrice'),
|
|
'markPrice': None,
|
|
'notional': None,
|
|
'collateral': None,
|
|
'unrealizedPnl': self.safe_number(position, 'unrealizedPnl'),
|
|
'side': side,
|
|
'contracts': self.parse_number(quantity),
|
|
'contractSize': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'hedged': None,
|
|
'maintenanceMargin': None,
|
|
'maintenanceMarginPercentage': None,
|
|
'initialMargin': None,
|
|
'initialMarginPercentage': None,
|
|
'leverage': None,
|
|
'liquidationPrice': None,
|
|
'marginRatio': None,
|
|
'marginMode': None,
|
|
'percentage': None,
|
|
})
|
|
|
|
def fetch_position(self, symbol: str, params={}):
|
|
"""
|
|
fetch data on an open position
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#list-positions
|
|
|
|
:param str symbol: unified market symbol of the market the position is held in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict: a `position structure <https://docs.ccxt.com/?id=position-structure>`
|
|
"""
|
|
positions = self.fetch_positions([symbol], params)
|
|
return self.safe_dict(positions, 0, {})
|
|
|
|
def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#list-positions
|
|
|
|
:param str[] [symbols]: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/?id=position-structure>`
|
|
"""
|
|
userAddress = None
|
|
subAccountNumber = None
|
|
userAddress, params = self.handle_public_address('fetchPositions', params)
|
|
subAccountNumber, params = self.handle_option_and_params(params, 'fetchOrders', 'subAccountNumber', '0')
|
|
self.load_markets()
|
|
request: dict = {
|
|
'address': userAddress,
|
|
'subaccountNumber': subAccountNumber,
|
|
'status': 'OPEN', # ['OPEN', 'CLOSED', 'LIQUIDATED']
|
|
}
|
|
response = self.indexerGetPerpetualPositions(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "positions": [
|
|
# {
|
|
# "market": "BTC-USD",
|
|
# "status": "OPEN",
|
|
# "side": "SHORT",
|
|
# "size": "-0.407",
|
|
# "maxSize": "-0.009",
|
|
# "entryPrice": "118692.04840909090909090909",
|
|
# "exitPrice": "119526.565625",
|
|
# "realizedPnl": "476.42665909090909090909088",
|
|
# "unrealizedPnl": "-57.26681734000000000000037",
|
|
# "createdAt": "2025-07-14T07:53:55.631Z",
|
|
# "createdAtHeight": "44140908",
|
|
# "closedAt": null,
|
|
# "sumOpen": "0.44",
|
|
# "sumClose": "0.032",
|
|
# "netFunding": "503.13121",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'positions', [])
|
|
return self.parse_positions(rows, symbols)
|
|
|
|
def hash_message(self, message):
|
|
return self.hash(message, 'keccak', 'hex')
|
|
|
|
def sign_hash(self, hash, privateKey):
|
|
signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None)
|
|
r = signature['r']
|
|
s = signature['s']
|
|
return {
|
|
'r': r.rjust(64, '0'),
|
|
's': s.rjust(64, '0'),
|
|
'v': self.sum(27, signature['v']),
|
|
}
|
|
|
|
def sign_message(self, message, privateKey):
|
|
return self.sign_hash(self.hash_message(message), privateKey[-64:])
|
|
|
|
def sign_onboarding_action(self) -> object:
|
|
message = {'action': 'dYdX Chain Onboarding'}
|
|
chainId = self.options['chainId']
|
|
domain: dict = {
|
|
'chainId': chainId,
|
|
'name': 'dYdX Chain',
|
|
}
|
|
messageTypes: dict = {
|
|
'dYdX': [
|
|
{'name': 'action', 'type': 'string'},
|
|
],
|
|
}
|
|
msg = self.eth_encode_structured_data(domain, messageTypes, message)
|
|
if self.privateKey is None or self.privateKey == '':
|
|
raise ArgumentsRequired(self.id + ' signOnboardingAction() requires a privateKey to be set.')
|
|
signature = self.sign_message(msg, self.privateKey)
|
|
return signature
|
|
|
|
def sign_dydx_tx(self, privateKey: str, message: Any, memo: str, chainId: str, account: Any, authenticators: Any, fee=None) -> str:
|
|
encodedTx, signDoc = self.encode_dydx_tx_for_signing(message, memo, chainId, account, authenticators, fee)
|
|
signature = self.sign_hash(encodedTx, privateKey)
|
|
return self.encode_dydx_tx_raw(signDoc, signature['r'] + signature['s'])
|
|
|
|
def retrieve_credentials(self) -> Any:
|
|
credentials = self.safe_dict(self.options, 'dydxCredentials')
|
|
if credentials is not None:
|
|
return credentials
|
|
entropy = self.safe_string(self.options, 'mnemonic')
|
|
if entropy is None:
|
|
signature = self.sign_onboarding_action()
|
|
entropy = self.hash_message(self.base16_to_binary(signature['r'] + signature['s']))
|
|
credentials = self.retrieve_dydx_credentials(entropy)
|
|
credentials['privateKey'] = self.binary_to_base16(credentials['privateKey'])
|
|
credentials['publicKey'] = self.binary_to_base16(credentials['publicKey'])
|
|
self.options['dydxCredentials'] = credentials
|
|
return credentials
|
|
|
|
def fetch_dydx_account(self):
|
|
# required in js
|
|
self.load_dydx_protos()
|
|
dydxAccount = self.safe_dict(self.options, 'dydxAccount')
|
|
if dydxAccount is not None:
|
|
return dydxAccount
|
|
if self.walletAddress is None:
|
|
raise ArgumentsRequired(self.id + ' fetchDydxAccount() requires the walletAddress to be set using the dydx chain address eg: dydx1cpb4tedmwq304c2kc9pwzjwq0sc6z2a4tasxrz')
|
|
if not self.walletAddress.startswith('dydx'):
|
|
raise ArgumentsRequired(self.id + ' fetchDydxAccount() requires a valid dydx chain address, starting with dydx, not the l1 address.')
|
|
request = {
|
|
'dydxAddress': self.walletAddress,
|
|
}
|
|
#
|
|
# {
|
|
# "info": {
|
|
# "address": "string",
|
|
# "pub_key": {
|
|
# "type_url": "string",
|
|
# "key": "string"
|
|
# },
|
|
# "account_number": "string",
|
|
# "sequence": "string"
|
|
# }
|
|
# }
|
|
#
|
|
response = self.nodeRestGetCosmosAuthV1beta1AccountInfoDydxAddress(request)
|
|
account = self.safe_dict(response, 'info')
|
|
account['pub_key'] = {
|
|
# encode with binary key would fail in python
|
|
'key': account['pub_key']['key'],
|
|
}
|
|
self.options['dydxAccount'] = account
|
|
return account
|
|
|
|
def pow(self, n: str, m: str):
|
|
r = Precise.string_mul(n, '1')
|
|
c = self.parse_to_int(m)
|
|
# TODO: cap
|
|
for i in range(1, c):
|
|
r = Precise.string_mul(r, n)
|
|
return r
|
|
|
|
def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only', False)
|
|
orderType = type.upper()
|
|
market = self.market(symbol)
|
|
orderSide = side.upper()
|
|
subaccountId = 0
|
|
subaccountId, params = self.handle_option_and_params(params, 'createOrder', 'subAccountId', subaccountId)
|
|
triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice')
|
|
stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice)
|
|
takeProfitPrice = self.safe_value(params, 'takeProfitPrice')
|
|
isConditional = triggerPrice is not None or stopLossPrice is not None or takeProfitPrice is not None
|
|
isMarket = orderType == 'MARKET'
|
|
timeInForce = self.safe_string_upper(params, 'timeInForce', 'GTT')
|
|
postOnly = self.is_post_only(isMarket, None, params)
|
|
amountStr = self.amount_to_precision(symbol, amount)
|
|
priceStr = self.price_to_precision(symbol, price)
|
|
marketInfo = self.safe_dict(market, 'info')
|
|
atomicResolution = marketInfo['atomicResolution']
|
|
quantumScale = self.pow('10', Precise.string_neg(atomicResolution))
|
|
quantums = Precise.string_mul(amountStr, quantumScale)
|
|
quantumConversionExponent = marketInfo['quantumConversionExponent']
|
|
priceScale = self.pow('10', Precise.string_sub(Precise.string_sub(atomicResolution, quantumConversionExponent), '-6'))
|
|
subticks = Precise.string_mul(priceStr, priceScale)
|
|
clientMetadata = 0
|
|
conditionalType = 0
|
|
conditionalOrderTriggerSubticks = '0'
|
|
orderFlag = None
|
|
timeInForceNumber = None
|
|
if timeInForce == 'FOK':
|
|
raise InvalidOrder(self.id + ' timeInForce fok has been deprecated')
|
|
if orderType == 'MARKET':
|
|
# short-term
|
|
orderFlag = 0
|
|
clientMetadata = 1 # STOP_MARKET / TAKE_PROFIT_MARKET
|
|
if timeInForce is not None:
|
|
# default is ioc
|
|
timeInForceNumber = 1
|
|
elif orderType == 'LIMIT':
|
|
if timeInForce == 'GTT':
|
|
# long-term
|
|
orderFlag = 64
|
|
if postOnly:
|
|
timeInForceNumber = 2
|
|
else:
|
|
timeInForceNumber = 0
|
|
else:
|
|
orderFlag = 0
|
|
if timeInForce == 'IOC':
|
|
timeInForceNumber = 1
|
|
else:
|
|
raise InvalidOrder('unexpected code path: timeInForce')
|
|
if isConditional:
|
|
# conditional
|
|
orderFlag = 32
|
|
if stopLossPrice is not None:
|
|
conditionalType = 1
|
|
conditionalOrderTriggerSubticks = self.price_to_precision(symbol, stopLossPrice)
|
|
elif takeProfitPrice is not None:
|
|
conditionalType = 2
|
|
conditionalOrderTriggerSubticks = self.price_to_precision(symbol, takeProfitPrice)
|
|
conditionalOrderTriggerSubticks = Precise.string_mul(conditionalOrderTriggerSubticks, priceScale)
|
|
latestBlockHeight = self.safe_integer(params, 'latestBlockHeight')
|
|
goodTillBlock = self.safe_integer(params, 'goodTillBlock')
|
|
goodTillBlockTime = None
|
|
goodTillBlockTimeInSeconds = 2592000
|
|
goodTillBlockTimeInSeconds, params = self.handle_option_and_params(params, 'createOrder', 'goodTillBlockTimeInSeconds', goodTillBlockTimeInSeconds) # default is 30 days
|
|
if orderFlag == 0:
|
|
if goodTillBlock is None:
|
|
# short term order
|
|
goodTillBlock = latestBlockHeight + 20
|
|
else:
|
|
if goodTillBlockTimeInSeconds is None:
|
|
raise ArgumentsRequired('goodTillBlockTimeInSeconds is required.')
|
|
goodTillBlockTime = self.seconds() + goodTillBlockTimeInSeconds
|
|
sideNumber = 1 if (orderSide == 'BUY') else 2
|
|
defaultClientOrderId = self.rand_number(9) # 2**32 - 1 is 10 digits, but it may overflow with 10
|
|
clientOrderId = self.safe_integer(params, 'clientOrderId', defaultClientOrderId)
|
|
orderPayload = {
|
|
'order': {
|
|
'orderId': {
|
|
'subaccountId': {
|
|
'owner': self.get_wallet_address(),
|
|
'number': subaccountId,
|
|
},
|
|
'clientId': clientOrderId,
|
|
'orderFlags': orderFlag,
|
|
'clobPairId': marketInfo['clobPairId'],
|
|
},
|
|
'side': sideNumber,
|
|
'quantums': self.to_dydx_long(quantums),
|
|
'subticks': self.to_dydx_long(subticks),
|
|
'goodTilBlock': goodTillBlock,
|
|
'goodTilBlockTime': goodTillBlockTime,
|
|
'timeInForce': timeInForceNumber,
|
|
'reduceOnly': reduceOnly,
|
|
'clientMetadata': clientMetadata,
|
|
'conditionType': conditionalType,
|
|
'conditionalOrderTriggerSubticks': self.to_dydx_long(conditionalOrderTriggerSubticks),
|
|
'orderRouterAddress': self.safe_string(self.options, 'routerAddress', 'dydx165sfn2k3vucvq7gklauy2r3agyjw4c3m60ascn'),
|
|
},
|
|
}
|
|
signingPayload = {
|
|
'typeUrl': '/dydxprotocol.clob.MsgPlaceOrder',
|
|
'value': orderPayload,
|
|
}
|
|
params = self.omit(params, ['reduceOnly', 'reduce_only', 'clientOrderId', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit', 'latestBlockHeight', 'goodTillBlock', 'goodTillBlockTimeInSeconds', 'subaccountId'])
|
|
orderId = self.create_order_id_from_parts(self.get_wallet_address(), subaccountId, clientOrderId, orderFlag, marketInfo['clobPairId'])
|
|
return [orderId, self.extend(signingPayload, params)]
|
|
|
|
def create_order_id_from_parts(self, address: str, subAccountNumber: float, clientOrderId: float, orderFlags: float, clobPairId: float) -> str:
|
|
nameSp = self.safe_string(self.options, 'namespace', '0f9da948-a6fb-4c45-9edc-4685c3f3317d')
|
|
prefixAddress = address + '-' + str(subAccountNumber)
|
|
prefix = self.uuid5(nameSp, prefixAddress)
|
|
orderInfo = prefix + '-' + self.number_to_string(clientOrderId) + '-' + self.number_to_string(clobPairId) + '-' + self.number_to_string(orderFlags)
|
|
return self.uuid5(nameSp, orderInfo)
|
|
|
|
def fetch_latest_block_height(self, params={}) -> int:
|
|
response = self.nodeRpcGetAbciInfo(params)
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": -1,
|
|
# "result": {
|
|
# "response": {
|
|
# "data": "dydxprotocol",
|
|
# "version": "9.1.0-rc0",
|
|
# "last_block_height": "49157714",
|
|
# "last_block_app_hash": "9LHAcDDI5zmWiC6bGiiGtxuWPlKJV+/fTBZk/WQ/Y4U="
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
info = self.safe_dict(result, 'response')
|
|
return self.safe_integer(info, 'last_block_height')
|
|
|
|
def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
|
|
"""
|
|
|
|
https://docs.dydx.xyz/interaction/trading#place-an-order
|
|
|
|
create a trade 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 fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.timeInForce]: "GTT", "IOC", or "PO"
|
|
:param float [params.triggerPrice]: The price a trigger order is triggered at
|
|
:param float [params.stopLossPrice]: price for a stoploss order
|
|
:param float [params.takeProfitPrice]: price for a takeprofit order
|
|
:param str [params.clientOrderId]: a unique id for the order
|
|
:param bool [params.postOnly]: True or False whether the order is post-only
|
|
:param bool [params.reduceOnly]: True or False whether the order is reduce-only
|
|
:param float [params.goodTillBlock]: expired block number for the order, required for market order and non limit GTT order, default value is latestBlockHeight + 20
|
|
:param float [params.goodTillBlockTimeInSeconds]: expired time elapsed for the order, required for limit GTT order and conditional, default value is 30 days
|
|
:returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
credentials = self.retrieve_credentials()
|
|
account = self.fetch_dydx_account()
|
|
lastBlockHeight = self.fetch_latest_block_height()
|
|
# params['latestBlockHeight'] = lastBlockHeight
|
|
newParams = self.extend(params, {'latestBlockHeight': lastBlockHeight})
|
|
orderRequestRes = self.create_order_request(symbol, type, side, amount, price, newParams)
|
|
orderId = orderRequestRes[0]
|
|
orderRequest = orderRequestRes[1]
|
|
chainName = self.options['chainName']
|
|
signedTx = self.sign_dydx_tx(credentials['privateKey'], orderRequest, '', chainName, account, None)
|
|
request = {
|
|
'tx': signedTx,
|
|
}
|
|
# nodeRpcGetBroadcastTxAsync
|
|
response = self.nodeRpcGetBroadcastTxSync(request)
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": -1,
|
|
# "result": {
|
|
# "code": 0,
|
|
# "data": "",
|
|
# "log": "[]",
|
|
# "codespace": "",
|
|
# "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
return self.safe_order({
|
|
'info': result,
|
|
'id': orderId,
|
|
'clientOrderId': orderRequest['value']['order']['orderId']['clientId'],
|
|
})
|
|
|
|
def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order:
|
|
"""
|
|
cancels an open order
|
|
|
|
https://docs.dydx.xyz/interaction/trading/#cancel-an-order
|
|
|
|
:param str id: it should be the hasattr(self, clientOrderId) case
|
|
: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 used when creating the order
|
|
:param boolean [params.trigger]: whether the order is a trigger/algo order
|
|
:param float [params.orderFlags]: default is 64, orderFlags for the order, market order and non limit GTT order is 0, limit GTT order is 64 and conditional order is 32
|
|
:param float [params.goodTillBlock]: expired block number for the order, required for market order and non limit GTT order(orderFlags = 0), default value is latestBlockHeight + 20
|
|
:param float [params.goodTillBlockTimeInSeconds]: expired time elapsed for the order, required for limit GTT order and conditional(orderFlagss > 0), default value is 30 days
|
|
:param int [params.subAccountId]: sub account id, default is 0
|
|
:returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False)
|
|
params = self.omit(params, ['trigger', 'stop'])
|
|
if not isTrigger and (symbol is None):
|
|
raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument')
|
|
self.load_markets()
|
|
market: Market = self.market(symbol)
|
|
clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientId', id)
|
|
if clientOrderId is None:
|
|
raise ArgumentsRequired(self.id + ' cancelOrder() requires a clientOrderId parameter, cancelling using id is not currently supported.')
|
|
idString = str(id)
|
|
if id is not None and idString.find('-') > -1:
|
|
raise NotSupported(self.id + ' cancelOrder() cancelling using id is not currently supported, please use provide the clientOrderId parameter.')
|
|
goodTillBlock = self.safe_integer(params, 'goodTillBlock')
|
|
goodTillBlockTimeInSeconds = 2592000
|
|
goodTillBlockTimeInSeconds, params = self.handle_option_and_params(params, 'cancelOrder', 'goodTillBlockTimeInSeconds', goodTillBlockTimeInSeconds) # default is 30 days
|
|
goodTillBlockTime = None
|
|
defaultOrderFlags = 32 if (isTrigger) else 64
|
|
orderFlags = self.safe_integer(params, 'orderFlags', defaultOrderFlags)
|
|
subAccountId = 0
|
|
subAccountId, params = self.handle_option_and_params(params, 'cancelOrder', 'subAccountId', subAccountId)
|
|
params = self.omit(params, ['clientOrderId', 'orderFlags', 'goodTillBlock', 'goodTillBlockTime', 'goodTillBlockTimeInSeconds', 'subaccountId', 'clientId'])
|
|
if orderFlags != 0 and orderFlags != 64 and orderFlags != 32:
|
|
raise InvalidOrder(self.id + ' invalid orderFlags, allowed values are(0, 64, 32).')
|
|
if orderFlags > 0:
|
|
if goodTillBlockTimeInSeconds is None:
|
|
raise ArgumentsRequired(self.id + ' goodTillBlockTimeInSeconds is required in params for long term or conditional order.')
|
|
if goodTillBlock is not None and goodTillBlock > 0:
|
|
raise InvalidOrder(self.id + ' goodTillBlock should be 0 for long term or conditional order.')
|
|
goodTillBlockTime = self.seconds() + goodTillBlockTimeInSeconds
|
|
else:
|
|
if goodTillBlock is None:
|
|
latestBlockHeight = self.fetch_latest_block_height()
|
|
goodTillBlock = latestBlockHeight + 20
|
|
credentials = self.retrieve_credentials()
|
|
account = self.fetch_dydx_account()
|
|
cancelPayload = {
|
|
'orderId': {
|
|
'subaccountId': {
|
|
'owner': self.get_wallet_address(),
|
|
'number': subAccountId,
|
|
},
|
|
'clientId': clientOrderId,
|
|
'orderFlags': orderFlags,
|
|
'clobPairId': market['info']['clobPairId'],
|
|
},
|
|
'goodTilBlock': goodTillBlock,
|
|
'goodTilBlockTime': goodTillBlockTime,
|
|
}
|
|
signingPayload = {
|
|
'typeUrl': '/dydxprotocol.clob.MsgCancelOrder',
|
|
'value': cancelPayload,
|
|
}
|
|
chainName = self.options['chainName']
|
|
signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, '', chainName, account, None)
|
|
request = {
|
|
'tx': signedTx,
|
|
}
|
|
# nodeRpcGetBroadcastTxAsync
|
|
response = self.nodeRpcGetBroadcastTxSync(request)
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": -1,
|
|
# "result": {
|
|
# "code": 0,
|
|
# "data": "",
|
|
# "log": "[]",
|
|
# "codespace": "",
|
|
# "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
return self.safe_order({
|
|
'info': result,
|
|
})
|
|
|
|
def cancel_orders(self, ids: List[str], symbol: Str = None, params={}):
|
|
"""
|
|
cancel multiple orders
|
|
:param str[] ids: order ids
|
|
:param str [symbol]: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str[] [params.clientOrderIds]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma
|
|
:param int [params.subAccountId]: sub account id, default is 0
|
|
:returns dict: an list of `order structures <https://docs.ccxt.com/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market: Market = self.market(symbol)
|
|
clientOrderIds = self.safe_list(params, 'clientOrderIds')
|
|
if not clientOrderIds:
|
|
raise NotSupported(self.id + ' cancelOrders only support clientOrderIds.')
|
|
subAccountId = 0
|
|
subAccountId, params = self.handle_option_and_params(params, 'cancelOrders', 'subAccountId', subAccountId)
|
|
goodTillBlock = self.safe_integer(params, 'goodTillBlock')
|
|
if goodTillBlock is None:
|
|
latestBlockHeight = self.fetch_latest_block_height()
|
|
goodTillBlock = latestBlockHeight + 20
|
|
params = self.omit(params, ['clientOrderIds', 'goodTillBlock', 'subaccountId'])
|
|
credentials = self.retrieve_credentials()
|
|
account = self.fetch_dydx_account()
|
|
cancelOrders = {
|
|
'clientIds': clientOrderIds,
|
|
'clobPairId': market['info']['clobPairId'],
|
|
}
|
|
cancelPayload = {
|
|
'subaccountId': {
|
|
'owner': self.get_wallet_address(),
|
|
'number': subAccountId,
|
|
},
|
|
'shortTermCancels': [cancelOrders],
|
|
'goodTilBlock': goodTillBlock,
|
|
}
|
|
signingPayload = {
|
|
'typeUrl': '/dydxprotocol.clob.MsgBatchCancel',
|
|
'value': cancelPayload,
|
|
}
|
|
chainName = self.options['chainName']
|
|
signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, '', chainName, account, None)
|
|
request = {
|
|
'tx': signedTx,
|
|
}
|
|
# nodeRpcGetBroadcastTxAsync
|
|
response = self.nodeRpcGetBroadcastTxSync(request)
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": -1,
|
|
# "result": {
|
|
# "code": 0,
|
|
# "data": "",
|
|
# "log": "[]",
|
|
# "codespace": "",
|
|
# "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
|
|
# }
|
|
# }
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
return [self.safe_order({
|
|
'info': result,
|
|
})]
|
|
|
|
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://docs.dydx.xyz/indexer-client/http#get-perpetual-market-orderbook
|
|
|
|
: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
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'market': market['id'],
|
|
}
|
|
response = self.indexerGetOrderbooksPerpetualMarketMarket(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "bids": [
|
|
# {
|
|
# "price": "118267",
|
|
# "size": "0.3182"
|
|
# }
|
|
# ],
|
|
# "asks": [
|
|
# {
|
|
# "price": "118485",
|
|
# "size": "0.0001"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'size')
|
|
|
|
def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
|
|
#
|
|
# {
|
|
# "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
|
|
# "sender": {
|
|
# "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
|
|
# "subaccountNumber": 0
|
|
# },
|
|
# "recipient": {
|
|
# "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
|
|
# "subaccountNumber": 1
|
|
# },
|
|
# "size": "0.000001",
|
|
# "createdAt": "2025-07-29T09:43:02.105Z",
|
|
# "createdAtHeight": "45116125",
|
|
# "symbol": "USDC",
|
|
# "type": "TRANSFER_OUT",
|
|
# "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
|
|
# }
|
|
#
|
|
currencyId = self.safe_string(item, 'symbol')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
currency = self.safe_currency(currencyId, currency)
|
|
type = self.safe_string_upper(item, 'type')
|
|
direction = None
|
|
if type is not None:
|
|
if type == 'TRANSFER_IN' or type == 'DEPOSIT':
|
|
direction = 'in'
|
|
elif type == 'TRANSFER_OUT' or type == 'WITHDRAWAL':
|
|
direction = 'out'
|
|
amount = self.safe_string(item, 'size')
|
|
timestamp = self.parse8601(self.safe_string(item, 'createdAt'))
|
|
sender = self.safe_dict(item, 'sender')
|
|
recipient = self.safe_dict(item, 'recipient')
|
|
return self.safe_ledger_entry({
|
|
'info': item,
|
|
'id': self.safe_string(item, 'id'),
|
|
'direction': direction,
|
|
'account': self.safe_string(sender, 'address'),
|
|
'referenceAccount': self.safe_string(recipient, 'address'),
|
|
'referenceId': self.safe_string(item, 'transactionHash'),
|
|
'type': self.parse_ledger_entry_type(type),
|
|
'currency': code,
|
|
'amount': self.parse_number(amount),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'before': None,
|
|
'after': None,
|
|
'status': None,
|
|
'fee': None,
|
|
}, currency)
|
|
|
|
def parse_ledger_entry_type(self, type):
|
|
ledgerType: dict = {
|
|
'TRANSFER_IN': 'transfer',
|
|
'TRANSFER_OUT': 'transfer',
|
|
'DEPOSIT': 'deposit',
|
|
'WITHDRAWAL': 'withdrawal',
|
|
}
|
|
return self.safe_string(ledgerType, type, type)
|
|
|
|
def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
|
|
"""
|
|
fetch the history of changes, actions done by the user or operations that altered balance of the user
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-transfers
|
|
|
|
:param str [code]: unified currency code, default is None
|
|
:param int [since]: timestamp in ms of the earliest ledger entry, default is None
|
|
:param int [limit]: max number of ledger entries to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict: a `ledger structure <https://docs.ccxt.com/?id=ledger-entry-structure>`
|
|
"""
|
|
self.load_markets()
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchLedger'}))
|
|
return self.parse_ledger(response, currency, since, limit)
|
|
|
|
def estimate_tx_fee(self, message: Any, memo: str, account: Any) -> Any:
|
|
txBytes = self.encode_dydx_tx_for_simulation(message, memo, account['sequence'], account['pub_key'])
|
|
request = {
|
|
'txBytes': txBytes,
|
|
}
|
|
response = self.nodeRestPostCosmosTxV1beta1Simulate(request)
|
|
#
|
|
# {
|
|
# gas_info: {gas_wanted: '18446744073709551615', gas_used: '86055'},
|
|
# result: {
|
|
# ...
|
|
# }
|
|
# }
|
|
#
|
|
gasInfo = self.safe_dict(response, 'gas_info')
|
|
if gasInfo is None:
|
|
raise ExchangeError(self.id + ' failed to simulate transaction.')
|
|
gasUsed = self.safe_string(gasInfo, 'gas_used')
|
|
if gasUsed is None:
|
|
raise ExchangeError(self.id + ' failed to simulate transaction.')
|
|
defaultFeeDenom = self.safe_string(self.options, 'defaultFeeDenom')
|
|
defaultFeeMultiplier = self.safe_string(self.options, 'defaultFeeMultiplier')
|
|
feeDenom = self.safe_dict(self.options, 'feeDenom')
|
|
gasPrice = None
|
|
denom = None
|
|
if defaultFeeDenom == 'uusdc':
|
|
gasPrice = feeDenom['USDC_GAS_PRICE']
|
|
denom = feeDenom['USDC_DENOM']
|
|
else:
|
|
gasPrice = feeDenom['CHAINTOKEN_GAS_PRICE']
|
|
denom = feeDenom['CHAINTOKEN_DENOM']
|
|
gasLimit = int(math.ceil(self.parse_to_numeric(Precise.string_mul(gasUsed, defaultFeeMultiplier))))
|
|
feeAmount = Precise.string_mul(self.number_to_string(gasLimit), gasPrice)
|
|
if feeAmount.find('.') >= 0:
|
|
feeAmount = self.number_to_string(int(math.ceil(self.parse_to_numeric(feeAmount))))
|
|
feeObj = {
|
|
'amount': feeAmount,
|
|
'denom': denom,
|
|
}
|
|
return {
|
|
'amount': [feeObj],
|
|
'gasLimit': gasLimit,
|
|
}
|
|
|
|
def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
"""
|
|
transfer currency internally between wallets on the same account
|
|
:param str code: unified currency code
|
|
:param float amount: amount to transfer
|
|
:param str fromAccount: account to transfer from *main, subaccount*
|
|
:param str toAccount: account to transfer to *subaccount, address*
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.vaultAddress]: the vault address for order
|
|
:returns dict: a `transfer structure <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
if code != 'USDC':
|
|
raise NotSupported(self.id + ' transfer() only support USDC')
|
|
self.load_markets()
|
|
fromSubaccountId = self.safe_integer(params, 'fromSubaccountId')
|
|
toSubaccountId = self.safe_integer(params, 'toSubaccountId')
|
|
if fromAccount != 'main':
|
|
# raise error if from subaccount id is undefind
|
|
if fromAccount is None:
|
|
raise NotSupported(self.id + ' transfer only support main > subaccount and subaccount <> subaccount.')
|
|
if fromSubaccountId is None or toSubaccountId is None:
|
|
raise ArgumentsRequired(self.id + ' transfer requires fromSubaccountId and toSubaccountId.')
|
|
params = self.omit(params, ['fromSubaccountId', 'toSubaccountId'])
|
|
credentials = self.retrieve_credentials()
|
|
account = self.fetch_dydx_account()
|
|
usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000'))
|
|
payload = None
|
|
signingPayload = None
|
|
if fromAccount == 'main':
|
|
# deposit to subaccount
|
|
if toSubaccountId is None:
|
|
raise ArgumentsRequired(self.id + ' transfer() requeire toSubaccoutnId.')
|
|
payload = {
|
|
'sender': self.get_wallet_address(),
|
|
'recipient': {
|
|
'owner': self.get_wallet_address(),
|
|
'number': toSubaccountId,
|
|
},
|
|
'assetId': 0,
|
|
'quantums': usd,
|
|
}
|
|
signingPayload = {
|
|
'typeUrl': '/dydxprotocol.sending.MsgDepositToSubaccount',
|
|
'value': payload,
|
|
}
|
|
else:
|
|
payload = {
|
|
'transfer': {
|
|
'sender': {
|
|
'owner': fromAccount,
|
|
'number': fromSubaccountId,
|
|
},
|
|
'recipient': {
|
|
'owner': toAccount,
|
|
'number': toSubaccountId,
|
|
},
|
|
'assetId': 0,
|
|
'amount': usd,
|
|
},
|
|
}
|
|
signingPayload = {
|
|
'typeUrl': '/dydxprotocol.sending.MsgCreateTransfer',
|
|
'value': payload,
|
|
}
|
|
txFee = self.estimate_tx_fee(signingPayload, '', account)
|
|
chainName = self.options['chainName']
|
|
signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, '', chainName, account, None, txFee)
|
|
request = {
|
|
'tx': signedTx,
|
|
}
|
|
# nodeRpcGetBroadcastTxAsync
|
|
response = self.nodeRpcGetBroadcastTxSync(request)
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": -1,
|
|
# "result": {
|
|
# "code": 0,
|
|
# "data": "",
|
|
# "log": "[]",
|
|
# "codespace": "",
|
|
# "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
|
|
# }
|
|
# }
|
|
#
|
|
return self.parse_transfer(response)
|
|
|
|
def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
|
|
#
|
|
# {
|
|
# "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
|
|
# "sender": {
|
|
# "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
|
|
# "subaccountNumber": 0
|
|
# },
|
|
# "recipient": {
|
|
# "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
|
|
# "subaccountNumber": 1
|
|
# },
|
|
# "size": "0.000001",
|
|
# "createdAt": "2025-07-29T09:43:02.105Z",
|
|
# "createdAtHeight": "45116125",
|
|
# "symbol": "USDC",
|
|
# "type": "TRANSFER_OUT",
|
|
# "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
|
|
# }
|
|
#
|
|
id = self.safe_string(transfer, 'id')
|
|
currencyId = self.safe_string(transfer, 'symbol')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
amount = self.safe_number(transfer, 'size')
|
|
sender = self.safe_dict(transfer, 'sender')
|
|
recipient = self.safe_dict(transfer, 'recipient')
|
|
fromAccount = self.safe_string(sender, 'address')
|
|
toAccount = self.safe_string(recipient, 'address')
|
|
timestamp = self.parse8601(self.safe_string(transfer, 'createdAt'))
|
|
return {
|
|
'info': transfer,
|
|
'id': id,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'currency': code,
|
|
'amount': amount,
|
|
'fromAccount': fromAccount,
|
|
'toAccount': toAccount,
|
|
'status': 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://docs.dydx.xyz/indexer-client/http#get-transfers
|
|
|
|
: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
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict[]: a list of `transfer structures <https://docs.ccxt.com/?id=transfer-structure>`
|
|
"""
|
|
self.load_markets()
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchTransfers'}))
|
|
transferIn = self.filter_by(response, 'type', 'TRANSFER_IN')
|
|
transferOut = self.filter_by(response, 'type', 'TRANSFER_OUT')
|
|
rows = self.array_concat(transferIn, transferOut)
|
|
return self.parse_transfers(rows, currency, since, limit)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# {
|
|
# "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
|
|
# "sender": {
|
|
# "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
|
|
# "subaccountNumber": 0
|
|
# },
|
|
# "recipient": {
|
|
# "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
|
|
# "subaccountNumber": 1
|
|
# },
|
|
# "size": "0.000001",
|
|
# "createdAt": "2025-07-29T09:43:02.105Z",
|
|
# "createdAtHeight": "45116125",
|
|
# "symbol": "USDC",
|
|
# "type": "TRANSFER_OUT",
|
|
# "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
|
|
# }
|
|
#
|
|
id = self.safe_string(transaction, 'id')
|
|
sender = self.safe_dict(transaction, 'sender')
|
|
recipient = self.safe_dict(transaction, 'recipient')
|
|
addressTo = self.safe_string(recipient, 'address')
|
|
addressFrom = self.safe_string(sender, 'address')
|
|
txid = self.safe_string(transaction, 'transactionHash')
|
|
currencyId = self.safe_string(transaction, 'symbol')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
timestamp = self.parse8601(self.safe_string(transaction, 'createdAt'))
|
|
amount = self.safe_number(transaction, 'size')
|
|
return {
|
|
'info': transaction,
|
|
'id': id,
|
|
'txid': txid,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'network': None,
|
|
'address': addressTo,
|
|
'addressTo': addressTo,
|
|
'addressFrom': addressFrom,
|
|
'tag': None,
|
|
'tagTo': None,
|
|
'tagFrom': None,
|
|
'type': self.safe_string_lower(transaction, 'type'), # 'deposit', 'withdrawal'
|
|
'amount': amount,
|
|
'currency': code,
|
|
'status': None,
|
|
'updated': None,
|
|
'internal': None,
|
|
'comment': None,
|
|
'fee': None,
|
|
}
|
|
|
|
def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a 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
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
if code != 'USDC':
|
|
raise NotSupported(self.id + ' withdraw() only support USDC')
|
|
self.load_markets()
|
|
self.check_address(address)
|
|
subaccountId = self.safe_integer(params, 'subaccountId')
|
|
if subaccountId is None:
|
|
raise ArgumentsRequired(self.id + ' withdraw requires subaccountId.')
|
|
params = self.omit(params, ['subaccountId'])
|
|
currency = self.currency(code)
|
|
credentials = self.retrieve_credentials()
|
|
account = self.fetch_dydx_account()
|
|
usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000'))
|
|
payload = {
|
|
'sender': {
|
|
'owner': self.get_wallet_address(),
|
|
'number': subaccountId,
|
|
},
|
|
'recipient': address,
|
|
'assetId': 0,
|
|
'quantums': usd,
|
|
}
|
|
signingPayload = {
|
|
'typeUrl': '/dydxprotocol.sending.MsgWithdrawFromSubaccount',
|
|
'value': payload,
|
|
}
|
|
txFee = self.estimate_tx_fee(signingPayload, tag, account)
|
|
chainName = self.options['chainName']
|
|
signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, tag, chainName, account, None, txFee)
|
|
request = {
|
|
'tx': signedTx,
|
|
}
|
|
# nodeRpcGetBroadcastTxAsync
|
|
response = self.nodeRpcGetBroadcastTxSync(request)
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": -1,
|
|
# "result": {
|
|
# "code": 0,
|
|
# "data": "",
|
|
# "log": "[]",
|
|
# "codespace": "",
|
|
# "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'result', {})
|
|
return self.parse_transaction(data, currency)
|
|
|
|
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.dydx.xyz/indexer-client/http#get-transfers
|
|
|
|
:param str code: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch withdrawals for
|
|
:param int [limit]: the maximum number of withdrawals structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchWithdrawals'}))
|
|
rows = self.filter_by(response, 'type', 'WITHDRAWAL')
|
|
return self.parse_transactions(rows, currency, since, limit)
|
|
|
|
def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all deposits made to an account
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-transfers
|
|
|
|
: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 str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchDeposits'}))
|
|
rows = self.filter_by(response, 'type', 'DEPOSIT')
|
|
return self.parse_transactions(rows, currency, since, limit)
|
|
|
|
def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch history of deposits and withdrawals
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-transfers
|
|
|
|
:param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
|
|
:param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
|
|
:param int [limit]: max number of deposit/withdrawals to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:param str [params.subAccountNumber]: sub account number
|
|
:returns dict: a list of `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchDepositsWithdrawals'}))
|
|
withdrawals = self.filter_by(response, 'type', 'WITHDRAWAL')
|
|
deposits = self.filter_by(response, 'type', 'DEPOSIT')
|
|
rows = self.array_concat(withdrawals, deposits)
|
|
return self.parse_transactions(rows, currency, since, limit)
|
|
|
|
def fetch_transactions_helper(self, code: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
methodName = self.safe_string(params, 'methodName')
|
|
params = self.omit(params, 'methodName')
|
|
userAddress = None
|
|
subAccountNumber = None
|
|
userAddress, params = self.handle_public_address(methodName, params)
|
|
subAccountNumber, params = self.handle_option_and_params(params, methodName, 'subAccountNumber', '0')
|
|
request: dict = {
|
|
'address': userAddress,
|
|
'subaccountNumber': subAccountNumber,
|
|
}
|
|
response = self.indexerGetTransfers(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "transfers": [
|
|
# {
|
|
# "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
|
|
# "sender": {
|
|
# "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
|
|
# "subaccountNumber": 0
|
|
# },
|
|
# "recipient": {
|
|
# "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
|
|
# "subaccountNumber": 1
|
|
# },
|
|
# "size": "0.000001",
|
|
# "createdAt": "2025-07-29T09:43:02.105Z",
|
|
# "createdAtHeight": "45116125",
|
|
# "symbol": "USDC",
|
|
# "type": "TRANSFER_OUT",
|
|
# "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
return self.safe_list(response, 'transfers', [])
|
|
|
|
def fetch_accounts(self, params={}) -> List[Account]:
|
|
"""
|
|
fetch all the accounts associated with a profile
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-subaccounts
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.address]: wallet address that made trades
|
|
:returns dict: a dictionary of `account structures <https://docs.ccxt.com/?id=account-structure>` indexed by the account type
|
|
"""
|
|
userAddress = None
|
|
userAddress, params = self.handle_public_address('fetchAccounts', params)
|
|
request: dict = {
|
|
'address': userAddress,
|
|
}
|
|
response = self.indexerGetAddressesAddress(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "subaccounts": [
|
|
# {
|
|
# "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
|
|
# "subaccountNumber": 0,
|
|
# "equity": "25346.73993597",
|
|
# "freeCollateral": "24207.8530595294",
|
|
# "openPerpetualPositions": {
|
|
# "BTC-USD": {
|
|
# "market": "BTC-USD",
|
|
# "status": "OPEN",
|
|
# "side": "SHORT",
|
|
# "size": "-0.491",
|
|
# "maxSize": "-0.009",
|
|
# "entryPrice": "118703.60811320754716981132",
|
|
# "exitPrice": "119655.95",
|
|
# "realizedPnl": "3075.17994830188679245283016",
|
|
# "unrealizedPnl": "1339.12776155490566037735812",
|
|
# "createdAt": "2025-07-14T07:53:55.631Z",
|
|
# "createdAtHeight": "44140908",
|
|
# "closedAt": null,
|
|
# "sumOpen": "0.53",
|
|
# "sumClose": "0.038",
|
|
# "netFunding": "3111.36894",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
# },
|
|
# "assetPositions": {
|
|
# "USDC": {
|
|
# "size": "82291.083758",
|
|
# "symbol": "USDC",
|
|
# "side": "LONG",
|
|
# "assetId": "0",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
# },
|
|
# "marginEnabled": True,
|
|
# "updatedAtHeight": "45234659",
|
|
# "latestProcessedBlockHeight": "45293477"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rows = self.safe_list(response, 'subaccounts', [])
|
|
result = []
|
|
for i in range(0, len(rows)):
|
|
account = rows[i]
|
|
accountId = self.safe_string(account, 'subaccountNumber')
|
|
result.append({
|
|
'id': accountId,
|
|
'type': None,
|
|
'currency': None,
|
|
'info': account,
|
|
'code': None,
|
|
})
|
|
return result
|
|
|
|
def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
query for balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://docs.dydx.xyz/indexer-client/http#get-subaccount
|
|
|
|
: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()
|
|
userAddress = None
|
|
userAddress, params = self.handle_public_address('fetchAccounts', params)
|
|
subaccountNumber = None
|
|
subaccountNumber, params = self.handle_option_and_params(params, 'fetchAccounts', 'subaccountNumber', 0)
|
|
request: dict = {
|
|
'address': userAddress,
|
|
'subaccountNumber': subaccountNumber,
|
|
}
|
|
response = self.indexerGetAddressesAddressSubaccountNumberSubaccountNumber(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "subaccount": {
|
|
# "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
|
|
# "subaccountNumber": 0,
|
|
# "equity": "161451.040416029",
|
|
# "freeCollateral": "152508.28819133578",
|
|
# "openPerpetualPositions": {
|
|
# "ETH-USD": {
|
|
# "market": "ETH-USD",
|
|
# "status": "OPEN",
|
|
# "side": "LONG",
|
|
# "size": "0.001",
|
|
# "maxSize": "0.002",
|
|
# "entryPrice": "3894.7",
|
|
# "exitPrice": "3864.5",
|
|
# "realizedPnl": "-0.034847",
|
|
# "unrealizedPnl": "-0.044675155",
|
|
# "createdAt": "2025-10-22T08:34:05.883Z",
|
|
# "createdAtHeight": "52228825",
|
|
# "closedAt": null,
|
|
# "sumOpen": "0.002",
|
|
# "sumClose": "0.001",
|
|
# "netFunding": "-0.004647",
|
|
# "subaccountNumber": 0
|
|
# },
|
|
# "BTC-USD": {
|
|
# "market": "BTC-USD",
|
|
# "status": "OPEN",
|
|
# "side": "SHORT",
|
|
# "size": "-4.1368",
|
|
# "maxSize": "-0.009",
|
|
# "entryPrice": "112196.87848803433219017636",
|
|
# "exitPrice": "113885.21872652924977050823",
|
|
# "realizedPnl": "-15180.426770788459736511679821",
|
|
# "unrealizedPnl": "17002.285719484425404321566048",
|
|
# "createdAt": "2025-07-14T07:53:55.631Z",
|
|
# "createdAtHeight": "44140908",
|
|
# "closedAt": null,
|
|
# "sumOpen": "5.3361",
|
|
# "sumClose": "1.1983",
|
|
# "netFunding": "-13157.288663",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
# },
|
|
# "assetPositions": {
|
|
# "USDC": {
|
|
# "size": "608580.951601",
|
|
# "symbol": "USDC",
|
|
# "side": "LONG",
|
|
# "assetId": "0",
|
|
# "subaccountNumber": 0
|
|
# }
|
|
# },
|
|
# "marginEnabled": True,
|
|
# "updatedAtHeight": "52228833",
|
|
# "latestProcessedBlockHeight": "52246761"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'subaccount')
|
|
return self.parse_balance(data)
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
account = self.account()
|
|
account['free'] = self.safe_string(response, 'freeCollateral')
|
|
result: dict = {
|
|
'info': response,
|
|
'USDC': account,
|
|
}
|
|
return self.safe_balance(result)
|
|
|
|
def nonce(self):
|
|
return self.milliseconds() - self.options['timeDifference']
|
|
|
|
def get_wallet_address(self):
|
|
if self.walletAddress is not None and self.walletAddress != '':
|
|
return self.walletAddress
|
|
dydxAccount = self.safe_dict(self.options, 'dydxAccount')
|
|
if dydxAccount is not None:
|
|
# return dydxAccount
|
|
wallet = self.safe_string(dydxAccount, 'address')
|
|
if wallet is not None:
|
|
return wallet
|
|
raise ArgumentsRequired(self.id + ' getWalletAddress() requires a wallet address. Set `walletAddress` or `dydxAccount` in exchange options.')
|
|
|
|
def sign(self, path, section='public', method='GET', params={}, headers=None, body=None):
|
|
pathWithParams = self.implode_params(path, params)
|
|
url = self.implode_hostname(self.urls['api'][section])
|
|
params = self.omit(params, self.extract_params(path))
|
|
params = self.keysort(params)
|
|
url += '/' + pathWithParams
|
|
if method == 'GET':
|
|
if params:
|
|
url += '?' + self.urlencode(params)
|
|
else:
|
|
body = self.json(params)
|
|
headers = {
|
|
'Content-type': 'application/json',
|
|
}
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if not response:
|
|
return None # fallback to default error handler
|
|
#
|
|
# abci response
|
|
# {"result": {"code": 0}}
|
|
#
|
|
# rest response
|
|
# {"code": 123}
|
|
#
|
|
result = self.safe_dict(response, 'result')
|
|
errorCode = self.safe_string(result, 'code')
|
|
if not errorCode:
|
|
errorCode = self.safe_string(response, 'code')
|
|
if errorCode:
|
|
errorCodeNum = self.parse_to_numeric(errorCode)
|
|
if errorCodeNum > 0:
|
|
feedback = self.id + ' ' + self.json(response)
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback)
|
|
raise ExchangeError(feedback)
|
|
return None
|
|
|
|
def set_sandbox_mode(self, enable: bool):
|
|
super(dydx, self).set_sandbox_mode(enable)
|
|
# rewrite testnet parameters
|
|
self.options['chainName'] = 'dydx-testnet-4'
|
|
self.options['chainId'] = 11155111
|
|
self.options['feeDenom']['CHAINTOKEN_DENOM'] = 'adv4tnt'
|