[TOC]

С быстрым развитием децентрализованных бирж (DEX) в сфере торговли криптовалютами количественные трейдеры постепенно начали обращаться к этим платформам для эффективной автоматизированной торговли. Как одна из самых популярных децентрализованных торговых платформ, dYdX предоставляет мощные торговые функции и поддерживает торговлю фьючерсными бессрочными контрактами. Ее последняя версия v4 оптимизирует производительность и пользовательский опыт, что делает ее первым выбором для многих количественных трейдеров.
В этой статье вы узнаете, как практиковать количественную торговлю на dYdX v4, в том числе как использовать его API для торговли, получения рыночных данных и управления счетами.

dYdX v3Аналогично транзакции генерируют вознаграждения, вознагражденияdYdXТокены.
Предыдущий обмен DEX по протоколу dYdX v3 был офлайн. Текущий адрес приложения dYdX v4:
После открытия страницы приложения в правом верхнем углу есть кнопка для подключения к кошельку. Отсканируйте QR-код для подключения к кошельку.
Если вы хотите сначала протестировать и ознакомиться с тестовой сетевой средой, вы можете использовать тестовую сеть:
Также нажмите кнопку подключения кошелька в правом верхнем углу, отсканируйте QR-код для подключения к кошельку и проверьте подпись. После успешного подключения кошелька будет автоматически сгенерирован адрес dydx v4. Этот адрес будет отображаться в правом верхнем углу страницы приложения. После нажатия на него появится меню. К ним относятся такие операции, как пополнение счета, снятие средств и перевод. Одно из различий между основной сетью dYdX (производственной средой) и тестовой сетью заключается в том, что при нажатии кнопки пополнения в тестовой сети 300 активов USDC будут автоматически зачислены на кран для тестирования. Если вы хотите делать реальные транзакции на dYdX, вам нужно внести активы USDC. Пополнение также очень удобно и совместимо с несколькими активами и цепочками.
Адрес учетной записи dYdX v4
Адрес аккаунта dYdX v4 выводится из адреса кошелька. Адрес аккаунта dYdX v4 выглядит следующим образом:dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx, — это адрес, начинающийся с dydx1. Этот адрес можно запросить в обозревателях блокчейнов.
Мнемоника Вы можете экспортировать мнемонику текущей учетной записи адреса dYdX, нажав кнопку «Экспорт пароля» в меню в правом верхнем углу. При добавлении биржи на платформу FMZ необходимо настроить данную мнемонику.
Мнемоники можно настроить непосредственно на платформе FMZ или сохранить локально на кастодиане. При использовании объекта обмена dydx v4 будет считываться содержимое файла, в котором записаны мнемоники, что будет продемонстрировано в практической части этой статьи.
Среда testnet отличается от среды mainnet в некоторых аспектах. Вот несколько простых отличий.
Перевод активов на субсчета.
В основной сети реализован механизм очистки субаккаунтов.subAccountNumber >= 128Если на субсчете с этим идентификатором нет позиций, активы будут автоматически переведены на субсчет с subAccountNumber 0.
В ходе тестирования было обнаружено, что в тестовой сети такой механизм отсутствует (или условия срабатывания были другими и в тестовой сети он не сработал).
Некоторые токен-имена.
Собственный токен dydx называется по-другому: MainnetDYDX, тестовая сетьDv4TNT
Конфигурация адреса, например идентификатор цепочки, адрес узла, адрес индексатора и т. д. Существует множество узлов и конфигураций, вот одна из них:
Основная сеть:
Адрес индексатора:https://indexer.dydx.trade
Идентификатор цепочки:dydx-mainnet-1
Узел REST:https://dydx-dao-api.polkachu.com:443
Тестовая сеть:
Адрес индексатора:https://indexer.v4testnet.dydx.exchange
Идентификатор цепочки:dydx-testnet-4
Узел REST:https://dydx-testnet-api.polkachu.com
Протокол dYdX v4 разработан на основе экосистемы космоса. Контент транзакций системы DEX dYdX v4 в основном состоит из двух частей:
Служба индексатора предоставляет протоколы REST и Websocket.
REST-протокол Интерфейс протокола REST поддерживает запросы рыночной информации, информацию о счетах, информацию о позициях, информацию о заказах и т. д. и был инкапсулирован в виде унифицированного API-интерфейса на платформе FMZ.
Протокол WebSocket На платформе FMZ вы можете использовать функцию Dial для создания соединения Websocket и подписки на рыночную информацию.
Следует отметить, что индексатор dydx v4 имеет ту же проблему, что и централизованная биржа, то есть обновления данных не столь своевременны. Например, иногда заказ может не быть найден при запросе сразу после размещения заказа. Рекомендуется после определенных операций (Sleep(n)) Подождите несколько секунд, прежде чем повторить запрос.
Ниже приведен пример использования функции Dial для создания соединения Websocket API и подписки на данные книги заказов:
function dYdXIndexerWSconnManager(streamingPoint) {
var self = {}
self.base = streamingPoint
self.wsThread = null
// 订阅
self.CreateWsThread = function (msgSubscribe) {
self.wsThread = threading.Thread(function (streamingPoint, msgSubscribe) {
// 订单薄
var orderBook = null
// 更新订单薄
var updateOrderbook = function(orderbook, update) {
// 更新 bids
if (update.bids) {
update.bids.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// 删除价格为 price 的买单
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
} else {
// 更新或新增买单
orderbook.bids = orderbook.bids.filter(bid => parseFloat(bid.price) !== priceFloat)
orderbook.bids.push({price: price, size: size})
// 按价格降序排序
orderbook.bids.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
}
})
}
// 更新 asks
if (update.asks) {
update.asks.forEach(([price, size]) => {
const priceFloat = parseFloat(price)
const sizeFloat = parseFloat(size)
if (sizeFloat === 0) {
// 删除价格为 price 的卖单
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
} else {
// 更新或新增卖单
orderbook.asks = orderbook.asks.filter(ask => parseFloat(ask.price) !== priceFloat)
orderbook.asks.push({price: price, size: size})
// 按价格升序排序
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)
}
// 监听
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) + "`")
}
}
Наиболее часто используемые сообщения в транзакциях — это сообщения о заказах, сообщения об отмене заказов и сообщения о переводах.
{
"@type": "/dydxprotocol.clob.MsgPlaceOrder",
"order": {
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": xxx,
"orderFlags": 64,
"clobPairId": 1
},
"side": "SIDE_BUY",
"quantums": "2000000",
"subticks": "3500000000",
"goodTilBlockTime": 1742295981
}
}
Лимитный ордер:
В функции ордера, инкапсулированной на платформе FMZ, значение orderFlags, используемое для лимитных ордеров, равно:ORDER_FLAGS_LONG_TERM = 64 # 长期订单В соответствии с ограничениями протокола DYDX v4 используется самый длительный срок действия заказа, который составляет 90 дней (все типы заказов на DYDX v4 имеют срок действия).
Рыночный ордер:
В функции ордера, инкапсулированной на платформе FMZ, значение orderFlags, используемое рыночным ордером, равно:ORDER_FLAGS_SHORT_TERM = 0 # 短期订单, согласно рекомендациям протокола DYDX v4:
// Recommend set to oracle price - 5% or lower for SELL, oracle price + 5% for BUY
Поскольку это не настоящий рыночный ордер, в качестве рыночного ордера используется цена оракула плюс-минус 5% проскальзывания. Настройка срока действия краткосрочных ордеров также отличается от настройки долгосрочных ордеров. Краткосрочные ордера используют срок действия высоты блока. Согласно рекомендации dydx v4, он устанавливается на текущий блок + 10 высот блока до истечения срока его действия.
Идентификатор заказа: Поскольку операция заказа выполняется непосредственно в цепочке, после трансляции сообщения индексатором не будет сгенерирован идентификатор заказа, а заказ индексатора не может быть использован в качестве возвращаемого значения функции заказа платформы. Для обеспечения уникальности идентификатора заказа и точности запроса заказа возвращается заказ индексатора. Идентификатор заказа состоит из следующей информации (через запятую):
Краткое содержание сообщения об отмене заказа
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Необходимо передать идентификатор заказа, возвращаемый интерфейсом заказа платформы FMZ.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
Под текущим адресом dydx v4 можно создать множество субсчетов. Субсчет с subAccountNumber 0 является первым автоматически созданным субсчетом. Идентификатор субсчета с subAccountNumber больше или равен 128 используется для изолированной позиционной торговли, для чего требуется не менее 20 активов USDC. Например, вы можете перейти от subAccountNumber 0 к 128 или от subAccountNumber 128 к 0. Перевод требует расходования платы за газ. Плата за газ может использовать токены USDC и dydx.
Вышеуказанный контент кратко объясняет некоторые детали упаковки. Далее, давайте попрактикуемся в конкретном использовании. Здесь мы используем тестовую сеть dYdX v4 для демонстрации. Тестовая сеть в основном такая же, как и основная сеть, и есть автоматический кран для получения тестовых активов . Хранитель развертывает Я не буду вдаваться в подробности об операции, а создам настоящий тест на FMZ.
После успешного подключения к приложению dYdX v4 с помощью криптовалютного кошелька (здесь я использую кошелек imToken) запросите свои тестовые активы, а затем экспортируйте мнемонику для вашей текущей учетной записи dYdX v4 (полученную из вашего кошелька).

