[TOC]
С быстрым развитием децентрализованных бирж (DEX) в сфере криптовалютных торговль, количественные трейдеры постепенно переходят на эти платформы для эффективной автоматизированной торговли. dYdX, являясь одной из самых популярных децентрализованных торговых платформ, предоставляет мощные торговые функции, поддерживая торговлю фьючерсами и перпетуальными контрактами. Её последняя версия v4 оптимизировала производительность и пользовательский опыт, став выбором многих алгоритмических трейдеров.
В этой статье будет рассмотрена практика количественной торговли на dYdX v4, включая использование API для совершения сделок, получения рыночных данных и управления аккаунтами.
dYdX v3
, торговля генерирует вознаграждения в токенах dYdX
.Предыдущая версия dYdX v3 DEX была отключена. Текущий адрес dYdX v4:
После открытия страницы приложения, в правом верхнем углу находится кнопка подключения кошелька. Для тестирования в тестовой сети можно использовать:
Аналогично, подключите кошелёк через сканирование QR-кода и подпишите запрос. После успешного подключения автоматически генерируется адрес dYdX v4, который отображается в правом верхнем углу. В выпадающем меню доступны операции пополнения, вывода и трансферов. Основное отличие тестовой сети от основной: при пополнении в тестовой сети автоматически зачисляется 300 USDC для тестирования. Для реальной торговли требуется внесение USDC через поддерживаемые сети.
Адрес аккаунта dYdX v4
Адрес формируется на основе адреса кошелька и имеет вид: dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx
(начинается с dydx1). Его можно проверить в блокчейн-эксплорерах.
Мнемоническая фраза Через меню «Экспорт пароля» можно получить мнемоническую фразу текущего аккаунта. Для добавления биржи на платформе FMZ требуется настройка этой фразы.
Мнемонику можно напрямую настроить на FMZ или хранить локально. В практической части статьи будет продемонстрирована работа с объектом биржи dydx v4.
Тестовая сеть имеет некоторые отличия от основной:
Трансферы между субаккаунтами.
В основной сети действует механизм автоматического перемещения активов при subAccountNumber >= 128
, если нет открытых позиций.
В тестовой сети этот механизм отсутствует (или условия срабатывания отличаются).
Названия токенов.
Нативный токен: DYDX
(основная сеть), Dv4TNT
(тестовая).
Конфигурация адресов (chain ID, ноды и т.д.):
Основная сеть:
Индексатор: https://indexer.dydx.trade
Chain ID: dydx-mainnet-1
REST нода: https://dydx-dao-api.polkachu.com:443
Тестовая сеть:
Индексатор: https://indexer.v4testnet.dydx.exchange
Chain ID: dydx-testnet-4
REST нода: https://dydx-testnet-api.polkachu.com
dYdX v4 разработан на экосистеме Cosmos. Основные компоненты:
Индексатор предоставляет REST и WebSocket API.
REST API Обеспечивает доступ к рыночным данным, информации об аккаунтах, позициях и ордерах. На платформе FMZ реализована унифицированная обёртка.
WebSocket API Для подключения через WebSocket на FMZ используется функция Dial. Пример подписки на стакан:
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))
}
})
}
// Обновление асков
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("Не удалось создать поток 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)
}
// Обработчик сообщений
self.Peek = function () {
return self.wsThread.peekMessage()
}
return self
}
function main() {
// Реальное подключение: wss://indexer.dydx.trade/v4/ws
// Тестовое подключение: 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 имеют срок действия).
Рыночный ордер:
Для рыночных ордеров используется значение orderFlags: ORDER_FLAGS_SHORT_TERM = 0 # Краткосрочный ордер
. В соответствии с рекомендациями dydx v4:
// Рекомендуется устанавливать цену: oracle price -5% для SELL, oracle price +5% для BUY
Поскольку это не истинный рыночный ордер, используется цена оракула с добавлением/вычитанием 5% для имитации рыночного исполнения. Срок действия краткосрочных ордеров также отличается - используется высота блока (goodTilBlock), рекомендуется устанавливать текущий блок +10.
Order ID: Поскольку операция размещения ордера выполняется непосредственно в блокчейне, после трансляции сообщения не генерируется индексный order ID. Для обеспечения уникальности и точности запросов, order ID формируется из следующих компонентов (разделены запятыми):
Сводка сообщения об отмене ордера
{
"@type": "/dydxprotocol.clob.MsgCancelOrder",
"orderId": {
"subaccountId": {
"owner": "xxx"
},
"clientId": 2585872024,
"orderFlags": 64,
"clobPairId": 1
},
"goodTilBlockTime": 1742295981
}
Требуется передача order ID, возвращенного API размещения ордера платформы FMZ.
{
"@type": "/dydxprotocol.sending.MsgCreateTransfer",
"transfer": {
"sender": {
"owner": "xxx"
},
"recipient": {
"owner": "xxx",
"number": 128
},
"amount": "10000000"
}
}
В dydx v4 можно создавать множество субаккаунтов, где subAccountNumber 0 - первый автоматически созданный субаккаунт. Номера субаккаунтов ≥128 используются для торговли с изолированными позициями, требующими минимум 20 USDC.
Пример: переводы между subAccountNumber 0 ↔ 128. Транзакции перевода требуют оплаты Gas Fee, которая может быть списана в USDC или токенах dydx.
Вышеизложенный материал кратко объяснил некоторые аспекты инкапсуляции. Теперь перейдем к практическому применению, используя тестовую сеть dYdX v4 для демонстрации. Тестовая сеть практически идентична основной сети и имеет автоматический кран для получения тестовых активов. Процедура развертывания опекуна (хранителя) не будет подробно описываться — создадим реальный тест на платформе FMZ.
После успешного подключения криптокошелька к приложению dYdX v4 (в примере используется imToken) и получения тестовых активов экспортируем мнемоническую фразу текущего аккаунта dYdX v4 (производного от кошелька).
Настраиваем мнемоническую фразу на платформе FMZ, используя метод локального файла (можно также ввести напрямую — фраза шифруется при настройке, не в открытом виде).
Размещается в директории ID реального теста хранителя. Также возможен другой путь (требуется указать абсолютный путь при настройке).
В поле для мнемонической фразы укажите: file:///mnemonic.txt
. Соответствующий абсолютный путь: директория_хранителя/logs/storage/594291
.
function main() {
// Переключение адреса индексатора тестовой сети
exchange.SetBase("https://indexer.v4testnet.dydx.exchange")
// Смена ChainId для тестовой сети
exchange.IO("chainId", "dydx-testnet-4")
// Обновление базового адреса REST API
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: "тестовые рынки", cols: ["key", "Символ", "Базовый актив", "Котируемый актив", "Шаг цены", "Шаг объема", "Точность цены", "Точность объема", "Мин. объем", "Макс. объем", "Мин. номинал", "Макс. номинал", "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、Размещение ордеров
```js
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 App:
Предварительное размещение двух ордеров в тестовой сети для проверки получения текущих ордеров и их отмены.
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: "Тест 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: "Тест 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")
// Пример управления субаккаунтами
var subaccounts = exchange.GetSubAccounts()
Log("Субаккаунты:", subaccounts)
// Создание нового субаккаунта
var newSubAcc = exchange.CreateSubAccount("subacc01")
Log("Создан субаккаунт:", newSubAcc)
// Установка активного субаккаунта
exchange.SetSubAccount("subacc01")
var balance = exchange.GetBalance()
LogStatus("Баланс активного субаккаунта:", balance)
}
exchange.IO(“restApiBase”, “https://dydx-testnet-api.polkachu.com”)
// Перевод 20 USDC с subAccountNumber 0 на 128, комиссия 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)
}
![Практика количественных стратегий на DEX (1): Руководство по dYdX v4](/upload/asset/175bd506389b9e8966ca.png)
Данные аккаунта после переключения на subAccountNumber 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
}
В субаккаунте subAccountNumber 128 подтвержден перевод 20 USDC.
Получение TxHash по ордеру через вызов REST-интерфейса узла
Для получения TxHash ордера объект биржи dydx сохраняет хэш в кеше, который можно запросить по ID ордера. Однако при остановке стратегии кешированные сопоставления хэшей транзакций заказов очищаются.
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": []
},
...
Приведенные тесты выполнены на последней версии опекуна. Для работы с dYdX v4 DEX требуется скачать актуальную версию опекуна.
Благодарим за поддержку и внимание к материалу.