[TOC]
Com a rápida ascensão das exchanges descentralizadas (DEX) no universo de negociação de criptomoedas, traders quantitativos estão gradualmente migrando para estas plataformas para execução automatizada eficiente. O dYdX, como uma das principais plataformas de trading descentralizado, oferece funcionalidades robustas para contratos perpétuos de futuros. Sua versão v4 traz otimizações de performance e experiência do usuário, consolidando-se como escolha preferencial para muitos profissionais de trading algorítmico.
Este artigo abordará práticas de trading quantitativo no dYdX v4, incluindo:
dYdX v3
, transações geram recompensas em tokens dYdX
.A versão anterior dYdX v3 foi descontinuada. O endereço atual do App v4:
Após acessar, clique no botão “Connect Wallet” no canto superior direito para autenticação via QR code.
Para testes na testnet:
O processo de conexão é idêntico. Após autenticação bem-sucedida, um endereço dYdX v4 será gerado (formato: dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
), visível no menu superior. Principais funcionalidades:
Endereço dYdX v4 Derivado do endereço da carteira conectada, auditável em blockchain explorers.
Frase Mnemônica Acessível via menu “Export Secret Phrase”. Essencial para configuração na plataforma FMZ:
Principais distinções operacionais:
Transferência entre Subcontas
≥128
sem posiçõesDenominação de Tokens
DYDX
(mainnet) vs Dv4TNT
(testnet)Configurações de Rede Exemplo de endpoints:
Mainnet
Indexador: https://indexer.dydx.trade
Chain ID: dydx-mainnet-1
Nó REST: https://dydx-dao-api.polkachu.com:443
Testnet
Indexador: https://indexer.v4testnet.dydx.exchange
Chain ID: dydx-testnet-4
Nó REST: https://dydx-testnet-api.polkachu.com
Baseado no ecossistema Cosmos, o sistema compreende dois componentes principais:
Fornece interfaces dual protocol:
REST API Consultas de book de ordens, posições e histórico. Integrado na FMZ via wrappers padronizados.
WebSocket Conexões persistentes para dados em tempo real. Exemplo de implementação:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// Assinar canais
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// Livro de ordens
var orderBook = null
// Atualizar livro
var updateOrderbook = function(orderbook, update) {
// Atualizar lados de compra
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Remover nível de preço
```javascript
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// Atualizar ou adicionar ordem de compra
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// Ordenar por preço em ordem decrescente
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// Atualizar asks
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Remover ordem de venda com preço price
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// Atualizar ou adicionar ordem de venda
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// Ordenar por preço em ordem crescente
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)
}
// Monitorar
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// real: wss://indexer.dydx.trade/v4/ws
// simulação: 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) + "`")
}
}
### Transmissão de mensagens de nó da cadeia dYdX
Os tipos mais utilizados em transações são: mensagens de ordem, cancelamento e transferência.
- Resumo da mensagem de ordem
```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
}
}
Ordem limitada:
A função de colocação de pedidos encapsulada na plataforma FMZ utiliza o valor ORDER_FLAGS_LONG_TERM = 64
para ordens limitadas. De acordo com as restrições do protocolo dydx v4, é utilizado o período de validade máximo de 90 dias (todos os tipos de ordens no dydx v4 possuem prazo de validade).
Ordem a mercado:
Para ordens a mercado, a plataforma FMZ utiliza ORDER_FLAGS_SHORT_TERM = 0
. Seguindo a recomendação do protocolo dydx v4:
// Recomenda-se definir para o preço do oráculo -5% ou menos para VENDA, preço do oráculo +5% para COMPRA
Como não são verdadeiras ordens de mercado, utiliza-se o preço do oráculo com ajuste de 5% como slippage. A validade difere das ordens de longo prazo, sendo definida por altura de bloco (current block + 10 blocos).
ID da ordem: Como a execução ocorre diretamente na blockchain, não há ID gerado por indexadores. Para garantir unicidade e precisão na consulta, o ID retornado é composto por (separados por vírgula):
Resumo da mensagem de cancelamento
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
É necessário passar o ID do pedido retornado pela interface de pedidos da plataforma FMZ.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Subcontas com subAccountNumber ≥ 128 são destinadas a produtos de margem isolada, requerendo mínimo de 20 USDC. Transferências entre subcontas (ex: 0 ↔ 128) consomem taxas de rede (Gas Fee), pagáveis em USDC ou token dydx. A subconta 0 é criada automaticamente como primária.
O conteúdo acima explica brevemente alguns detalhes de encapsulamento. Vamos agora praticar o uso específico, utilizando a testnet do dYdX v4 para demonstração. A testnet é essencialmente idêntica à mainnet, com um faucet automático para obter ativos de teste. Não repetiremos os detalhes de implantação do custodiante, focando na criação de testes reais na FMZ.
Após conectar com sucesso uma carteira de criptomoedas ao dYdX v4 App (utilizamos a carteira imToken), obtenha os ativos de teste e exporte o mnemônico da conta atual do dYdX v4 (derivada da carteira).
Configure o mnemônico na plataforma FMZ usando o método de arquivo local (também é possível inserir diretamente, com criptografia no armazenamento).
Coloque-o no diretório do ID da estratégia do custodiante, ou em outro local (especifique o caminho absoluto na configuração).
No campo de mnemônico, insira: file:///mnemonic.txt
. O caminho real corresponde a: Diretório do custodiante/logs/storage/594291
.
function main() {
// Alterar o endereço do indexador da rede de teste
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Alterar ChainId da rede de teste
exchange.IO("chainId", "dydx-testnet-4")
// Alterar endereço do nó REST da rede de teste
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Teste de leitura das informações da conta
Log(exchange.GetAccount())
}
Resultado da leitura das informações da conta na 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
}
Teste realizado na mainnet sem mudança para 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. Realizar ordens
```js
function main() {
// Alterar endereço do indexador da testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Configurar ChainId da testnet
exchange.IO("chainId", "dydx-testnet-4")
// Configurar endpoint REST da testnet
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Ordem limitada, fazer oferta
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// Ordem a mercado
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idSell:", idSell)
Log("idBuy:", idBuy)
Log("idMarket:", idMarket)
}
Página do aplicativo dYdX v4:
Teste prévio de duas ordens na testnet: recuperar ordens ativas e cancelar ordens.
function main() {
// Alterar endereço do indexador da testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Configurar ChainId da testnet
exchange.IO("chainId", "dydx-testnet-4")
// Configurar endpoint REST da testnet
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() {
// Alterar endereço do indexador da testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Configurar ChainId da testnet
exchange.IO("chainId", "dydx-testnet-4")
// Configurar endpoint REST da testnet
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() {
// Alterar endereço do indexador da testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Configurar ChainId da testnet
exchange.IO("chainId", "dydx-testnet-4")
// Configurar endpoint REST da testnet
```por_Latn
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// subAccountNumber 0 -> 128: 20 USDC, Taxa de Gas é adv4tnt, ou seja, token dydx
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// Alternar para a subconta subAccountNumber 128, ler informações da conta para verificação
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
Alternando para a subconta subAccountNumber 128, dados retornados 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
}
É possível observar que na subconta subAccountNumber 128 foram transferidos 20 USDC.
Como obter TxHash de ordens: método de chamada de nó REST via IO
Como obter o TxHash do pedido: o objeto de exchange dydx armazena em cache o TxHash, que pode ser consultado usando o ID do pedido. No entanto, quando a estratégia é interrompida, o mapeamento de hashes Tx dos pedidos em cache é limpo.
function main() {
// Alterar endereço do indexer para testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Alterar ChainId para testnet
exchange.IO("chainId", "dydx-testnet-4")
// Alterar endereço do nó REST para 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 limpar a tabela de mapeamento, 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)
}
}
Mensagem consultada via TxHash:
var ret = exchange.IO(“api”, “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)
Conteúdo muito extenso, seção demonstrativa:
{
"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": []
},
...
Este teste foi baseado na versão mais recente do hosted agent. É necessário baixar a versão mais atualizada para suportar a DEX dYdX v4
Agradecemos pelo apoio e pela leitura.