Con el rápido aumento de los intercambios descentralizados (DEX) en el campo del comercio de criptomonedas, los comerciantes cuantitativos se han vuelto gradualmente a estas plataformas para un comercio automatizado eficiente. Como una de las plataformas de comercio descentralizado más populares, dYdX proporciona funciones comerciales potentes y admite el comercio de contratos perpetuos de futuros.
Este artículo presentará cómo practicar el comercio cuantitativo en dYdX v4, incluida la forma de usar su API para operar, obtener datos de mercado y administrar cuentas.
dYdX v3
Con Ethereum, el comercio genera recompensas, que son recompensasdYdX
tokens.El anterior intercambio DEX del protocolo dYdX v3 ha estado fuera de línea.
Después de abrir la página de la aplicación, hay un botón para conectarse a la billetera en la esquina superior derecha.
Si desea probar y familiarizarse con el entorno de la red de prueba primero, puede usar la red de prueba:
Además, haga clic en el botón de conexión de la billetera en la esquina superior derecha, escanee el código para conectar la billetera y verifique la firma. Después de que la billetera se conecte con éxito, se generará automáticamente una dirección dydx v4. Esta dirección se mostrará en la esquina superior derecha de la página de la aplicación. Haga clic en ella y aparecerá un menú. Hay operaciones como recargar, retirar y transferir. Una de las diferencias entre la red principal dYdX (entorno de producción) y la red de prueba es que cuando hace clic en recargar en la red de prueba, se cargarán automáticamente 300 activos USDC utilizando el grifo para la prueba. Si desea realizar transacciones reales en dYdX, debe cargar activos USDC. Recargar también es muy conveniente y compatible con múltiples activos y cadenas para recargar.
Dirección de la cuenta dYdX v4
La dirección de la cuenta dYdX v4 se deriva de la dirección de la billetera.dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
, que es una dirección que comienza con dydx1. Esta dirección se puede consultar en exploradores de blockchain.
Mnemónicos
Puede utilizar el botón
Los mnemónicos se pueden configurar directamente en la plataforma FMZ o guardar localmente en el docker.
El entorno de testnet es diferente del entorno de mainnet en algunos aspectos.
subAccountNumber >= 128
, si la subcuenta del ID no tiene posiciones, los activos se limpiarán automáticamente en la subcuenta cuyo subcuenta Número es 0.
Durante los ensayos, se comprobó que la red de ensayo no tiene este mecanismo (o las condiciones de activación son diferentes y no se ha activado en la red de ensayo).DYDX
, red de pruebasDv4TNT
La red principal:
Dirección del indexador:https://indexer.dydx.trade
Identificador de la cadena:dydx-mainnet-1
Nodo REST:https://dydx-dao-api.polkachu.com:443
Redes de prueba:
Dirección del indexador:https://indexer.v4testnet.dydx.exchange
Identificador de la cadena:dydx-testnet-4
Nodo REST:https://dydx-testnet-api.polkachu.com
El protocolo dYdX v4 se desarrolla basado en el ecosistema cosmos.
El servicio de indexación proporciona protocolos REST y Websocket.
Protocolo REST La interfaz del protocolo REST admite consulta de información de mercado, información de cuenta, información de posición, información de pedido y otras consultas, y se ha encapsulado como una interfaz API unificada en la plataforma FMZ.
Protocolo WebSocket En la plataforma FMZ, puede usar la función Dial para crear una conexión Websocket y suscribirse a la información del mercado.
Se debe tener en cuenta que el índice de dydx v4 tiene el mismo problema que los intercambios centralizados, es decir, las actualizaciones de datos no son tan oportunas. Por ejemplo, a veces puede que no pueda encontrar el pedido si lo consulta inmediatamente después de realizar un pedido. Se recomienda esperar unos segundos después (Sleep(n)
) ciertas operaciones antes de realizar la consulta.
Aquí hay un ejemplo de cómo usar la función Dial para crear una conexión Websocket API y suscribirse a los datos del libro de pedidos:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// subscription
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// Order book
var orderBook = null
// Update order book
var updateOrderbook = function(orderbook, update) {
// Update bids
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Delete the buy order with price
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// Update or add a buy order
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// Sort by price descending
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// Update asks
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Delete the sell order with price
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// Update or add a sell order
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// Sort by price ascending
orderbook.asks.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))
}
})
}
return orderbook
}
var conn = Dial(`${streamingPoint}|reconnect=true&payload=${JSON.stringify(msgSubscribe)}`)
if (!conn) {
Log("createWsThread failed.")
return
}
while (true) {
var data = conn.read()
if (data) {
var msg = null
try {
msg = JSON.parse(data)
if (msg["type"] == "subscribed") {
orderBook = msg["contents"]
threading.currentThread().postMessage(orderBook)
} else if (msg["type"] == "channel_data") {
orderBook = updateOrderbook(orderBook, msg["contents"])
threading.currentThread().postMessage(orderBook)
}
} catch (e) {
Log("e.name:", e.name, "e.stack:", e.stack, "e.message:", e.message)
}
}
}
}, streamingPoint, msgSubscribe)
}
// monitor
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// real : wss://indexer.dydx.trade/v4/ws
// simulate : wss://indexer.v4testnet.dydx.exchange/v4/ws
var symbol = "ETH-USD"
var manager = dYdXIndexerWSconnManager("wss://indexer.dydx.trade/v4/ws")
manager.CreateWsThread({"type": "subscribe", "channel": "v4_orderbook", "id": symbol})
var redCode = "#FF0000"
var greenCode = "#006400"
while (true) {
var depthTbl = {type: "table", title: symbol + " / depth", cols: ["level", "price", "amount"], rows: []}
var depth = manager.Peek()
if (depth) {
for (var i = 0; i < depth.asks.length; i++) {
if (i > 9) {
break
}
var ask = depth.asks[i]
depthTbl.rows.push(["asks " + (i + 1) + greenCode, ask.price + greenCode, ask.size + greenCode])
}
depthTbl.rows.reverse()
for (var i = 0; i < depth.bids.length; i++) {
if (i > 9) {
break
}
var bid = depth.bids[i]
depthTbl.rows.push(["bids " + (i + 1) + redCode, bid.price + redCode, bid.size + redCode])
}
}
LogStatus(_D(), "\n`" + JSON.stringify(depthTbl) + "`")
}
}
Los mensajes más utilizados en las transacciones son los mensajes de pedido, los mensajes de cancelación de pedido y los mensajes de transferencia.
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
Orden límite:
En la función de orden encapsulada en la plataforma FMZ, el valor de orderFlags utilizado para las órdenes límite es:ORDER_FLAGS_LONG_TERM = 64 # Long-term order
De acuerdo con las limitaciones del protocolo dydx v4, se utiliza el período de validez del pedido más largo, que es de 90 días (todos los tipos de pedidos en dydx v4 tienen períodos de validez).
Orden del mercado:
En la función de orden encapsulada en la plataforma FMZ, el valor de orderFlags utilizado para las órdenes de mercado es:ORDER_FLAGS_SHORT_TERM = 0 # Short-term order
, de acuerdo con las recomendaciones del protocolo dydx v4:
// Recomendar el precio del oráculo - 5% o menos para vender, precio del oráculo + 5% para comprar
Como no es una orden de mercado verdadera, se utiliza el precio del oráculo, más o menos 5% de deslizamiento como la orden de mercado. El período de validez de las órdenes a corto plazo también es diferente al de las órdenes a largo plazo. Las órdenes a corto plazo utilizan el período de validez de la altura del bloque, que se establece en el bloque actual + 10 alturas del bloque de acuerdo con la recomendación de dydx v4.
Pares de negociación Dirección de la cuenta corriente dydx Número de subcuenta (subcuenta Número) clienteId (generado al azar) clobPairId (ID del símbolo de la transacción) Orden Banderas goodTilData (millisegundos)
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
El ID de pedido devuelto por la interfaz de pedido de la plataforma FMZ debe enviarse.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Muchas subcuentas se pueden crear bajo la dirección dydx v4 actual. La subcuenta con subCuenta Número 0 es la primera subcuenta creada automáticamente. Por ejemplo, puede ir desde el subCuento Número 0 -> 128, o desde el subCuento Número 128 -> 0. Las transferencias requieren una tarifa de gas.
El contenido anterior explica brevemente algunos detalles del empaquetado. A continuación, practicemos el uso específico. Aquí usamos la red de prueba dYdX v4 para la demostración. La red de prueba es básicamente la misma que la red principal, y hay un grifo automático para recibir activos de prueba. La operación de implementación del docker no se repetirá.
Después de conectarse con éxito a la aplicación dYdX v4 utilizando una billetera de criptomonedas (uso la billetera imToken aquí), reclame sus activos de prueba y luego exporte el mnemónico para su cuenta actual de dYdX v4 (derivada de su billetera).
Configurar el mnemónico en la plataforma FMZ. Aquí usamos el método de archivo local para configurarlo (también puede rellenarlo directamente y configurarlo en la plataforma. El mnemónico se configura después del cifrado, no en texto plano).
Por supuesto, también se puede colocar en otros directorios (la ruta específica debe escribirse durante la configuración).
Rellene el cuadro de edición mnemónica:file:///mnemonic.txt
, la trayectoria real correspondiente es:docker directory/logs/storage/594291
.
function main() {
// Switch the indexer address of the test chain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Switch the ChainId of the test chain
exchange.IO("chainId", "dydx-testnet-4")
// Switch the REST node address of the test chain
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Read account information test
Log(exchange.GetAccount())
}
Lea la información de la cuenta de la red de prueba:
{
"Info": {
"subaccounts": [{
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"subaccountNumber": 0,
"equity": "300.386228",
"latestProcessedBlockHeight": "28193227",
"freeCollateral": "300.386228",
"openPerpetualPositions": {},
"assetPositions": {
"USDC": {
"subaccountNumber": 0,
"size": "300.386228",
"symbol": "USDC",
"side": "LONG",
"assetId": "0"
}
},
"marginEnabled": true,
"updatedAtHeight": "28063818"
}, {
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"equity": "0",
"freeCollateral": "0",
"openPerpetualPositions": {},
"marginEnabled": true,
"subaccountNumber": 1,
"assetPositions": {},
"updatedAtHeight": "27770289",
"latestProcessedBlockHeight": "28193227"
}, {
"equity": "0",
"openPerpetualPositions": {},
"marginEnabled": true,
"updatedAtHeight": "28063818",
"latestProcessedBlockHeight": "28193227",
"subaccountNumber": 128,
"freeCollateral": "0",
"assetPositions": {},
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez"
}],
"totalTradingRewards": "0.021744179376211564"
},
"Stocks": 0,
"FrozenStocks": 0,
"Balance": 300.386228,
"FrozenBalance": 0,
"Equity": 300.386228,
"UPnL": 0
}
No cambió a la red de ensayo, probado con la red principal
function main() {
var markets = exchange.GetMarkets()
if (!markets) {
throw "get markets error"
}
var tbl = {type: "table", title: "test markets", cols: ["key", "Symbol", "BaseAsset", "QuoteAsset", "TickSize", "AmountSize", "PricePrecision", "AmountPrecision", "MinQty", "MaxQty", "MinNotional", "MaxNotional", "CtVal"], rows: []}
for (var symbol in markets) {
var market = markets[symbol]
tbl.rows.push([symbol, market.Symbol, market.BaseAsset, market.QuoteAsset, market.TickSize, market.AmountSize, market.PricePrecision, market.AmountPrecision, market.MinQty, market.MaxQty, market.MinNotional, market.MaxNotional, market.CtVal])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
function main() {
// Switch the indexer address of the test chain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Switch the ChainId of the test chain
exchange.IO("chainId", "dydx-testnet-4")
// Switch the REST node address of the test chain
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Limit order, pending order
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// Market order
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idSell:", idSell)
Log("idBuy:", idBuy)
Log("idMarket:", idMarket)
}
Página de la aplicación dYdX v4:
La red de pruebas realiza dos pedidos por adelantado, prueba la obtención de los pedidos pendientes actuales y cancela los pedidos.
function main() {
// Switch the indexer address of the test chain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Switch the ChainId of the test chain
exchange.IO("chainId", "dydx-testnet-4")
// Switch the REST node address of the test chain
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var orders = exchange.GetOrders()
Log("orders:", orders)
for (var order of orders) {
exchange.CancelOrder(order.Id, order)
Sleep(2000)
}
var tbl = {type: "table", title: "test GetOrders", cols: ["Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
for (var order of orders) {
tbl.rows.push([order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
function main() {
// Switch the indexer address of the test chain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Switch the ChainId of the test chain
exchange.IO("chainId", "dydx-testnet-4")
// Switch the REST node address of the test chain
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var p1 = exchange.GetPositions("USD.swap")
var p2 = exchange.GetPositions("ETH_USD.swap")
var p3 = exchange.GetPositions()
var p4 = exchange.GetPositions("SOL_USD.swap")
var tbls = []
for (var positions of [p1, p2, p3, p4]) {
var tbl = {type: "table", title: "test GetPosition/GetPositions", cols: ["Symbol", "Amount", "Price", "FrozenAmount", "Type", "Profit", "Margin", "ContractType", "MarginLevel"], rows: []}
for (var p of positions) {
tbl.rows.push([p.Symbol, p.Amount, p.Price, p.FrozenAmount, p.Type, p.Profit, p.Margin, p.ContractType, p.MarginLevel])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
function main() {
// Switch the indexer address of the test chain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Switch the ChainId of the test chain
exchange.IO("chainId", "dydx-testnet-4")
// Switch the REST node address of the test chain
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// subAccountNumber 0 -> 128 : 20 USDC, Gas Fee is adv4tnt, i.e. dydx token
// var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
// Log("ret:", ret)
// Switch to subaccount subAccountNumber 128 and read account information to check
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
Información del registro:
Cambiar a la subcuenta cuyo subAccountNumber es 128, y los datos devueltos por GetAccount es:
{
"Info": {
"subaccounts": [{
"subaccountNumber": 0,
"assetPositions": {
"USDC": {
"size": "245.696892",
"symbol": "USDC",
"side": "LONG",
"assetId": "0",
"subaccountNumber": 0
}
},
"updatedAtHeight": "28194977",
"latestProcessedBlockHeight": "28195008",
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"freeCollateral": "279.5022142346",
"openPerpetualPositions": {
"ETH-USD": {
"closedAt": null,
"size": "0.01",
"maxSize": "0.01",
"exitPrice": null,
"unrealizedPnl": "-0.17677323",
"subaccountNumber": 0,
"status": "OPEN",
"createdAt": "2024-12-26T03:36:09.264Z",
"createdAtHeight": "28194494",
"sumClose": "0",
"netFunding": "0",
"market": "ETH-USD",
"side": "LONG",
"entryPrice": "3467.2",
"realizedPnl": "0",
"sumOpen": "0.01"
}
},
"marginEnabled": true,
"equity": "280.19211877"
}, {
"openPerpetualPositions": {},
"assetPositions": {},
"marginEnabled": true,
"latestProcessedBlockHeight": "28195008",
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"subaccountNumber": 1,
"equity": "0",
"freeCollateral": "0",
"updatedAtHeight": "27770289"
}, {
"openPerpetualPositions": {},
"updatedAtHeight": "28194977",
"latestProcessedBlockHeight": "28195008",
"address": "dydx1fzsndj35a26maujxff88q2ge5jr4nzfeljn2ez",
"subaccountNumber": 128,
"assetPositions": {
"USDC": {
"assetId": "0",
"subaccountNumber": 128,
"size": "20",
"symbol": "USDC",
"side": "LONG"
}
},
"marginEnabled": true,
"equity": "20",
"freeCollateral": "20"
}],
"totalTradingRewards": "0.021886899964446858"
},
"Stocks": 0,
"FrozenStocks": 0,
"Balance": 20,
"FrozenBalance": 0,
"Equity": 20,
"UPnL": 0
}
Se puede ver que la subcuenta con la subcuenta número 128 ha transferido 20 USDC.
De acuerdo con la orden, obtener TxHash y probar el método de IO llamando nodo REST
¿Cómo obtener el TxHash de una orden? El objeto de intercambio dydx almacenará en caché el TxHash, que se puede consultar utilizando el ID de orden. Sin embargo, después de que se detenga la estrategia, se borrará el mapa de hash de orden tx almacenado en caché.
function main() {
// Switch the indexer address of the test chain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Switch the ChainId of the test chain
exchange.IO("chainId", "dydx-testnet-4")
// Switch the REST node address of the test chain
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var id1 = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.002)
var hash1 = exchange.IO("getTxHash", id1)
Log("id1:", id1, "hash1:", hash1)
var id2 = exchange.CreateOrder("ETH_USD.swap", "buy", 2900, 0.003)
var hash2 = exchange.IO("getTxHash", id2)
Log("id2:", id2, "hash2:", hash2)
// To clear the mapping table, use: exchange.IO("getTxHash", "")
var arr = [hash1, hash2]
Sleep(10000)
for (var txHash of arr) {
// GET https://docs.cosmos.network /cosmos/tx/v1beta1/txs/{hash}
var ret = exchange.IO("api", "GET", "/cosmos/tx/v1beta1/txs/" + txHash)
Log("ret:", ret)
}
}
Mensajes consultados a través de TxHash:
el valor de lasexchange.IO("api",
GET , /cosmos/tx/v1beta1/txs/ + txHash)
El contenido es demasiado largo, así que aquí hay algunos extractos para demostrar:
{
"tx_response": {
"codespace": "",
"code": 0,
"logs": [],
"info": "",
"height": "28195603",
"data": "xxx",
"raw_log": "",
"gas_wanted": "-1",
"gas_used": "0",
"tx": {
"@type": "/cosmos.tx.v1beta1.Tx",
"body": {
"messages": [{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"good_til_block_time": 1742961542,
"condition_type": "CONDITION_TYPE_UNSPECIFIED",
"order_id": {
"clob_pair_id": 1,
"subaccount_id": {
"owner": "xxx",
"number": 0
},
"client_id": 2999181974,
"order_flags": 64
},
"side": "SIDE_BUY",
"quantums": "3000000",
"client_metadata": 0,
"conditional_order_trigger_subticks": "0",
"subticks": "2900000000",
"time_in_force": "TIME_IN_FORCE_UNSPECIFIED",
"reduce_only": false
}
}],
"memo": "FMZ",
"timeout_height": "0",
"extension_options": [],
"non_critical_extension_options": []
},
...
Las pruebas anteriores se basan en el docker más reciente.
Gracias por su apoyo y gracias por leer.