[TOC]
本篇是关于DEX交易所量化实践的第三篇,这次要介绍Vertex协议的使用指南。
在传统去中心化交易所(DEX)的框架中,量化交易者往往需要妥协:要么接受自动化做市商(AMM)模型的高滑点与低执行效率,要么陷入跨链流动性割裂、衍生品功能单一的技术困局。而Vertex Protocol的横空出世,正在用一场“去中心化+机构级”的融合实验,重新定义量化策略的链上疆域——这里没有“二选一”的难题,只有速度、深度与自由的终极平衡。
作为首个将统一多链流动性池、混合订单簿(CLOB)与嵌入式货币市场整合于一体的DEX,Vertex以“中心化体验,去中心化灵魂”为核心,为量化交易者开辟了一条独特的赛道:
速度与流动性的新定义
随着区块链技术的不断演进,传统中心化交易所(CEX)与去中心化交易所(DEX)之间的界限正逐渐模糊。Vertex Edge 作为 Vertex 平台的中枢,不仅重塑了交易速度和流动性,还通过跨链整合将卓越的订单撮合技术和自我托管优势完美结合,为全球交易者带来全新的 DeFi 体验。
统一跨链流动性:打破流动性碎片 在传统市场中,不同链之间的流动性常常存在割裂现象,导致交易者无法享受到最佳的成交价格和深度。Vertex Edge 正是在这一背景下应运而生,通过一个统一的订单簿网络,实现了跨越多条链的永久流动性同步共享。 目前,Vertex Edge 已将永续合约流动性覆盖到包括 Arbitrum、Base、Sei、Blast、Mantle、Sonic 以及 Abstract 在内的 7 条主流链上,使交易者无需再为流动性分散而烦恼,能够以最优的价格进行交易,真正做到全球流动性无缝对接。
混合订单簿交易:极速撮合与链上结算的完美平衡 Vertex Edge 采用了混合订单簿交易模式,其核心技术包括:
离链订单簿撮合器:利用超高速的离链撮合机制,实现订单匹配,延迟时间仅在 5-15 毫秒之间,与大多数中心化交易所媲美; 链上风险引擎与 AMM:在每条支持的链上均部署了风险管理系统和自动化做市商(AMM),确保订单在匹配后能以安全、透明的方式进行结算。 这种架构不仅保证了交易的极速响应,还通过链上结算为用户提供了去中心化的安全保障,让交易者既享受 CEX 级别的性能,又保持资产自我托管的独立性。
多账户功能:在单一钱包内管理多账户,更高效地分配资金; 杠杆现货头寸:利用全部资产作为保证金,实现更高资本效率; 灵活风险管理:将存款、仓位与盈亏数据统一考量,从而精准调控风险暴露。
登录 「vertex protocol」 页面地址:
vertex和大部分DEX一样,登录dapp后都需要连接钱包授权,Vertex的子账户系统是根据一个标签来区分,标签参与钱包地址计算,得出一个子账号钱包地址,同时授权这个地址可以进行下单等操作。
例如使用WalletConnect连接时的钱包地址为:0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43
,默认使用的标签为”default”,通过计算得出的子账号地址为:0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c4364656661756c740000000000
。默认的标签即:64656661756c740000000000
。
vertex 支持冲入多种资产,一般选择充值USDC作为保证金,直接使用连接的钱包转账交易。需要注意的是在vertex上提现、划转、发送代币、子账户划转等操作都是会消耗USDC的,并且费用不低,所以这些操作需要谨慎调用。
切换到不同的链:
在 vertex 上,不同的链有不同的节点、索引器、链ID等配置信息,FMZ封装时默认的链为Arbitrum One
,可以使用Log(HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=contracts"))
查询某个链的ID、部署的合约信息等。
登录FMZ.COM之后,在交易所配置页面,选择「加密货币」,选择vertex交易所,可以直接配置dapp上的钱包代理秘钥,当然配置钱包私钥也是可以的,在vertex上可以使用接口管理代理秘钥授权/解除授权等操作,也比较方便。
代理秘钥:
例如在vertex的DEX交易所前端页面:chrome浏览器(打开调试) -> Application -> Local Storage -> https://app.vertex -> vertex userSettings
配置vertex交易所
在FMZ上需要配置两个内容,第一个是钱包地址(不是签名用的代理秘钥的地址,一定要是连接dapp的钱包地址)。第二个是签名用的秘钥(可以是钱包私钥,也可以是代理秘钥)。由于可以使用代理秘钥,所以配置的钱包地址和秘钥并不一定是一对的。
vertex的子账号系统根据标签来识别,在FMZ上默认使用标签为default
标签的主子账号,如果需要切换,在代码中可以使用:
exchange.IO("subAccountTag", "default") // 切换到主子账号
exchange.IO("subAccountTag", "test01") // 切换到标签名为 test01 的子账号
配置好了交易所配置信息,部署一个可以访问到vertex接口的托管者程序,我们就可以开始写点儿代码进行实践操作了。
我们使用主子账户进行测试(tag为:default的子账户),我们使用Arbitrum网络。
1、获取合约市场品种信息
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", "CtValCcy"
],
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, market.CtValCcy
])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
return markets
}
可以看到vertex上的合约品种为USDC本位合约,保证金为USDC,一张合约的价值表示一个对应币种,例如BTC_USDC.swap
即BTC的USDC本位合约,一张表示一个BTC的头寸。以上代码展示了如何请求合约市场信息,输出各项内容。
2、获取深度信息(订单薄)
function main() {
var depths = [{"symbol": "ETH_USDC"}, {"symbol": "SOL_USDC"}, {"symbol": "BTC_USDC"}]
for (var ele of depths) {
ele["depth"] = exchange.GetDepth(ele["symbol"] + ".swap")
}
var tbls = []
for (var ele of depths) {
var tbl = {"type": "table", "title": ele["symbol"], "cols": ["level", "price", "amount"], "rows": []}
var depth = ele["depth"]
for (var i = 0 ; i < 3 ; i++) {
tbl["rows"].push(["卖" + (i + 1), depth.Asks[i].Price, depth.Asks[i].Amount])
}
tbl["rows"].reverse()
for (var i = 0 ; i < 3 ; i++) {
tbl["rows"].push(["买" + (i + 1), depth.Bids[i].Price, depth.Bids[i].Amount])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
3、市场成交的订单流信息
function main() {
var arrTrades = [{"symbol": "ETH_USDC"}, {"symbol": "SOL_USDC"}, {"symbol": "BTC_USDC"}]
for (var ele of arrTrades) {
ele["trades"] = exchange.GetTrades(ele["symbol"] + ".swap")
}
var tbls = []
for (var ele of arrTrades) {
var tbl = {"type": "table", "title": ele["symbol"], "cols": ["Time", "Price", "Amount", "side"], "rows": []}
var trades = ele["trades"]
for (var trade of trades) {
tbl["rows"].push([_D(trade.Time), trade.Price, trade.Amount, trade.Type == 0 ? "买入" : "卖出"])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
4、K线数据
function main() {
let c = KLineChart({
overlay: true
})
let bars = exchange.GetRecords("SOL_USDC.swap")
if (!bars) {
return
}
bars.forEach(function(bar, index) {
c.begin(bar)
Log(index, bar)
c.close()
})
}
vertex 图表
FMZ策略运行画图
5、资金费率
function main() {
var fundings = exchange.GetFundings()
var tbl = {
"type": "table",
"title": "GetFundings",
"cols": ["Symbol", "Interval", "Time", "Rate"],
"rows": [],
}
for (var f of fundings) {
tbl["rows"].push([f.Symbol, f.Interval / 3600000, _D(f.Time), f.Rate * 100 + " %"])
}
LogStatus(_D(), "\n`" + JSON.stringify(tbl) + "`")
}
资金费率周期为1小时。
6、查询链上数据,给定账户、品种的最大订单量
Max Order Size Gets the max order size possible of a given product for a given subaccount.
function main() {
// GET [GATEWAY_REST_ENDPOINT]/query?type=max_order_size&product_id={product_id}&sender={sender}&price_x18={price_x18}&direction={direction}
// price_x18=3000000000000000000000 : 3000 USDC
// product_id=4 : ETH_USDC.swap
// sender=0x123 : e.g. 0x123
return HttpQuery("https://gateway.prod.vertexprotocol.com/query?type=max_order_size&product_id=4&sender=0x123&price_x18=3000000000000000000000&direction=short")
}
最终请求返回的数据:{"status":"success","data":{"max_order_size":"170536415320344899"},"request_type":"query_max_order_size"}
可知,当前账户可用资产对于价格为3000的以太坊永续合约,卖单订单最大下单量为:0.17 ETH
7、查询钱包授权的代理秘钥信息
Linked Signer Retrieves current linked signer of a provided subaccount
function main() {
return HttpQuery("https://gateway.prod.vertexprotocol.com/query?type=linked_signer&subaccount=0x123")
}
查询到授权的信息:
{“status”:“success”,“data”:{“linked_signer”:“0x79119…”},“request_type”:“query_linked_signer”} “0x79119…“这个地址就是在vertex前端页面连接钱包时,授权下单交易的代理地址。这个授权可以取消、新增(通过API调用)。
接下来就是本篇的重点了,忙活了半天其实就是为了在去中心化交易所上简单、快捷的进行交易。
1、下单
测试比较简单的交易,下普通的限价单。
function main() {
var id1 = exchange.CreateOrder("ETH_USDC.swap", "buy", 2000, 0.1)
var id2 = exchange.CreateOrder("SOL_USDC.swap", "buy", 60, 2)
Log("ETH_USDC.swap id1:", id1)
Log("SOL_USDC.swap id2:", id2)
var orders = exchange.GetOrders("USDC.swap")
var tbl = {type: "table", title: "test GetOrders", cols: ["Symbol", "Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
for (var order of orders) {
tbl.rows.push([order.Symbol, order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
2、撤单
function main() {
var orders = exchange.GetOrders("USDC.swap")
var tbl = {type: "table", title: "test GetOrders", cols: ["Symbol", "Id", "Price", "Amount", "DealAmount", "AvgPrice", "Status", "Type", "Offset", "ContractType"], rows: []}
for (var order of orders) {
tbl.rows.push([order.Symbol, order.Id, order.Price, order.Amount, order.DealAmount, order.AvgPrice, order.Status, order.Type, order.Offset, order.ContractType])
exchange.CancelOrder(order.Id)
}
LogStatus("`" + JSON.stringify(tbl) + "`")
return exchange.GetOrders()
}
3、查询持仓
function main() {
// 使用市价单下单持仓
exchange.SetCurrency("ETH_USDC")
exchange.SetContractType("swap")
exchange.Buy(-1, 0.01)
var positions = exchange.GetPositions()
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])
}
LogStatus("`" + JSON.stringify(tbl) + "`")
}
需要注意的是,vertex API给出的数据与vertex 前端页面上显示的内容有偏差,主要是持仓均价、持仓盈亏有差异,已经反馈给vertex团队,后续可能会升级更新。
4、Trigger Order
由于 vertex 的 Trigger Order 是独立的endPoint,所以在使用exchange.IO
函数下条件单时,需要指明Trigger: https://trigger.prod.vertexprotocol.com
,接下来让我们继续实践操作。
function main() {
// isTrigger : true
var nonce = exchange.IO("nonce", true) // 如果是 Trigger Order 订单用到的 nonce 需要指定 isTrigger : true
// flag , reduceOnly
var expiration = exchange.IO("expiration", "GTC", false) // 设置订单为GTC类型,非只减仓
// params
var params = {
"place_order": {
"product_id": 4,
"order": {
"sender": "0x123...",
"priceX18": "4100000000000000000000",
"amount": "-100000000000000000",
"expiration": expiration,
"nonce": nonce
},
"trigger": {
"price_above": "4000000000000000000000"
}
}
}
return exchange.IO("api", "POST", "https://trigger.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
在trigger端点下,还有:
调用方式与Place Trigger Order类似,这里就不再赘述。
1、切换子账号tag
默认的子账号tag为:default
,我们切换为一个自定义的tag:subAcc02
。
function main() {
exchange.IO("subAccountTag", "subAcc02")
return exchange.GetAccount()
}
当向子账号地址转账时,vertex才会真正创建这个子账号。
2、向vertex子账号转账
需要查询账户的nonce,作为参数传入转账接口的参数中。
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"transfer_quote": {
"tx": {
// default -> subAcc02
"sender": "0xabc...", // default
"recipient": "0xdef...", // subAcc02
"amount": "7000000000000000000",
"nonce": nonce
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
例如:0xabc… 对应的是tag为default的子账号地址。 0xdef… 对应的是tag为subAcc02的子账号地址。 0x123… 为钱包地址。
1、从vertex提币到钱包
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"withdraw_collateral": {
"tx": {
"sender": "0xabc...", // default
"productId": 0, // USDC : 0 , precision : 6
"amount": "10000000", // 10 USDC
"nonce": nonce
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
注意USDC的精度。
2、铸造LP
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"mint_lp": {
"tx": {
"sender": "0xabc...", // default
"productId": 31, // USDT_USDC
"amountBase": "10000000000000000000",
"quoteAmountLow": "9999900000000000000",
"quoteAmountHigh": "10100000000000000000",
"nonce": nonce,
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
铸造LP代币,给交易对USDT_USDC
的交换池添加了流动性。
3、销毁LP
function main() {
var ret = HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=nonces&address=0x123...")
var obj = JSON.parse(ret)
var nonce = obj["data"]["tx_nonce"]
Log("nonce:", nonce)
var params = {
"burn_lp": {
"tx": {
"sender": "0xabc...", // default
"productId": 31, // USDT_USDC
"amount": "7500000000000000000",
"nonce": nonce,
}
}
}
return exchange.IO("api", "POST", "https://gateway.prod.vertexprotocol.com/v1/execute", "", JSON.stringify(params))
}
Websocket接口端点:wss://gateway.prod.vertexprotocol.com/v1/subscribe
。
vertex 的 websocket接口需要启用压缩,所以在使用Dial函数创建websocket连接时,需要指定:enableCompression=true
。
1、订阅公共接口
var ws = null
function main() {
var params = {
"method": "subscribe",
"stream": {
"type": "book_depth",
"product_id": 4
},
"id": 0
}
ws = Dial("wss://gateway.prod.vertexprotocol.com/v1/subscribe|enableCompression=true")
if (!ws) {
Log("error")
return
}
ws.write(JSON.stringify(params))
for (var i = 0 ; i < 10 ; i++) {
var ret = ws.read()
if (ret) {
Log(ret)
}
}
}
function onexit() {
ws.close()
Log("close ws")
}
2、订阅私有接口验证
因为私有接口需要验证签名,所以增加了一个计算签名的函数exchange.IO("signature", strPayload)
。
var ws = null
function main() {
// authenticate
var expiration = new Date().getTime() + 1000 * 15
var params = {
"method": "authenticate",
"id": 0,
"tx": {
"sender": "0x123...",
"expiration": String(expiration)
}
}
var signature = exchange.IO("signature", JSON.stringify(params))
Log("signature:", signature)
params["signature"] = signature
ws = Dial("wss://gateway.prod.vertexprotocol.com/v1/subscribe|enableCompression=true")
if (!ws) {
Log("error")
return
}
ws.write(JSON.stringify(params))
var ret = ws.read()
Log(ret)
// order_update
var orderUpdateParams = {
"method": "subscribe",
"stream": {
"type": "order_update",
"subaccount": "0x123...",
"product_id": 4
},
"id": 0
}
ws.write(JSON.stringify(orderUpdateParams))
var id = exchange.CreateOrder("ETH_USDC.swap", "buy", 2300, 0.1)
Log("id:", id)
for (var i = 0 ; i < 10 ; i++) {
var ret = ws.read(1000)
if (ret) {
Log(ret)
}
}
// list
var listParams = {
"method": "list",
"id": 0
}
ws.write(JSON.stringify(listParams))
ret = ws.read()
Log(ret)
}
function onexit() {
ws.close()
Log("close ws")
}
vertex现货交易所对象特有的功能切换:
- exchange.IO(“trade_margin”) : 启用现货交易市场的Margin Spot
。
- exchange.IO(“trade_normal”) : 关闭现货交易市场的Margin Spot
。
在FMZ上不仅封装了vertex永续合约交易市场,也封装了vertex现货交易市场。vertex现货的封装与vertex合约一致,在FMZ平台上添加vertex现货交易所对象即可使用。
exchange.IO("trade_margin")
开启。接下来我们在FMZ平台仅用不到40行代码来设计一个简单的策略,参数越少普适性越强。策略用到了「画线类库」,用来画图。
策略代码:
function printProfit() {
var account = _C(exchange.GetAccount)
LogProfit(account.Balance)
}
function main() {
var initPrice = -1
while (true) {
// tick
var t = _C(exchange.GetTicker, symbol)
var nowPrice = t.Last
if (nowPrice <= 0) {
Log("ticker:", t)
Sleep(10000)
continue
}
if (initPrice < 0) {
initPrice = nowPrice
}
// pos
var pos = _C(exchange.GetPositions, symbol)
// assets
var assets = _C(exchange.GetAssets)
var rate = (nowPrice - initPrice) / initPrice
if (rate < -ratio) {
var id = exchange.CreateOrder(symbol, "buy", -1, amount)
Log("open long, id:", id)
initPrice = nowPrice
} else if (rate > ratio) {
var id = exchange.CreateOrder(symbol, "sell", -1, amount)
Log("open short, id:", id)
initPrice = nowPrice
}
$.PlotLine("price", nowPrice) // 使用画线类库,需要在策略模板栏勾选「画线类库」来画图
LogStatus(_D(), ", initPrice:", initPrice, ", rate:", rate, "\n", pos, "\n", assets)
Sleep(1000 * 60)
}
}
策略参数:
策略回测:
vertex 合约上默认的是单向持仓,我们直接上实盘跑跑看。
半夜策略自己开仓了,早上一看已经浮盈了(开心)。
策略仅供教学、测试、学习,实盘慎用。
以上测试,基于最新的托管者,需要下载最新的托管者才支持 Vertex DEX 聚合器。
感谢支持,感谢您的阅读。