Type/to search
8
Follow
1364
Followers
DEX交易所量化实践(1)-- dYdX v4 使用指南
Discussions
Created 2024-12-24 17:09:32  Updated 2024-12-26 21:41:46
 0
 1445

img

前言

随着去中心化交易所(DEX)在加密货币交易领域的迅速崛起,量化交易者开始逐步转向这些平台进行高效的自动化交易。dYdX 作为最受欢迎的去中心化交易平台之一,提供了强大的交易功能,支持期货永续合约交易,其最新版本 v4 更是优化了性能和用户体验,成为了许多量化交易者的首选。

本篇文章将介绍如何在 dYdX v4 上进行量化交易实践,包括如何使用其 API 进行交易、获取市场数据以及管理账户等。

  • 测试环境切换
  • 市场信息查询
  • 订单信息、持仓信息查询
  • 下单交易
  • 子账号管理
  • 节点方法请求

dYdX v4 DEX

  • dYdX 测试网 App页面

    img

  • dYdX v3一样,交易会产生奖励,奖励dYdX代币。

    img

钱包连接、登录、配置信息

之前的dYdX v3 协议 DEX交易所已经下线,目前dYdX v4 App地址:

https://dydx.trade/trade/ETH-USD

打开App页面之后,右上角有连接钱包的按钮,扫码连接钱包。

如果希望先在测试网环境测试熟悉,可以使用测试网:

https://v4.testnet.dydx.exchange/trade/ETH-USD

同样是右上角点击连接钱包按钮,扫码连接钱包,签名验证。钱包连接成功后会自动生成一个dydx v4地址,App页面右上角就会显示这个地址,点击后会弹出一个菜单。其中有充值、提币、转账等操作。dYdX主网(生产环境)与测试网的区别之一是:在测试网点击充值,会自动使用水龙头冲入300 USDC资产用于测试。如果希望在dYdX上做真正的交易就需要冲入USDC资产了,充值也很方便,兼容多种资产、链进行充值。

  • dYdX v4 账户地址
    dYdX v4 账户地址由钱包地址派生,dYdX v4 账户地址类似这个样子:dydx1xxxxxxxxxxxxxxxxxxxxq2ge5jr4nzfeljxxxx,是dydx1开头的地址。这个地址可以在区块链explorers中查询。

  • 助记词
    可以用右上角的菜单中点击「导出密码口令」按钮导出当前dYdX地址账户的助记词。在FMZ平台上添加交易所时,需要配置这个助记词。

    助记词可以直接配置在FMZ平台,也可以保存在托管者本地,在使用dydx v4交易所对象时,会读取记录助记词的文件内容,在本文实操部分会进行演示。

主网、测试网差异

测试网环境在某些方面与主网环境有一些差别,以下简单列举几条。

  • 子账号资产划转。
    主网有子账号清扫机制,在subAccountNumber >= 128时,如果该ID的子账号没有持仓会自动清扫资产到subAccountNumber为0的子账号。
    测试中发现测试网没有这种机制(或者触发条件不同,在测试网没有触发过)。
  • 某些代币名称。
    原生代币dydx的命名不同:主网DYDX,测试网Dv4TNT
  • 地址配置,例如链ID,节点地址,索引器地址等。
    节点和配置有很多,这里列举其中一种:
    • 主网:
      索引器地址:https://indexer.dydx.trade
      链ID:dydx-mainnet-1
      REST节点:https://dydx-dao-api.polkachu.com:443

    • 测试网:
      索引器地址:https://indexer.v4testnet.dydx.exchange
      链ID:dydx-testnet-4
      REST节点:https://dydx-testnet-api.polkachu.com

dYdX v4 协议架构

dYdX v4 协议基于 cosmos 生态开发,dYdX v4 DEX 系统交易相关内容主要有2部分构成:

  • 负责行情信息、账户信息等查询的索引器。
  • dydx区块链订单消息、撤单消息、转账消息等。

索引器

索引器服务提供了REST协议和Websocket协议。

  • REST协议
    REST协议接口支持行情信息查询、账户信息、持仓信息、订单信息等查询,在FMZ平台已经封装为平台统一的API接口。

  • WebSocket协议
    在FMZ平台可以使用Dial函数创建Websocket连接,订阅行情等信息。

需要注意dydx v4的索引器与中心化交易所有同样的一个问题,数据更新并非那么及时,例如有时候下单后立即查询,可能查询不到订单。建议在某些操作后(Sleep(n))等待几秒后再查询。

