[TOC]
Avec l’essor rapide des exchanges décentralisés (DEX) dans l’écosystème cryptographique, les traders quantitatifs se tournent progressivement vers ces plateformes pour effectuer des transactions automatisées efficaces. dYdX, l’une des plateformes de trading décentralisées les plus populaires, propose des fonctionnalités avancées incluant le trading de contrats perpétuels futures. Sa dernière version v4 améliore les performances et l’expérience utilisateur, devenant ainsi le choix privilégié de nombreux quant traders.
Cet article présente les pratiques de trading quantitatif sur dYdX v4, incluant l’utilisation de son API pour l’exécution d’ordres, l’accès aux données de marché et la gestion de compte.
dYdX v3
, les transactions génèrent des récompenses en tokens dYdX
.L’ancienne version v3 du protocole dYdX DEX a été retirée. L’adresse actuelle de l’application dYdX v4 est :
Après ouverture de l’application, utilisez le bouton de connexion en haut à droite pour lier votre portefeuille via QR code.
Pour une prise en main préalable en environnement test, utilisez :
Le processus de connexion est identique : authentification par signature. Une adresse dYdX v4 est générée automatiquement après connexion, visible en haut à droite. Un menu contextuel permet les opérations de dépôt, retrait et transfert. La différence principale entre mainnet et testnet réside dans le robinet (faucet) automatique de 300 USDC sur le testnet via l’option de dépôt.
Adresse de compte dYdX v4
Dérivée de l’adresse du portefeuille, elle suit le format dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
(préfixe dydx1), consultable sur les explorers blockchain.
Phrase mnémonique Accessible via le bouton « Exporter la phrase secrète » dans le menu contextuel. Cette phrase est nécessaire pour configurer l’échange sur la plateforme FMZ.
La phrase peut être stockée directement sur FMZ ou localement. Les démonstrations pratiques suivantes illustreront son utilisation avec les objets d’échange dYdX v4.
Plusieurs distinctions notables existent entre les environnements :
Transferts inter-sous-comptes
Le mainnet implémente un mécanisme de nettoyage automatique pour les sous-comptes avec subAccountNumber >= 128
sans positions ouvertes. Ce comportement n’est pas observé sur le testnet.
Dénominations de tokens
Le token natif diffère : DYDX
(mainnet) vs Dv4TNT
(testnet)
Configuration réseau Exemples de paramètres :
Mainnet :
Indexer : https://indexer.dydx.trade
Chain ID : dydx-mainnet-1
Nœud REST : https://dydx-dao-api.polkachu.com:443
Testnet :
Indexer : https://indexer.v4testnet.dydx.exchange
Chain ID : dydx-testnet-4
Nœud REST : https://dydx-testnet-api.polkachu.com
Basé sur l’écosystème Cosmos, le système dYdX v4 repose sur deux composants majeurs :
L’indexeur expose des API REST et WebSocket :
API REST Intégrée dans FMZ pour les requêtes standardisées : données de marché, positions, historiques…
WebSocket
Utilisable via la fonction Dial
sur FMZ pour l’abonnement en temps réel.
Une particularité importante : les mises à jour de l’indexeur présentent parfois des latences (exemple : délai d’apparition des nouveaux ordres). Il est recommandé d’implémenter des pauses (Sleep(n)
) après les opérations critiques.
Exemple de connexion WebSocket pour le carnet d’ordres :
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// Abonnement
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// Carnet d'ordres
var orderBook = null
// Mise à jour du carnet
var updateOrderbook = function(orderbook, update) {
// Mise à jour des offres (bids)
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Suppression de l'ordre au prix correspondant
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// Mettre à jour ou ajouter une offre d'achat
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// Tri par prix décroissant
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// Mettre à jour les asks
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Supprimer l'offre de vente au prix price
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// Mettre à jour ou ajouter une offre de vente
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// Tri par prix croissant
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("Échec de la création du thread WebSocket.")
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)
}
// Écoute
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// réel : wss://indexer.dydx.trade/v4/ws
// simulation : wss://indexer.v4testnet.dydx.exchange/v4/ws
var symbol = "ETH-USD"
var manager = dYdXIndexerWSconnManager("wss://indexer.dydx.trade/v4/ws")
```fra_Latn
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) + "`")
}
}
Les messages les plus utilisés dans le trading sont : messages d’ordre, d’annulation et de transfert.
Synopsis des messages d’ordre
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
Ordre limite :
Sur la plateforme FMZ, les fonctions d’ordre encapsulées utilisent la valeur ORDER_FLAGS_LONG_TERM = 64 # Ordre à long terme
pour le paramètre orderFlags
. Conformément aux restrictions du protocole dydx v4, la durée de validité maximale est utilisée (90 jours). Tous les types d’ordre sur dydx v4 ont une expiration.
Ordre au marché :
Les fonctions d’ordre encapsulées utilisent ORDER_FLAGS_SHORT_TERM = 0 # Ordre à court terme
. Selon les recommandations du protocole dydx v4 :
// Recommandé : prix oracle -5% ou moins pour VENTE, prix oracle +5% pour ACHAT
Ce n’étant pas un véritable ordre au marché, le prix oracle est utilisé avec un slippage de ±5%. La validité des ordres courts diffère : elle utilise la hauteur de bloc, configurée à bloc actuel + 10
selon les recommandations.
ID d’ordre :
Les opérations s’effectuant directement sur la chaîne, il n’y a pas d’ID généré par l’indexeur. Pour garantir l’unicité et la traçabilité, l’ID de retour est composé de (séparés par des virgules) :
Synopsis des messages d’annulation
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Requiert l’ID d’ordre retourné par l’interface FMZ.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Sur dydx v4, un compte peut créer plusieurs sous-comptes. Le sous-compte 0 est créé automatiquement. Les sous-comptes avec subAccountNumber ≥ 128
sont dédiés aux produits isolés (requiert minimum 20 USDC).
Exemple : transfert possible entre sous-compte 0 ↔ 128. Les transferts consomment des frais de réseau (Gas Fee), payables en USDC ou token dydx.
Le contenu ci-dessus explique brièvement certains détails d’encapsulation. Passons maintenant à la pratique concrète en utilisant le réseau de test dYdX v4 pour démonstration. Le réseau de test est quasi identique au réseau principal et dispose d’un robinet automatique pour obtenir des actifs de test. Les opérations de déploiement de l’hébergeur ne seront pas détaillées ici - nous procéderons directement à la création d’un test en conditions réelles sur FMZ.
Après avoir connecté avec succès le portefeuille cryptographique à l’application dYdX v4 (ici via le portefeuille imToken), récupérez les actifs de test puis exportez la phrase mnémonique du compte dYdX v4 actuel (dérivé du portefeuille).
Configurez la phrase mnémonique sur la plateforme FMZ en utilisant une méthode par fichier local (possible aussi de la saisir directement - elle sera chiffrée avant stockage).
Placez-le dans le répertoire de l’ID de stratégie du dossier de l’hébergeur. Autre répertoire possible (préciser le chemin absolu si différent).
Dans le champ mnémonique, saisir : file:///mnemonic.txt
. Chemin physique correspondant : répertoire de l'hébergeur/logs/storage/594291
.
function main() {
// Définir l'adresse de l'indexeur de la chaîne de test
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Modifier le ChainId pour la chaîne de test
exchange.IO("chainId", "dydx-testnet-4")
// Définir l'adresse du nœud REST de test
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Test de lecture des informations du compte
Log(exchange.GetAccount())
}
Résultat de la lecture des informations du compte test :
{
"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
}
Test effectué sur le réseau principal sans basculement
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. Passer un ordre
```js
function main() {
// Changer l'adresse de l'indexeur de la chaîne de test
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Modifier le ChainId de la chaîne de test
exchange.IO("chainId", "dydx-testnet-4")
// Changer l'adresse du nœud REST de la chaîne de test
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Ordre limite, passer un ordre
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// Ordre au marché
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idSell:", idSell)
Log("idBuy:", idBuy)
Log("idMarket:", idMarket)
}
Page d’application dYdX v4 :
Test préalable de deux ordres sur le testnet, vérification de la récupération des ordres ouverts et annulation.
function main() {
// Changer l'adresse de l'indexeur de la chaîne de test
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Modifier le ChainId de la chaîne de test
exchange.IO("chainId", "dydx-testnet-4")
// Changer l'adresse du nœud REST de la chaîne de test
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() {
// Changer l'adresse de l'indexeur de la chaîne de test
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Modifier le ChainId de la chaîne de test
exchange.IO("chainId", "dydx-testnet-4")
// Changer l'adresse du nœud REST de la chaîne de test
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() {
// Changer l'adresse de l'indexeur de la chaîne de test
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Modifier le ChainId de la chaîne de test
exchange.IO("chainId", "dydx-testnet-4")
// Changer l'adresse du nœud REST de la chaîne de test
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// subAccountNumber 0 -> 128 : 20 USDC , Frais de gaz adv4tnt (jeton dydx)
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// Basculer vers le sous-compte subAccountNumber 128 et vérifier les informations du compte
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
Après avoir basculé vers le sous-compte subAccountNumber 128, les données renvoyées par GetAccount sont :
{
"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
}
On peut voir que le sous-compte subAccountNumber 128 a reçu un transfert de 20 USDC.
Récupérer le TxHash d’un ordre et tester la méthode d’appel REST via IO
L’objet d’échange dydx met en cache les TxHash, consultables via l’ID de commande. Cependant, les mappages de hachage tx des ordres en cache sont effacés après l’arrêt de la stratégie.
function main() {
// Changer l'adresse de l'indexeur de la chaîne de test
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Définir le ChainId de la chaîne de test
exchange.IO("chainId", "dydx-testnet-4")
// Configurer l'adresse du nœud REST de la chaîne de test
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)
// Pour vider la table de hachage utiliser : 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)
}
}
Résultat de la requête par TxHash :
var ret = exchange.IO(“api”, “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)
Contenu tronqué pour démonstration :
{
"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": []
},
...
Ce test est basé sur la dernière version de l’hôte géré - téléchargez la mise à jour pour supporter dYdX v4 DEX
Merci pour votre soutien et votre lecture attentive.