[TOC]
This is the third article about the quantitative practice of DEX exchanges. In this article, we will introduce the user guide of the Vertex protocol.
In the framework of traditional decentralized exchanges (DEX), quantitative traders often need to compromise: either accept the high slippage and low execution efficiency of the automated market maker (AMM) model, or fall into the technical dilemma of cross-chain liquidity fragmentation and single derivative function. The emergence of Vertex Protocol is redefining the on-chain territory of quantitative strategies with a “decentralized + institutional-level” fusion experiment - there is no “choose one or the other” problem here, only the ultimate balance of speed, depth and freedom.
As the first DEX to integrate a unified multi-chain liquidity pool, hybrid order book (CLOB) and embedded currency market, Vertex takes “centralized experience, decentralized soul” as its core and opens up a unique track for quantitative traders:
A new definition of speed and fluidity
As blockchain technology continues to evolve, the boundaries between traditional centralized exchanges (CEX) and decentralized exchanges (DEX) are blurring gradually. As the hub of the Vertex platform, Vertex Edge not only reshapes transaction speed and liquidity, but also combines excellent order matching technology and self-custody advantages perfectly through cross-chain integration, bringing a new DeFi experience to global traders.
Unified cross-chain liquidity: breaking liquidity fragmentation In traditional markets, liquidity between different chains is often fragmented, resulting in traders being unable to enjoy the best transaction price and depth. Vertex Edge was born in this context, and through a unified order book network, it has achieved synchronous sharing of permanent liquidity across multiple chains. At present, Vertex Edge has covered perpetual contract liquidity on 7 mainstream chains including Arbitrum, Base, Sei, Blast, Mantle, Sonic and Abstract, so that traders no longer need to worry about the dispersion of liquidity, and trade at the best price, truly achieving seamless global liquidity.
Hybrid order book trading: the perfect balance between ultra-fast matching and on-chain settlement Vertex Edge adopts a hybrid order book trading model, and its core technologies include:
Off-chain order book matcher: Utilizes ultra-high-speed off-chain matching mechanism to achieve order matching, with a latency of only 5-15 milliseconds, comparable to most centralized exchanges; On-chain risk engine and AMM: Risk management system and automated market maker (AMM) are deployed on each supported chain to ensure that orders can be settled in a secure and transparent manner after matching. This architecture not only ensures extremely fast response times for transactions, but also provides users with decentralized security through on-chain settlement, allowing traders to enjoy CEX-level performance while maintaining the independence of asset self-custody.
Multi-account function: manage multiple accounts in a single wallet to allocate funds more efficiently; Leveraged spot positions: use all assets as margin to achieve higher capital efficiency; Flexible risk management: consider deposits, positions and profit and loss data in a unified manner to control risk exposure accurately.
Log in to the “vertex protocol” page address:
Vertex is the same as most DEXs. After logging into the dapp, you need to connect to the wallet for authorization. Vertex’s sub-account system is distinguished based on a label. The label participates in the wallet address calculation to obtain a sub-account wallet address. At the same time, this address is authorized to place orders and other operations.
For example, the wallet address when connected using WalletConnect is: 0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43
, the default label used is “default”, and the sub-account address calculated is: 0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c4364656661756c7400000000000
. The default label is: 64656661756c7400000000000
.
Vertex supports charging into multiple assets. Generally, we can choose to top up USDC as margin and use the connected wallet to transfer transactions directly. It should be noted that operations such as withdrawing, transferring, sending tokens, and transferring sub-accounts on Vertex will consume USDC, and the fees are not low, so these operations need to be called with caution.
Switch to a different chain:
On vertex, different chains have different configuration information such as nodes, indexers, chain IDs, etc. The default chain when FMZ is encapsulated is Arbitrum One
, we can use Log(HttpQuery("https://gateway.prod.vertexprotocol.com/v1/query?type=contracts"))
to query a chain’s ID, deployed contract information, etc.
After logging in to FMZ.COM, on the exchange configuration page, select “Cryptocurrency”, select Vertex Exchange, you can configure the wallet proxy key on the dapp directly. Of course, you can also configure the wallet private key. On Vertex, you can use the interface to manage proxy key authorization/deauthorization and other operations, which is also convenient.
Proxy key:
For example, on the front-end page of the vertex DEX exchange: Chrome browser (turn on debugging) -> Application -> Local Storage -> https://app.vertex -> vertex userSettings
Configure Vertex exchange
There are two things that need to be configured on FMZ. The first is the wallet address (not the address of the proxy key used for signing, it must be the wallet address connected to the dapp). The second is the secret key used for signing (it can be the wallet private key or the proxy key). Since the proxy key can be used, the configured wallet address and secret key are not necessarily a pair.
The vertex sub-account system is identified by tags. On FMZ, the default main sub-account is used. If you need to switch, you can use the following in the code:
exchange.IO("subAccountTag", "default") // Switch to the main sub-account
exchange.IO("subAccountTag", "test01") // Switch to the sub-account with the label name test01
After configuring the exchange configuration information and deploying a docker program that can access the vertex interface, we can start writing some code for practical operations.
We use the main sub-account for testing (the sub-account with the tag: default), and we use the Arbitrum network.
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
}
It can be seen that the contract type on Vertex is USDC-based contract, the margin is USDC, and the value of one contract represents one corresponding currency. For example, BTC_USDC.swap
is the USDC-based contract of BTC, one represents one BTC position. The above code shows how to request contract market information and output various contents.
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(["sell" + (i + 1), depth.Asks[i].Price, depth.Asks[i].Amount])
}
tbl["rows"].reverse()
for (var i = 0 ; i < 3 ; i++) {
tbl["rows"].push(["buy" + (i + 1), depth.Bids[i].Price, depth.Bids[i].Amount])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
Status information
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 ? "buy" : "sell"])
}
tbls.push(tbl)
}
LogStatus("`" + JSON.stringify(tbls) + "`")
}
Status information
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 chart
FMZ strategy operation chart
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) + "`")
}
The funding rate period is 1 hour.
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")
}
The data returned by the final request: {"status":"success","data":{"max_order_size":"170536415320344899"},"request_type":"query_max_order_size"}
It can be seen that the current account’s available assets for the Ethereum perpetual contract with a price of 3000, the maximum order quantity for a sell order is: 0.17 ETH
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")
}
Authorization information found:
{“status”:“success”,“data”:{“linked_signer”:“0x79119…”},“request_type”:“query_linked_signer”} “0x79119…” This address is the proxy address that authorizes the order transaction when the wallet is connected to the vertex front-end page. This authorization can be canceled or added (through API calls).
The following is the focus of this article. We have been busy for a long time just to conduct transactions simply and quickly on decentralized exchanges. 1. Place an order
Test a simple trade and place a normal limit order.
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) + "`")
}
Status information
Log information
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()
}
Status information
Log information
function main() {
// Use market orders to open positions
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) + "`")
}
Status information
Log information
It should be noted that the data provided by the Vertex API deviates from the content displayed on the Vertex front-end page, mainly due to differences in the average holding price and the profit and loss of holdings. This has been fed back to the Vertex team and it may be updated later.
Since the Trigger Order of vertex is an independent endPoint, when using the exchange.IO
function to place a conditional order, we need to specify Trigger: https://trigger.prod.vertexprotocol.com
. Next, let’s continue to practice operation.
function main() {
// isTrigger : true
var nonce = exchange.IO("nonce", true) // If the nonce used in a Trigger Order, you need to specify isTrigger: true
// flag , reduceOnly
var expiration = exchange.IO("expiration", "GTC", false) // Set the order to GTC type, not just reduce position
// 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))
}
Under the trigger endpoint, there is also:
The calling method is similar to Place Trigger Order, so I will not go into details here.
The default sub-account tag is: default
, we switch to a custom tag: subAcc02
.
function main() {
exchange.IO("subAccountTag", "subAcc02")
return exchange.GetAccount()
}
Function results
When transferring money to a sub-account address, Vertex will create this sub-account.
The nonce of the account needs to be queried and passed into the parameters of the transfer interface as a parameter.
https://gateway.prod.vertexprotocol.com/v1/query? type=nonces&address=0x6B3f11d807809B0b1E5e3243df04a280d9F94bF4
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))
}
For example: 0xabc… corresponds to the sub-account address with tag default. 0xdef… corresponds to the sub-account address with tag subAcc02. 0x123… is the wallet address.
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))
}
Pay attention to the precision of USDC.
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))
}
Minting LP tokens adds liquidity to the exchange pool for the trading pair USDT_USDC
.
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 interface endpoint: wss://gateway.prod.vertexprotocol.com/v1/subscribe
.
The websocket interface of vertex needs to enable compression, so when using the Dial function to create a websocket connection, we need to specify: enableCompression=true
.
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")
}
Because the private interface needs to verify the signature, a function exchange.IO("signature", strPayload)
is added to calculate the signature.
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 spot exchange object-specific function switches:
Margin Spot
.Margin Spot
.FMZ not only encapsulates the vertex perpetual contract trading market, but also the vertex spot trading market. The encapsulation of vertex spot is consistent with the vertex contract, and it can be used by adding the vertex spot exchange object on the FMZ platform.
exchange.IO("trade_margin")
to start.Next, we use less than 40 lines of code to design a simple strategy on the FMZ platform. The fewer parameters, the more universal it is. The strategy uses the “Plot Library” to draw graphs.
Strategy code:
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) // To use the plot library, you need to check "Plot Library" in the strategy template column to draw
LogStatus(_D(), ", initPrice:", initPrice, ", rate:", rate, "\n", pos, "\n", assets)
Sleep(1000 * 60)
}
}
Strategy parameters:
Strategy backtesting:
The default position on the vertex contract is unidirection position, so let’s go to the live trading directly to try it out.
The strategy opened a position by itself in the middle of the night, and when I looked in the morning I saw that it had already made a profit!
The strategy is for teaching, testing and learning only, and it should be used with caution in live trading.
The above tests are based on the latest docker. You need to download the latest docker to support the Vertex DEX aggregator.
Thank you for your support and thank you for reading.