[TOC]
Mit dem rasanten Aufstieg dezentraler Börsen (DEX) im Kryptohandel wenden sich quantitative Händler zunehmend diesen Plattformen für effizientes automatisiertes Trading zu. dYdX, als eine der beliebtesten dezentralen Handelsplattformen, bietet leistungsstarke Handelsfunktionen mit Unterstützung für Perpetual Futures. Die neueste Version v4 optimiert Performance und Benutzererfahrung, wodurch sie zur ersten Wahl für viele Quantitative Trading-Strategien wird.
Dieser Artikel beschreibt die praktische Umsetzung quantitativen Handels auf dYdX v4, inklusive API-Nutzung für Orderausführung, Marktdatenabfrage und Kontomanagement.
dYdX v3
generiert Trading Belohnungen in dYdX
-Token.Die frühere dYdX v3 Protocol DEX wurde eingestellt. Die aktuelle dYdX v4 App-Adresse lautet:
Nach dem Öffnen der App-Oberfläche findet sich oben rechts ein Wallet-Verbindungsbutton für QR-Code-Linking.
Für Testnetz-Experimente verwenden Sie:
Nach erfolgreicher Wallet-Verbindung via Signaturvalidierung generiert das System automatisch eine dYdX v4-Adresse (sichtbar oben rechts). Das Kontextmenü ermöglicht Einzahlungen, Abhebungen und Transfers. Ein Hauptunterschied zwischen Mainnet und Testnet: Im Testnetz erhalten Sie per Faucet automatisch 300 USDC Testguthaben. Für reales Trading sind USDC-Einzahlungen erforderlich, die über mehrere Chains unterstützt werden.
dYdX v4 Kontoadresse
Abgeleitet von der Wallet-Adresse, zeigt das Format dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
(beginnt mit dydx1). Diese Adresse ist über Blockchain-Explorer einsehbar.
Mnemonic-Phrase
Über das Kontextmenü (“Exportiere Passphrase”) lässt sich die Mnemonic-Phrase des aktuellen Kontos exportieren. Bei der Exchange-Konfiguration auf FMZ ist diese Phrase erforderlich.
Die Mnemonic-Phrase kann direkt auf FMZ hinterlegt oder lokal gespeichert werden. Im Praxisteil demonstrieren wir die Nutzung über Dateieinbindung.
Testnetz und Mainnet zeigen folgende relevante Unterschiede:
Unterkonto-Transfers
Mainnet implementiert automatische Guthabenbereinigung bei subAccountNumber >= 128
(leere Positionen werden zu Subkonto 0 transferiert). Im Testnetz fehlt dieser Mechanismus (oder hat abweichende Triggerbedingungen).
Token-Nomenklatur
Native Token-Bezeichnungen differieren: Mainnet DYDX
vs. Testnet Dv4TNT
Netzwerkkonfiguration
Chain-IDs und Node-Adressen variieren:
Mainnet:
Indexer: https://indexer.dydx.trade
Chain-ID: dydx-mainnet-1
REST-Node: https://dydx-dao-api.polkachu.com:443
Testnet:
Indexer: https://indexer.v4testnet.dydx.exchange
Chain-ID: dydx-testnet-4
REST-Node: https://dydx-testnet-api.polkachu.com
Das dYdX v4 Protokoll basiert auf dem Cosmos-Ökosystem. Das Handelssystem besteht aus zwei Kernkomponenten:
Der Indexer-Service bietet REST- und WebSocket-Schnittstellen.
REST-Protokoll
Ermöglicht Abfragen von Marktdaten, Kontoständen, Positionen und Orderbüchern. FMZ bietet hierfür standardisierte API-Wrapper.
WebSocket-Protokoll
Nutzbar via Dial
-Funktion für Echtzeitdaten (z.B. Orderbuch-Updates).
Achtung: Wie bei zentralisierten Börsen können Indexer-Datenverzögerungen auftreten. Nach Orderplatzierungen empfiehlt sich eine Verzögerung (Sleep(n)
) vor Folgeabfragen.
Beispiel für WebSocket-Nutzung mit Orderbuch-Subscription:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// Subscription
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// Orderbuch-Objekt
var orderBook = null
// Orderbuch-Update-Logik
var updateOrderbook = function(orderbook, update) {
// Bids aktualisieren
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Lösche Buy-Order bei Preisstufe
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// Kauforder aktualisieren oder hinzufügen
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// Nach Preis absteigend sortieren
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// Asks aktualisieren
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Verkaufsorder mit Preis price entfernen
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// Verkaufsorder aktualisieren oder hinzufügen
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// Nach Preis aufsteigend sortieren
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("Erstellen des WebSocket-Threads fehlgeschlagen.")
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)
}
// Überwachung
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// Real: wss://indexer.dydx.trade/v4/ws
// Testumgebung: wss://indexer.v4testnet.dydx.exchange/v4/ws
var symbol = "ETH-USD"
var manager = dYdXIndexerWSconnManager("wss://indexer.dydx.trade/v4/ws")
```javascript
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) + "`")
}
}
Die am häufigsten verwendeten Nachrichten im Handel sind Auftragsnachrichten, Stornierungsnachrichten und Überweisungsnachrichten.
Zusammenfassung der Auftragsnachrichten
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
orderFlags
-Wert:ORDER_FLAGS_LONG_TERM = 64 # Langzeit-Order
.
Gemäß den Einschränkungen des dydx v4-Protokolls wird die maximale Gültigkeitsdauer von 90 Tagen genutzt (alle Ordertypen auf dydx v4 haben eine Verfallszeit).
orderFlags
-Wert verwendet:ORDER_FLAGS_SHORT_TERM = 0 # Kurzzeit-Order
.
Entsprechend der dydx v4-Empfehlung:
// Empfohlen wird, den Oracle-Preis minus 5 % oder niedriger für SELL und Oracle-Preis plus 5 % für BUY festzulegen
Da es sich nicht um echte Market-Orders handelt, wird der Oracle-Preis mit einem Aufschlag/Abzug von 5 % als Ausführungspreis verwendet. Die Gültigkeitsdauer von Kurzzeit-Orders basiert auf Blockhöhen und wird gemäß Protokollvorgabe auf aktuelle Blockhöhe + 10 Blöcke gesetzt.
Order-ID:
Da die Orderausführung direkt on-chain erfolgt, gibt es keine Indexer-generierte Order-ID. Um Eindeutigkeit und Abfragegenauigkeit zu gewährleisten, setzt sich die Order-ID aus folgenden Komponenten zusammen (englische Kommatrennung):
subaccountNumber
)clientId
clobPairId
)orderFlags
goodTilData
(Millisekunden)Zusammenfassung der Stornierungsnachrichten
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Erfordert die Übergabe der über FMZ-Schnittstelle zurückgegebenen Order-ID.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Aktuell können unter einer dydx v4-Adresse viele Subkonten erstellt werden. Dabei ist subAccountNumber
0 das erste automatisch erstellte Subkonto. Subkonten-IDs mit einem subAccountNumber
≥ 128 werden für isolierte Handelsinstrumente verwendet und erfordern mindestens 20 USDC an Vermögen.
Beispielsweise können Transfers von subAccountNumber 0 → 128
oder 128 → 0
durchgeführt werden. Überweisungen verbrauchen Gas-Gebühren, die in USDC oder dydx-Token bezahlt werden können.
Die vorherigen Abschnitte haben grundlegende Implementierungsdetails erläutert. Nun wollen wir die praktische Anwendung untersuchen. Hier verwenden wir das dYdX v4 Testnetz für die Demonstration. Das Testnetz entspricht weitgehend dem Hauptnetz und verfügt über einen automatischen Faucet für Testguthaben. Die Deployment-Prozedur für Host-Nodes wird nicht erneut beschrieben, da wir direkt Live-Tests auf FMZ durchführen.
Nach erfolgreicher Verbindung der Krypto-Wallet mit der dYdX v4 App (hier: imToken Wallet) und Bezug der Testassets, exportieren Sie den Mnemonic des aktuellen dYdX v4 Kontos (wallet-abgeleitet).
Konfigurieren Sie den Mnemonic auf der FMZ-Plattform mittels lokaler Datei (alternativ direkte Eingabe - Mnemonics werden verschlüsselt gespeichert, nicht im Klartext).
Speichern Sie die Datei im Host-Node-Verzeichnis unter der Live-Test-ID (alternativ beliebiger Pfad mit entsprechender Pfadangabe).
Mnemonic-Feld eintragen: file:///mnemonic.txt
(entspricht physischem Pfad: Host-Node-Verzeichnis/logs/storage/594291
).
function main() {
// Indexer-Adresse der Testnetzkette ändern
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// ChainId des Testnetzes setzen
exchange.IO("chainId", "dydx-testnet-4")
// REST-API-Endpoint des Testnetzes konfigurieren
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Test des Kontoinformationsabrufs
Log(exchange.GetAccount())
}
Ergebnis des Kontoinformationsabrufs:
{
"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
}
Nicht zum Testnetz gewechselt, Test mit dem Hauptnetz durchgeführt
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. Orderaufgabe
```js
function main() {
// Wechseln Sie die Indexer-Adresse der Testnetzkette
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Wechseln Sie die ChainId der Testnetzkette
exchange.IO("chainId", "dydx-testnet-4")
// Wechseln Sie die REST-Node-Adresse der Testnetzkette
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Limit-Order, Order platzieren
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)
}
dYdX v4 App-Oberfläche:
Im Testnetz zwei Orders im Voraus platzieren, um das Abrufen aktueller Orders und das Stornieren von Orders zu testen.
function main() {
// Wechseln Sie die Indexer-Adresse der Testnetzkette
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Wechseln Sie die ChainId der Testnetzkette
exchange.IO("chainId", "dydx-testnet-4")
// Wechseln Sie die REST-Node-Adresse der Testnetzkette
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() {
// Wechseln Sie die Indexer-Adresse der Testnetzkette
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Wechseln Sie die ChainId der Testnetzkette
exchange.IO("chainId", "dydx-testnet-4")
// Wechseln Sie die REST-Node-Adresse der Testnetzkette
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() {
// Wechseln Sie die Indexer-Adresse der Testnetzkette
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Wechseln Sie die ChainId der Testnetzkette
exchange.IO("chainId", "dydx-testnet-4")
// Wechseln Sie die REST-Node-Adresse der Testnetzkette
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
exchange.IO(“restApiBase”, “https://dydx-testnet-api.polkachu.com”)
// SubAccountNumber 0 -> 128: 20 USDC, Gas Fee entspricht adv4tnt (dydx-Token)
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// Wechsel zu Subaccount 128 zur Kontostandsprüfung
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
![Quantitative Praktiken für DEX-Börsen (1) – Leitfaden zu dYdX v4](/upload/asset/175bd506389b9e8966ca.png)
Auszug aus den zurückgegebenen Daten nach Wechsel zu Subaccount 128:
```JSON
{
"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
}
Der Subaccount mit der Nummer 128 zeigt erfolgreich transferierte 20 USDC an.
Abfrage von TxHash über Order-ID mittels REST-Node
TxHash-Ermittlung: Die dydx-Exchange-Instanz speichert TxHash temporär. Die Abfrage erfolgt über Order-ID. Hinweis: Bei Strategiestopp wird der Cache geleert.
function main() {
// Wechseln Sie die Indexer-Adresse der Testchain
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// ChainId der Testchain setzen
exchange.IO("chainId", "dydx-testnet-4")
// REST-Node-Adresse der Testchain setzen
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)
// Zum Leeren der Mapping-Tabelle kann verwendet werden: 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)
}
}
Über TxHash abgefragte Nachricht:
var ret = exchange.IO(“api”, “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)
Inhalt zu lang, Auszug zur Demonstration:
{
"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": []
},
...
Dieser Test basiert auf dem aktuellsten Custodian. Der neueste Custodian muss heruntergeladen werden, um dYdX v4 DEX zu unterstützen
Vielen Dank für Ihre Unterstützung und für das Lesen dieses Artikels.