资源加载中... loading...

DEX交易所量化实践(1)-- dYdX v4 使用指南

Author: 发明者量化-小小梦, Created: 2024-12-24 17:09:32, Updated: 2024-12-26 21:41:46

[TOC]

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连接,订阅订单薄数据的例子:

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链节点消息广播

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

  • 订单消息摘要

    {
      "@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(毫秒)
  • 撤单消息摘要

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

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

  • 转账消息摘要

    {
      "@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测试网

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
}

3、市场信息查询

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

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、下单

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、订单信息

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

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、持仓信息查询

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、子账号管理

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返回的数据:

{
	"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哈希映射会清空。

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)

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

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

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


More