[TOC]
Con el rápido auge de los exchanges descentralizados (DEX) en el ámbito de trading de criptomonedas, los traders cuantitativos han comenzado a migrar gradualmente hacia estas plataformas para realizar operaciones automatizadas eficientes. dYdX, como una de las plataformas de trading descentralizado más populares, ofrece funcionalidades robustas para transacciones, soportando contratos perpetuos de futuros. Su versión más reciente, v4, optimiza el rendimiento y la experiencia de usuario, convirtiéndose en la elección principal de muchos traders algorítmicos.
Este artículo explorará cómo implementar estrategias de trading cuantitativo en dYdX v4, incluyendo el uso de su API para operar, obtener datos de mercado y gestionar cuentas.
dYdX v3
, las transacciones generan recompensas en tokens dYdX
.El protocolo anterior dYdX v3 para el exchange DEX ha sido descontinuado. La dirección actual de la App dYdX v4 es:
Tras abrir la App, el botón para conectar wallet se encuentra en la esquina superior derecha. Escanee el código QR para vincular su wallet.
Para realizar pruebas en el entorno de testnet, utilice:
El proceso es similar: conéctese mediante el botón superior derecho, autentique con firma digital. Tras una conexión exitosa, se generará automáticamente una dirección dYdX v4 visible en el menú superior. Desde allí se acceden operaciones como depósito, retiro y transferencias. Una diferencia clave entre mainnet y testnet es que en testnet, al hacer clic en “Depositar”, se reciben automáticamente 300 USDC de prueba mediante un faucet. Para trading real en mainnet, se requiere depositar USDC reales, proceso compatible con múltiples activos y cadenas.
Dirección de cuenta en dYdX v4
Derivada de la dirección del wallet, tiene el formato dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
(prefijo dydx1). Puede consultarse en exploradores de blockchain.
Frase semilla (mnemonic) Desde el menú superior, seleccione “Exportar frase secreta” para obtener el mnemonic de la cuenta. En la plataforma FMZ, este se configura al añadir el exchange.
El mnemonic puede almacenarse directamente en FMZ o gestionarse localmente. En las secciones prácticas demostraremos su uso mediante objetos de exchange dydx v4.
El entorno de testnet presenta algunas variaciones respecto a mainnet:
Transferencias entre subcuentas.
En mainnet existe un mecanismo de consolidación para subcuentas con subAccountNumber >= 128
: si no tienen posiciones abiertas, sus fondos se transfieren automáticamente a la subcuenta 0.
En testnet, este comportamiento no se observó (o tiene condiciones de activación diferentes).
Nomenclatura de tokens.
El token nativo difiere: DYDX
en mainnet vs Dv4TNT
en testnet.
Configuración de endpoints (chain IDs, nodos REST, etc.). Ejemplos de configuración:
Mainnet:
Endpoint del indexador: https://indexer.dydx.trade
Chain ID: dydx-mainnet-1
Nodo REST: https://dydx-dao-api.polkachu.com:443
Testnet:
Endpoint del indexador: https://indexer.v4testnet.dydx.exchange
Chain ID: dydx-testnet-4
Nodo REST: https://dydx-testnet-api.polkachu.com
El protocolo dYdX v4 se desarrolló sobre el ecosistema Cosmos. Su sistema de trading consta de dos componentes principales:
El servicio del indexador ofrece APIs REST y WebSocket.
API REST Permite consultar datos de mercado, estados de cuenta, posiciones y órdenes. En FMZ, estas interfaces están encapsuladas como APIs unificadas.
WebSocket
Utilice la función Dial
en FMZ para establecer conexiones WebSocket y suscribirse a flujos de datos.
Nota crítica: El indexador de dYdX v4 comparte una limitación común con exchanges centralizados: la latencia en actualizaciones de datos. Por ejemplo, consultar una orden inmediatamente después de crearla podría no reflejarla. Se recomienda introducir retardos (Sleep(n)
) tras operaciones críticas.
Ejemplo de conexión WebSocket para obtener el libro de órdenes:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// Suscripción
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// Libro de órdenes
var orderBook = null
// Actualizar libro
var updateOrderbook = function(orderbook, update) {
// Actualizar bids
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Eliminar bid a precio 'price'
```javascript
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// Actualizar o agregar orden de compra
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// Ordenar por precio en orden descendente
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// Actualizar asks
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Eliminar orden de venta con precio price
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// Actualizar o agregar orden de venta
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// Ordenar por precio en orden ascendente
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)
}
// Monitorizar mensajes
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// real: wss://indexer.dydx.trade/v4/ws
// simulación: 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) + "`")
}
}
### Mensajes de difusión en nodos de la cadena dYdX
Los tipos más utilizados en transacciones son: mensajes de órdenes, cancelaciones y transferencias.
- Resumen de mensajes de órdenes
```JSON
{
"@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 limitada:
En las funciones encapsuladas por FMZ, se utiliza el valor ORDER_FLAGS_LONG_TERM = 64 # Orden de largo plazo
. Según las limitaciones del protocolo dydx v4, se aplica el plazo máximo de validez de 90 días (todas las órdenes en dydx v4 tienen caducidad).
Orden de mercado:
Las funciones de FMZ utilizan ORDER_FLAGS_SHORT_TERM = 0 # Orden de corto plazo
. Siguiendo la recomendación del protocolo:
// Recomendado establecer precio del oráculo -5% para VENTA, +5% para COMPRA
Al no ser órdenes de mercado reales, se utiliza el precio del oráculo con un deslizamiento del 5%. Las órdenes de corto plazo usan expiración por altura de bloque, configurada como bloque actual +10 según sugerencia del protocolo.
ID de orden:
Al ejecutarse directamente en cadena, no se genera OrderID por índices. Para garantizar unicidad y precisión en consultas, el ID se compone de (separados por comas):
Resumen de mensajes de cancelación
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Requiere el OrderID devuelto por la API de órdenes de FMZ.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
En dydx v4, cada dirección puede crear múltiples subcuentas. La subcuenta 0 es creada automáticamente, mientras que las subcuentas con número ≥128 se usan para trading con margen aislado, requiriendo mínimo 20 USDC.
Ejemplo: transferencias entre subcuenta 0 ↔ 128. Las transferencias consumen Gas Fee, pagadero en USDC o token dydx.
El contenido anterior explica brevemente algunos detalles de encapsulamiento. Ahora procederemos a implementar el uso específico utilizando la testnet de dYdX v4 para demostración. La testnet es prácticamente idéntica a la mainnet y cuenta con un faucet automático para obtener assets de prueba. No se repetirá la operación de implementación del custodio, realizaremos pruebas en vivo directamente en FMZ.
Después de conectar exitosamente una wallet de criptomonedas a la aplicación dYdX v4 (en este caso utilizamos imToken), reclame los assets de prueba y exporte el mnemónico de la cuenta actual de dYdX v4 (derivada de la wallet).
Configure el mnemónico en la plataforma FMZ utilizando el método de archivo local (también se puede ingresar directamente, el mnemónico se configura encriptado, no en texto plano).
Colóquelo en el directorio del ID de operación en vivo dentro de la carpeta del custodio, aunque también puede ubicarse en otras rutas (debe especificarse la ruta completa durante la configuración).
En el campo de mnemónico ingrese: file:///mnemonic.txt
. La ruta real corresponde a: directorio_del_custodio/logs/storage/594291
.
function main() {
// Cambiar la dirección del indexador de la cadena de prueba
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Modificar ChainId para testnet
exchange.IO("chainId", "dydx-testnet-4")
// Cambiar dirección del nodo REST para testnet
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Prueba de lectura de información de cuenta
Log(exchange.GetAccount())
}
Información de cuenta obtenida de testnet:
{
"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
}
Prueba realizada en mainnet sin cambiar a testnet
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])
}
### 4. Colocación de órdenes
```js
function main() {
// Cambiar la dirección del indexador de la cadena de prueba
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Cambiar ChainId de la cadena de prueba
exchange.IO("chainId", "dydx-testnet-4")
// Cambiar dirección del nodo REST de la cadena de prueba
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Orden limitada, colocar orden
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// Orden de mercado
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idVenta:", idSell)
Log("idCompra:", idBuy)
Log("idMercado:", idMarket)
}
Interfaz de dYdX v4 App:
Prueba de obtención y cancelación de órdenes existentes en testnet.
function main() {
// Cambiar dirección del indexador de la cadena de prueba
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Cambiar ChainId de la cadena de prueba
exchange.IO("chainId", "dydx-testnet-4")
// Cambiar dirección del nodo REST de la cadena de prueba
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
var orders = exchange.GetOrders()
Log("Órdenes:", orders)
for (var order of orders) {
exchange.CancelOrder(order.Id, order)
Sleep(2000)
}
var tbl = {type: "table", title: "Prueba 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() {
// Cambiar dirección del indexador de la cadena de prueba
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Cambiar ChainId de la cadena de prueba
exchange.IO("chainId", "dydx-testnet-4")
// Cambiar dirección del nodo REST de la cadena de prueba
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: "Prueba 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() {
// Cambiar dirección del indexador de la cadena de prueba
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Cambiar ChainId de la cadena de prueba
exchange.IO("chainId", "dydx-testnet-4")
// Cambiar dirección del nodo REST de la cadena de prueba
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// subAccountNumber 0 -> 128 : 20 USDC , Tarifa Gas como adv4tnt (token dydx)
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// Cambiar a subcuenta subAccountNumber 128, leer y verificar información de cuenta
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
Cambio a subcuenta con subAccountNumber 128, datos devueltos por GetAccount:
{
"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 confirma que en la subcuenta subAccountNumber 128 se han depositado 20 USDC.
Método para obtener TxHash de órdenes mediante llamadas REST
El exchange dydx almacena en caché los TxHash, consultables mediante ID de orden. Sin embargo, el mapeo se borra al detener la estrategia.
function main() {
// Configurar dirección del indexador para testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Especificar ChainId de testnet
exchange.IO("chainId", "dydx-testnet-4")
// Configurar endpoint REST de testnet
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)
// Para vaciar la tabla de mapeo se puede usar: 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)
}
}
Mensaje obtenido al consultar mediante TxHash:
var ret = exchange.IO(“api”, “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)
Contenido demasiado extenso, se muestra un extracto:
{
"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 la última versión del host. Es necesario descargar el host más reciente para compatibilidad con dYdX v4 DEX
Gracias por su apoyo y por leer este material.