[TOC]
Dengan pesatnya perkembangan bursa terdesentralisasi (DEX) dalam ekosistem perdagangan kripto, trader kuantitatif mulai beralih ke platform-platform ini untuk melakukan trading otomatis yang efisien. dYdX sebagai salah satu platform perdagangan terdesentralisasi paling populer, menawarkan fitur trading yang kuat dengan dukungan kontrak perpetual berjangka. Versi terbaru v4 telah mengoptimalkan performa dan pengalaman pengguna, menjadikannya pilihan utama banyak trader kuantitatif.
Artikel ini akan membahas praktik trading kuantitatif di dYdX v4, termasuk penggunaan API untuk trading, pengambilan data pasar, dan manajemen akun.
dYdX v3
, aktivitas trading akan menghasilkan reward berupa token dYdX
.Protokol dYdX v3 DEX sebelumnya telah dihentikan. Alamat App dYdX v4 saat ini:
Setelah membuka halaman App, tombol koneksi dompet terdapat di pojok kanan atas. Lakukan pemindaian QR code untuk menghubungkan dompet.
Untuk pengujian di lingkungan testnet, gunakan:
Proses koneksi dompet sama: pindai QR code dan lakukan verifikasi tanda tangan. Setelah berhasil terhubung, alamat dydx v4 akan otomatis digenerate dan ditampilkan di pojok kanan atas. Menu pop-up akan muncul saat mengklik alamat tersebut, berisi opsi deposit, penarikan, dan transfer. Perbedaan utama antara mainnet (produksi) dan testnet adalah: di testnet, fitur deposit akan otomatis mengisi 300 USDC melalui faucet untuk keperluan testing. Untuk trading real, diperlukan deposit USDC asli yang mendukung berbagai aset dan blockchain.
Alamat Akun dYdX v4
Alamat akun dYdX v4 diturunkan dari alamat dompet, dengan format dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
(diawali dydx1). Alamat ini dapat diverifikasi melalui blockchain explorers.
Mnemonic Frasa mnemonik dapat di-export melalui menu “Export Passphrase” di pojok kanan atas. Konfigurasi mnemonic ini diperlukan saat menambahkan exchange di Platform FMZ.
Mnemonic dapat dikonfigurasi langsung di Platform FMZ atau disimpan secara lokal. Pada implementasi praktis, objek exchange dydx v4 akan membaca konten file yang berisi mnemonic.
Lingkungan testnet memiliki beberapa perbedaan dengan mainnet:
Transfer aset sub-akun.
Mainnet memiliki mekanisme pembersihan sub-akun: saat subAccountNumber >= 128
dan tidak ada posisi terbuka, aset akan otomatis ditransfer ke sub-akun dengan ID 0. Mekanisme ini tidak ditemukan di testnet (atau memiliki kondisi trigger berbeda).
Penamaan token.
Token native dydx memiliki nama berbeda: DYDX
(mainnet) vs Dv4TNT
(testnet)
Konfigurasi alamat node. Contoh konfigurasi:
Mainnet:
Alamat indexer: https://indexer.dydx.trade
Chain ID: dydx-mainnet-1
REST node: https://dydx-dao-api.polkachu.com:443
Testnet:
Alamat indexer: https://indexer.v4testnet.dydx.exchange
Chain ID: dydx-testnet-4
REST node: https://dydx-testnet-api.polkachu.com
Protokol dYdX v4 dikembangkan berbasis ekosistem Cosmos. Sistem DEX dYdX v4 terdiri dari dua komponen utama:
Layanan indexer menyediakan protokol REST dan Websocket.
Protokol REST Menyediakan antarmuka untuk query informasi pasar, akun, posisi, dan pesanan. Telah dienkapsulasi sebagai API standar di Platform FMZ.
Protokol WebSocket Dapat menggunakan fungsi Dial di Platform FMZ untuk membuat koneksi Websocket dan berlangganan data pasar.
Perlu diperhatikan bahwa update data di indexer dydx v4 tidak selalu real-time. Disarankan untuk menambahkan delay (Sleep(n)
) setelah operasi tertentu sebelum melakukan query.
Contoh implementasi koneksi Websocket menggunakan fungsi Dial untuk berlangganan data orderbook:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// Berlangganan
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// Buku pesanan
var orderBook = null
// Perbarui buku pesanan
var updateOrderbook = function(orderbook, update) {
// Perbarui bids
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// Hapus pesanan beli dengan harga price
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// Update atau tambah pesanan beli
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// Urutkan berdasarkan harga secara descending (menurun)
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) {
// Hapus pesanan jual dengan harga price
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// Update atau tambah pesanan jual
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// Urutkan berdasarkan harga secara ascending (meningkat)
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("Gagal membuat wsThread")
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)
}
// Message listener
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// Real: wss://indexer.dydx.trade/v4/ws
// Simulasi: 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) + "`")
}
}
Pesan yang paling umum digunakan dalam trading adalah pesan order, pembatalan order, dan transfer dana.
Ringkasan Pesan Order
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
Limit Order:
Fungsi pemesanan yang dienkapsulasi di platform FMZ menggunakan nilai orderFlags
: ORDER_FLAGS_LONG_TERM = 64 # Pesanan jangka panjang
. Sesuai batasan protokol dydx v4, digunakan masa berlaku maksimum yaitu 90 hari (semua tipe order di dydx v4 memiliki masa berlaku).
Market Order:
Fungsi pemesanan untuk market order menggunakan nilai orderFlags
: ORDER_FLAGS_SHORT_TERM = 0 # Pesanan jangka pendek
. Berdasarkan rekomendasi dydx v4:
// Disarankan diatur ke harga oracle - 5% atau lebih rendah untuk SELL, harga oracle + 5% untuk BUY
Karena bukan market order sebenarnya, digunakan harga oracle dengan penyesuaian slippage 5%. Masa berlaku order jangka pendek juga berbeda, menggunakan blok tinggi sebagai batas waktu sesuai rekomendasi dydx v4: blok saat ini + 10 blok.
Order ID: Karena proses pemesanan dilakukan langsung di chain, tidak ada order ID yang dihasilkan oleh indexer. Untuk memastikan keunikan dan akurasi pencarian, order ID dikonstruksi dari komponen berikut (dipisahkan koma):
subaccountNumber
)clientId
(acak)clobPairId
(ID instrumen)orderFlags
goodTilData
(milidetik)Ringkasan Pesan Pembatalan Order
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Diperlukan ID order yang dikembalikan oleh antarmuka pemesanan FMZ.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Subaccount dengan subAccountNumber
≥128 di dydx v4 digunakan untuk trading isolated margin, memerlukan minimal 20 USDC. Contoh transfer: dari subaccount 0 → 128 atau 128 → 0. Transfer memerlukan Gas Fee yang dapat dibayar dengan USDC atau token dydx.
Konten di atas menjelaskan secara singkat beberapa detail enkapsulasi. Selanjutnya kita akan melakukan praktik penggunaan konkret menggunakan testnet dYdX v4 untuk demonstrasi. Testnet ini memiliki konfigurasi yang hampir identik dengan mainnet dan dilengkapi faucet otomatis untuk mendapatkan aset uji. Proses deployment operator tidak akan diulangi lagi di sini, kita akan langsung membuat tes langsung di FMZ.
Setelah berhasil menghubungkan dompet kripto ke aplikasi dYdX v4 (dalam kasus ini menggunakan dompet imToken), klaim aset uji lalu ekspor mnemonic akun dYdX v4 saat ini (yang diturunkan dari dompet).
Konfigurasikan mnemonic di platform FMZ menggunakan metode file lokal (bisa juga diisi langsung, mnemonik dienkripsi sebelum dikonfigurasi, bukan dalam bentuk teks biasa).
Letakkan di direktori ID akun real-time dalam folder operator, atau di direktori lain (pastikan menulis path lengkap saat konfigurasi).
Isi kolom mnemonic dengan: file:///mnemonic.txt
. Path aktual yang sesuai adalah: Direktori operator/logs/storage/594291
.
function main() {
// Alamat indeksor untuk jaringan tes
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// ChainId untuk jaringan tes
exchange.IO("chainId", "dydx-testnet-4")
// Alamat node REST untuk jaringan tes
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Tes pembacaan informasi akun
Log(exchange.GetAccount())
}
Hasil pembacaan informasi akun 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
}
Tidak beralih ke testnet, menggunakan mainnet untuk pengujian
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. Pemesanan
```js
function main() {
// Mengubah alamat indexer untuk testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Mengubah ChainId untuk testnet
exchange.IO("chainId", "dydx-testnet-4")
// Mengubah alamat node REST untuk testnet
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// Order limit, pasang order
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// Order pasar
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idSell:", idSell)
Log("idBuy:", idBuy)
Log("idMarket:", idMarket)
}
Halaman Aplikasi dYdX v4:
Mempersiapkan dua order di testnet untuk menguji fungsi pengambilan order dan pembatalan order.
function main() {
// Mengubah alamat indexer untuk testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Mengubah ChainId untuk testnet
exchange.IO("chainId", "dydx-testnet-4")
// Mengubah alamat node REST untuk 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: "Uji GetOrders", cols: ["Id", "Harga", "Jumlah", "JumlahTereksekusi", "HargaRata", "Status", "Tipe", "Offset", "JenisKontrak"], 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() {
// Mengubah alamat indexer untuk testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Mengubah ChainId untuk testnet
exchange.IO("chainId", "dydx-testnet-4")
// Mengubah alamat node REST untuk 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: "Uji GetPosition/GetPositions", cols: ["Simbol", "Jumlah", "Harga", "JumlahBeku", "Tipe", "Profit", "Margin", "JenisKontrak", "TingkatMargin"], 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() {
// Mengubah alamat indexer untuk testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Mengubah ChainId untuk testnet
exchange.IO("chainId", "dydx-testnet-4")
// Mengubah alamat node REST untuk testnet
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// SubAccountNumber 0 -> 128: Transfer 20 USDC, Biaya Gas menggunakan adv4tnt (token dydx)
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// Beralih ke subakun subAccountNumber 128, membaca informasi akun untuk pemeriksaan
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}
Hasil GetAccount setelah beralih ke subakun dengan subAccountNumber 128:
{
"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
}
Dapat dilihat pada subakun dengan subAccountNumber 128, telah ditransfer 20 USDC.
Mendapatkan TxHash berdasarkan pesanan, menguji metode pemanggilan node REST melalui IO
Cara mendapatkan TxHash pesanan: objek bursa dydx akan menyimpan cache TxHash, dapat dicari menggunakan ID pesanan. Namun mapping hash tx yang di-cache akan direset setelah strategi dihentikan.
function main() {
// Mengubah alamat indexer ke testnet
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Mengatur ChainId untuk testnet
exchange.IO("chainId", "dydx-testnet-4")
// Mengatur alamat node REST 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)
// Untuk mengosongkan tabel pemetaan, gunakan: 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)
}
}
Pesan yang diperoleh melalui query TxHash:
var ret = exchange.IO(“api”, “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)
Konten terlalu panjang, ditampilkan sebagian sebagai demonstrasi:
{
"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": []
},
...
Pengujian di atas menggunakan versi terbaru hosted, perlu mengunduh hosted terbaru untuk mendukung dYdX v4 DEX
Terima kasih atas dukungannya. Terima kasih telah membaca.