Avec l'essor rapide des bourses décentralisées (DEX) dans le domaine du trading de crypto-monnaie, les traders quantitatifs se sont progressivement tournés vers ces plateformes pour un trading automatisé efficace.
Cet article présentera comment pratiquer le trading quantitatif sur dYdX v4, y compris comment utiliser son API pour trader, obtenir des données de marché et gérer des comptes.
dYdX v3
Avec Ethereum, le trading génère des récompenses, qui sont des récompensesdYdX
tokens.L'échange DEX précédent du protocole dYdX v3 a été désactivé. L'adresse actuelle de l'application dYdX v4 est:
Après avoir ouvert la page de l'application, il y a un bouton pour se connecter au portefeuille dans le coin supérieur droit. Scannez le code QR pour se connecter au portefeuille.
Si vous voulez tester et vous familiariser avec l'environnement du réseau de test en premier, vous pouvez utiliser le réseau de test:
En outre, cliquez sur le bouton de connexion du portefeuille dans le coin supérieur droit, scannez le code pour connecter le portefeuille et vérifiez la signature. Une fois le portefeuille connecté avec succès, une adresse dydx v4 sera générée automatiquement. Cette adresse sera affichée dans le coin supérieur droit de la page de l'application. Cliquez dessus et un menu apparaîtra. Il existe des opérations telles que la recharge, le retrait et le transfert.
Adresse du compte dYdX v4
L'adresse du compte dYdX v4 est dérivée de l'adresse du portefeuille.dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
, qui est une adresse commençant par dydx1.
Mnémonique
Vous pouvez utiliser le bouton
Les mnémoniques peuvent être configurées directement sur la plate-forme FMZ ou enregistrées localement sur le docker.
L'environnement testnet est différent de l'environnement mainnet à certains égards.
subAccountNumber >= 128
, si le sous-compte de l'ID n'a pas de positions, les actifs seront automatiquement nettoyés sur le sous-compte dont le sous-compteNumber est 0.
Au cours des essais, il a été constaté que le réseau d'essai ne possède pas ce mécanisme (ou que les conditions de déclenchement sont différentes et qu'il n'a pas été déclenché dans le réseau d'essai).DYDX
Réseau d'essaiDv4TNT
Réseau principal:
Adresse de l' indexeur:https://indexer.dydx.trade
Identifiant de la chaîne:dydx-mainnet-1
Nœud REST:https://dydx-dao-api.polkachu.com:443
Réseau de test:
Adresse de l' indexeur:https://indexer.v4testnet.dydx.exchange
Identifiant de la chaîne:dydx-testnet-4
Nœud REST:https://dydx-testnet-api.polkachu.com
Le protocole dYdX v4 est développé sur la base de l'écosystème cosmos.
Le service d'indexation fournit des protocoles REST et Websocket.
Protocole REST L'interface de protocole REST prend en charge la requête d'informations sur le marché, les informations sur les comptes, les informations de position, les informations sur les commandes et d'autres requêtes, et a été encapsulée comme une interface API unifiée sur la plateforme FMZ.
Protocole WebSocket Sur la plateforme FMZ, vous pouvez utiliser la fonction Dial pour créer une connexion Websocket et vous abonner aux informations de marché.
Il convient de noter que l'indexeur de dydx v4 a le même problème que les échanges centralisés, c'est-à-dire que les mises à jour des données ne sont pas si opportunes. Par exemple, il se peut que vous ne puissiez pas trouver l'ordre si vous le consultez immédiatement après avoir passé une commande. Il est recommandé d'attendre quelques secondes après (Sleep(n)
) certaines opérations avant la requête.
Voici un exemple d'utilisation de la fonction Dial pour créer une connexion API Websocket et souscrire aux données du carnet de commandes:
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) + "`")
}
}
Les messages les plus couramment utilisés dans les transactions sont les messages de commande, les messages d'annulation de commande et les messages de transfert.
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
L' ordre limite:
Dans la fonction d'ordre encapsulée sur la plateforme FMZ, la valeur de orderFlags utilisée pour les ordres limites est:ORDER_FLAGS_LONG_TERM = 64 # Long-term order
Conformément aux limites du protocole dydx v4, la plus longue période de validité des commandes est utilisée, qui est de 90 jours (tous les types de commandes sur dydx v4 ont des périodes de validité).
L'ordre du marché:
Dans la fonction d'ordre encapsulée sur la plateforme FMZ, la valeur de orderFlags utilisée pour les ordres de marché est:ORDER_FLAGS_SHORT_TERM = 0 # Short-term order
, selon les recommandations du protocole dydx v4:
// Recommander fixé au prix oracle - 5% ou moins pour VENDRE, prix oracle + 5% pour acheter
Comme il ne s'agit pas d'un véritable ordre de marché, le prix de l'oracle est utilisé, plus ou moins 5% de glissement comme ordre de marché. La période de validité des ordres à court terme est également différente de celle des ordres à long terme. Les ordres à court terme utilisent la période de validité de la hauteur du bloc, qui est réglée sur le bloc actuel + 10 hauteurs de bloc selon la recommandation de dydx v4.
Paires de négociation Adresse du compte courant dydx Numéro de sous-compte (subcompteNuméro) clientId (généré aléatoirement) clobPairId (identifiant du symbole de transaction) ordreFlags goodTilData (millièmes de seconde)
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
L'identifiant de commande renvoyé par l'interface de commande de la plateforme FMZ doit être transmis.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
De nombreux sous-comptes peuvent être créés sous l'adresse dydx v4. Le sous-compte avec le sous-compte numéro 0 est le premier sous-compte créé automatiquement. Par exemple, vous pouvez passer du sous-compte numéro 0 -> 128 ou du sous-compte numéro 128 -> 0. Les transferts nécessitent des frais de gaz.
Le contenu ci-dessus explique brièvement certains détails de l'emballage. Ensuite, pratiquons l'utilisation spécifique. Ici, nous utilisons le réseau de test dYdX v4 pour la démonstration. Le réseau de test est fondamentalement le même que le réseau principal, et il y a un robinet automatique pour recevoir les actifs de test. L'opération de déploiement du docker ne sera pas répétée. Créez un test de trading en direct sur FMZ.
Après vous être connecté à l'application dYdX v4 avec succès à l'aide d'un portefeuille de crypto-monnaie (j'utilise le portefeuille imToken ici), revendiquez vos actifs de test, puis exportez le mnémonique pour votre compte dYdX v4 actuel (dérivé de votre portefeuille).
Configurer le mnémonique sur la plateforme FMZ. Ici, nous utilisons la méthode de fichier local pour le configurer (vous pouvez également le remplir directement et le configurer sur la plateforme. Le mnémonique est configuré après le chiffrement, pas en texte brut).
Bien sûr, il peut également être placé dans d'autres répertoires (le chemin spécifique doit être écrit pendant la configuration).
Remplissez la zone de modification mnémonique:file:///mnemonic.txt
, la trajectoire réelle correspondante est: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())
}
Lisez les informations du compte réseau de 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
}
Ne pas passer au réseau d'essai, testé avec le réseau 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)
}
Page de l'application dYdX v4:
Le réseau de test passe deux commandes à l'avance, teste les commandes en attente actuelles et annule les commandes.
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)
}
Informations sur le journal:
Passez au sous-compte dont le numéro de sous-compte est 128, et 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 avec le sous-compte numéro 128 a transféré 20 USDC.
Selon l'ordre, obtenir TxHash et tester la méthode d'IO appelant le nœud REST
Comment obtenir le TxHash d'une commande? L'objet d'échange dydx cache le TxHash, qui peut être interrogé à l'aide de l'identifiant de commande. Cependant, après l'arrêt de la stratégie, la carte de hachage d'ordre tx mis en cache sera effacée.
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)
}
}
Les messages interrogés par TxHash:
Le taux de changeexchange.IO"api",
GET , /cosmos/tx/v1beta1/txs/ + txHash)
Le contenu est trop long, voici donc quelques extraits 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": []
},
...
Les tests ci-dessus sont basés sur le dernier docker. Vous devez télécharger le dernier docker pour supporter dYdX v4 DEX
Merci pour votre soutien et merci de lire.