这里给出一个使用Dial函数,创建Websocket API连接,订阅订单薄数据的例子:

javascript
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) + "`") } }

dYdX链节点消息广播

在交易中最常用的是订单消息、撤单消息、转账消息。

  • 订单消息摘要

    json
    { "@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个区块高度后失效。

    • 订单ID:
      由于下单操作是直接在链上进行,消息广播后不会有索引器生成的订单ID,无法使用索引器订单作为平台下单函数的返回值,为了确保订单ID唯一性和订单查询准确,返回的订单ID由以下信息构成(英文逗号间隔):

      • 交易对
      • dydx当前账户地址
      • 子账号序号(subaccountNumber)
      • clientId(随机生成)
      • clobPairId(交易品种ID)
      • orderFlags
      • goodTilData(毫秒)
  • 撤单消息摘要

    json
    { "@type": "/dydxprotocol.clob.MsgCancelOrder", "orderId": { "subaccountId": { "owner": "xxx" }, "clientId": 2585872024, "orderFlags": 64, "clobPairId": 1 }, "goodTilBlockTime": 1742295981 }

    需要传入FMZ平台下单接口返回的订单ID。

  • 转账消息摘要

    json
    { "@type": "/dydxprotocol.sending.MsgCreateTransfer", "transfer": { "sender": { "owner": "xxx" }, "recipient": { "owner": "xxx", "number": 128 }, "amount": "10000000" } }

    当前dydx v4 地址下可以创建很多子账号,其中subAccountNumber为0是第一个自动创建的子账号,subAccountNumber高于等于128的子账号ID用于逐仓品种的交易,最少需要20USDC的资产。
    举例,可以从subAccountNumber 0 -> 128 ,也可以从subAccountNumber 128 -> 0。划转需要消耗Gas Fee。Gas Fee可以使用USDC、dydx 代币。

FMZ平台 dYdX v4 实践

以上内容简单说明了一些封装细节,接下来我们一起来实践一下具体使用,这里使用dYdX v4的测试网进行演示,测试网与主网基本一致,并且有自动的水龙头可以领取测试资产,托管者部署操作就不再赘述,在FMZ上创建实盘测试。

1、配置

使用加密货币钱包成功连接dYdX v4 App后(我这里使用的imToken钱包),领取测试资产,然后导出当前dYdX v4账号(由钱包派生)助记词。

img

把助记词配置在FMZ平台,这里我们使用本地文件方式配置(也可以直接填写,配置到平台,助记词是加密后配置上的,并非明文)。

  • 助记词文件:mnemonic.txt

    img

    放在托管者目录下实盘ID文件夹目录内,当然也可以放在其它目录(配置时需要写具体路径)。

  • 在FMZ上配置交易所

    https://www.fmz.com/m/platforms/add

    助记词编辑框填写:file:///mnemonic.txt,对应的实际路径为:托管者所在目录/logs/storage/594291

    img

2、切换到dydx v4测试网

javascript
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()) }

读取到测试网账户信息:

json
{ "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 }

3、市场信息查询

没有切换到测试网,用主网测试

javascript
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) + "`") }

img

4、下单

javascript
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) }

img

dYdX v4 App 页面:

img

5、订单信息

测试网提前挂出两个订单,测试获取当前挂单,撤销订单。

javascript
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) + "`") }

img

6、持仓信息查询

javascript
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) + "`") }

img

7、子账号管理

javascript
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) }

img

切换到subAccountNumber为128的子账号,GetAccount返回的数据:

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的子账号,转入了20USDC。

8、获取TxHash,调用REST节点接口

根据订单,获取TxHash,测试IO调用REST节点的方法

如何获取订单的TxHash,交易所对象dydx会缓存TxHash,可以用订单ID查询。但是策略停止后,缓存的订单 tx哈希映射会清空。

javascript
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) } }

img

通过TxHash查询到的消息:

var ret = exchange.IO("api", "GET", "/cosmos/tx/v1beta1/txs/" + txHash)

内容太长,节选部分演示:

json
{ "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": [] }, ...

THE END

以上测试,基于最新的托管者,需要下载最新的托管者才支持dYdX v4 DEX

感谢支持,感谢您的阅读。

Comment
All comments (0)
No data
No data
  • 1
iPhone Download
Forums
PINE Language
© 2015 - ∞ INVENTOR PTE LTD (SG)