Настройте мнемонику на платформе FMZ. Здесь мы используем метод локального файла для ее настройки (вы также можете заполнить его напрямую и настроить на платформе. Мнемоника настраивается после шифрования, а не в виде простого текста).

Поместите его в реальный каталог папки ID диска в каталоге custodian. Конечно, его можно разместить и в других каталогах (конкретный путь необходимо указать во время настройки).
Заполните мнемоническое поле редактирования:file:///mnemonic.txt, соответствующий фактический путь:托管者所在目录/logs/storage/594291。

function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// 读取账户信息测试
Log(exchange.GetAccount())
}
Прочтите информацию об учетной записи тестовой сети:
{
"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
}
Не переключался на тестовую сеть, тестировал с основной сетью
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() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// 限价单,挂单
var idSell = exchange.CreateOrder("ETH_USD.swap", "sell", 4000, 0.002)
var idBuy = exchange.CreateOrder("ETH_USD.swap", "buy", 3000, 0.003)
// 市价单
var idMarket = exchange.CreateOrder("ETH_USD.swap", "buy", -1, 0.01)
Log("idSell:", idSell)
Log("idBuy:", idBuy)
Log("idMarket:", idMarket)
}

Страница приложения dYdX v4:

Тестовая сеть размещает два ордера заранее, тестирует получение текущих отложенных ордеров и отменяет ордера.
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
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() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
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() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
exchange.IO("restApiBase", "https://dydx-testnet-api.polkachu.com")
// subAccountNumber 0 -> 128 : 20 USDC , Gas Fee 为 adv4tnt 即 dydx token
var ret = exchange.IO("transferUSDCToSubaccount", 0, 128, "adv4tnt", 20)
Log("ret:", ret)
// 切换到子账号subAccountNumber 128 ,读取账户信息检查
exchange.IO("subAccountNumber", 128)
var account = exchange.GetAccount()
Log("account:", account)
}

Переключитесь на субсчет, subAccountNumber которого равен 128, и 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
}
Видно, что на субсчет с subAccountNumber 128 было переведено 20 USDC.
Согласно заказу, получите TxHash и протестируйте метод IO, вызывающий REST-узел.
Как получить TxHash заказа? Объект обмена dydx кэширует TxHash, который можно запросить, используя идентификатор заказа. Однако после остановки стратегии кэшированная карта хэшей транзакций ордеров будет очищена.
function main() {
// 切换测试链的索引器地址
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// 切换测试链的ChainId
exchange.IO("chainId", "dydx-testnet-4")
// 切换测试链的REST节点地址
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)
// 清空映射表可以使用: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)
}
}

Сообщения, запрошенные через TxHash:
var ret = exchange.IO(“api”, “GET”, “/cosmos/tx/v1beta1/txs/” + txHash)
Содержание слишком длинное, поэтому вот несколько отрывков для демонстрации:
{
"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": []
},
...
Вышеуказанные тесты основаны на последнем custodian. Вам необходимо загрузить последний custodian для поддержки dYdX v4 DEX
Спасибо за вашу поддержку и спасибо за чтение.