输入/搜索内容
内置函数
结构体
内置变量与常量
通用约定
交易所
能力矩阵
中心化交易所
去中心化交易所
Hyperliquid
Lighter
edgeX
Aster
Aevo
Pendle
WOOFi Pro
Uniswap / PancakeSwap
Aerodrome / Velodrome
KyberSwap
1inch
EVM DEX通用指令
Jupiter
OKX DEX
DEX策略示例
LP自动调仓
Gauge质押复投
DEX与CEX搬砖
Jupiter限价单网格
DEX定投
pump.fun打新策略
预测市场
证券与期货
链上与自定义接入

对同一币种在链上DEX与中心化交易所(CEX)现货之间的价差进行套利:按每笔数量分别获取两边的可成交价格,扣除手续费、gas和安全垫后,若净利达到门槛,则在两边各执行一笔交易。

做法
  1. 获取价格:DEX端使用exchange.IO("route")按每笔数量QTY询价(买入、卖出各询价一次,取最优路径,报价已包含池子手续费与价格冲击);CEX端使用exchange.GetDepth获取盘口深度,按QTY逐档模拟吃单并计算成交均价。
  2. 分别计算两个方向的净利:DEX买入 → CEX卖出 = CEX卖出所得 × (1 − CEX_FEE) − DEX买入花费 − DEX_GAS_USD;CEX买入 → DEX卖出 = DEX卖出所得 − CEX买入花费 × (1 + CEX_FEE) − DEX_GAS_USD。
  3. 当净利(以bps计,相对于成交额)扣除安全垫SAFETY_BPS后仍不低于MIN_NET_BPS,且不在冷却期内、两边库存充足、基础币持仓不超过MAX_BASE时执行交易:先执行DEX腿(链上交易可能失败或产生滑点,限价让出SLIPPAGE),通过exchange.GetOrder等待兑换交易上链并确认成交量,再按该成交量在CEX下限价单。
  4. 两边均需预先备好基础币和计价币,策略本身不执行转账;当库存集中到一边后,只能等待反向套利机会,任一边库存不足时不执行交易。
  5. 状态栏:显示两个方向的DEX价格、CEX成交均价、毛价差、净利、判断结论与DEX路径,以及两边库存。

交易所对象

需配置两个交易所对象,顺序如下:exchanges[0]为DEX(Uniswap / PancakeSwap、Aerodrome / Velodrome、Jupiter等),exchanges[1]为CEX现货(如Binance)。两边的计价币最好保持一致(默认均为ETH_USDC),否则还需承担稳定币脱锚的风险。

参数
参数默认值说明
DEX_SYMBOLETH_USDCDEX交易对
CEX_SYMBOLETH_USDCCEX交易对(须为同一币种,计价币最好相同)
QTY0.01每笔交易数量(以基础币计)
DEX_GAS_USD0.05DEX每笔兑换的gas费用(美元):Base、Arbitrum约0.01~0.1,Solana约0.001,以太坊主网约2~10
CEX_FEE0.001CEX吃单(Taker)手续费率
SAFETY_BPS10安全垫(bps):用于覆盖从报价到成交期间的价格变动
MIN_NET_BPS20扣除安全垫后的净利门槛(bps)
SLIPPAGE0.003下单价格保护:DEX限价与CEX限价各让出0.3%
MAX_BASE0.1每边基础币持仓上限
COOLDOWN_SEC60每次搬砖后的冷却时间(秒)
INTERVAL15检查间隔(秒)
PAPER_BASE0.05DRY_RUN纸面库存:每边的初始基础币数量
PAPER_QUOTE200DRY_RUN纸面库存:每边的初始计价币数量
DRY_RUNtrue为true时仅记录和模拟,不实际下单

风险
  • 两条腿并非原子执行:DEX腿成交后价格可能已发生变化,导致CEX腿以更差的价格成交;若CEX腿下单失败,将留下单边敞口(日志会标红提示,需人工处理)。
  • 链上交易可能遭遇抢跑或三明治攻击(MEV),也可能回滚(仅扣除gas),且在等待上链期间价格同样会变动。
  • 价差通常很小,在gas较高的链(如以太坊主网)上基本没有套利机会。

DRY_RUN与实盘

当DRY_RUN为true(默认)时不实际下单:仅记录“将会执行”的交易,DEX腿通过exchange.IO("simulate")进行模拟(Jupiter不支持simulate,仅查看询价结果;钱包余额不足时模拟会报错,属正常现象),库存按PAPER_BASE、PAPER_QUOTE记录纸面账目。将DRY_RUN改为false即切换为实盘,需先在两边备好库存,QTY建议从小额开始。

示例

完整策略代码(DRY_RUN = true 时仅记录并模拟,不实际下单)。

javascript
/* * DEX ↔ CEX 现货搬砖(范例策略) * * 同一个币在链上 DEX 与中心化交易所现货之间的价差套利:按每笔交易量 QTY 取两边的可成交价—— * DEX:exchange.IO("route", 交易对, "buy"/"sell", QTY) 的最优路径报价(已含池子手续费与价格冲击); * CEX:GetDepth 盘口按 QTY 逐档吃单的均价; * 两个方向分别算净利: * DEX 买 → CEX 卖:CEX 卖出所得 × (1 − CEX_FEE) − DEX 买入花费 − DEX_GAS_USD * CEX 买 → DEX 卖:DEX 卖出所得 − CEX 买入花费 × (1 + CEX_FEE) − DEX_GAS_USD * 净利(bps,相对成交额)扣掉安全垫 SAFETY_BPS 后仍不低于 MIN_NET_BPS 时,两边各做一笔:先做 DEX 腿 * (链上可能失败或滑点),确认成交后再按成交量做 CEX 腿。两边都要事先备好币和计价币(不转账,库存 * 搬到一边以后只能等反向机会),每边的基础币持仓不超过 MAX_BASE,单边不够就不做。 * * DRY_RUN = true(默认)不下单:记录"会做的"交易,DEX 腿用 IO("simulate", …) 模拟(Jupiter 没有 simulate, * 只看询价;钱包余额不够时模拟报错,属正常),库存按 PAPER_* 起始值记纸面账。 * * 风险:两条腿不是原子的,DEX 腿成交后价格可能已变,CEX 腿以更差的价格成交;链上交易可能被抢跑(MEV)、 * 回滚(只扣 gas)或等待上链期间价格变化;CEX 和 DEX 的计价币不同(如 USDT 与 USDC)时还有脱锚风险, * 默认两边用同一个计价币。价差通常很小,gas 高的链(以太坊主网)基本没有机会。 * * 用法:exchanges[0] 添加 DEX(Uniswap、PancakeSwap、Aerodrome、Velodrome、Jupiter 等),exchanges[1] * 添加 CEX 现货(如 Binance);两边都放好基础币和计价币。 */ // ---------------- 参数 ---------------- var DEX_SYMBOL = "ETH_USDC" // DEX 交易对 var CEX_SYMBOL = "ETH_USDC" // CEX 交易对(同一个币;计价币最好相同) var QTY = 0.01 // 每笔数量(基础币) var DEX_GAS_USD = 0.05 // DEX 每笔兑换的 gas(美元):Base/Arbitrum 约 0.01~0.1,Solana 约 0.001,以太坊主网 2~10 var CEX_FEE = 0.001 // CEX 吃单手续费率 var SAFETY_BPS = 10 // 安全垫(bps):报价到成交之间的价格变动 var MIN_NET_BPS = 20 // 扣安全垫后的净利门槛(bps) var SLIPPAGE = 0.003 // 下单价格保护:DEX 限价与 CEX 限价各让出 0.3% var MAX_BASE = 0.1 // 每边基础币持仓上限 var COOLDOWN_SEC = 60 // 每次搬砖后的冷却时间 var INTERVAL = 15 // 检查间隔(秒) var PAPER_BASE = 0.05 // DRY_RUN 纸面库存:每边的起始基础币 var PAPER_QUOTE = 200 // DRY_RUN 纸面库存:每边的起始计价币 var DRY_RUN = true // true 只记录与模拟,不下单 var dex, cex var paper = null // DRY_RUN 纸面库存 {dex: {base, quote}, cex: {base, quote}} var stats = _G("arb_stats") || { trades: 0, pnl: 0 } var lastTrade = 0 var cexDecimals = 4 // CEX 数量精度(启动时从 GetMarkets 读) function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } // 盘口按数量逐档吃:返回 {avg, worst},深度不够返回 null function walk(levels, qty) { var left = qty, cost = 0 for (var i = 0; i < levels.length && left > 1e-12; i++) { var q = Math.min(left, levels[i].Amount) cost += q * levels[i].Price left -= q if (left <= 1e-12) return { avg: cost / qty, worst: levels[i].Price } } return null } function inventory() { if (DRY_RUN) return paper var a = dex.GetAccount(), b = cex.GetAccount() if (!a || !b) throw new Error("GetAccount failed: " + GetLastError()) return { dex: { base: a.Stocks, quote: a.Balance }, cex: { base: b.Stocks, quote: b.Balance } } } // 等 DEX 兑换上链,返回成交的基础币数量(失败返回 0) function dexFill(id) { for (var i = 0; i < 60; i++) { var o = dex.GetOrder(id) if (o && o.Status === ORDER_STATE_CLOSED) return o.DealAmount if (o && o.Status === ORDER_STATE_CANCELED) return 0 Sleep(2000) } Log("DEX 订单", id, "两分钟未确认,不做 CEX 腿,请人工检查", "#FF0000") return 0 } function execute(op, inv) { var dexSide = op.dir === "A" ? "buy" : "sell", cexSide = op.dir === "A" ? "sell" : "buy" var dexPx = op.dexPrice * (dexSide === "buy" ? 1 + SLIPPAGE : 1 - SLIPPAGE) var cexPx = op.cexWorst * (cexSide === "buy" ? 1 + SLIPPAGE : 1 - SLIPPAGE) Log("搬砖", op.name, "数量", QTY, "DEX", dexSide, fmt(op.dexPrice, 4), "CEX", cexSide, fmt(op.cexAvg, 4), "预计净利", fmt(op.net, 4), "(" + fmt(op.netBps, 1) + " bps)", "#0000FF") lastTrade = Date.now() if (DRY_RUN) { if (dex.GetName() !== "Jupiter") { var sim = dex.IO("simulate", DEX_SYMBOL, dexSide, QTY, dexPx) Log(sim ? "DEX simulate 通过:" + sim.route : "DEX simulate 未通过(钱包余额不够时正常):" + GetLastError()) } var sign = op.dir === "A" ? 1 : -1 inv.dex.base += sign * QTY; inv.dex.quote -= sign * op.dexPrice * QTY inv.cex.base -= sign * QTY; inv.cex.quote += sign * op.cexAvg * QTY * (1 - sign * CEX_FEE) stats.trades++; stats.pnl += op.net return } var id = dex.CreateOrder(DEX_SYMBOL, dexSide, dexPx, QTY) if (!id) { Log("DEX 下单失败(不做 CEX 腿):", GetLastError(), "#FF0000"); return } var filled = dexFill(id) if (filled <= 0) { Log("DEX 腿未成交,不做 CEX 腿"); return } var p = Math.pow(10, cexDecimals) var cid = cex.CreateOrder(CEX_SYMBOL, cexSide, cexPx, Math.floor(filled * p) / p) if (!cid) { Log("CEX 腿下单失败,单边敞口", filled, "请人工处理:", GetLastError(), "#FF0000"); return } stats.trades++; stats.pnl += op.net * filled / QTY } function scan() { var buy = dex.IO("route", DEX_SYMBOL, "buy", QTY) var sell = dex.IO("route", DEX_SYMBOL, "sell", QTY) var depth = cex.GetDepth(CEX_SYMBOL) if (!buy || !sell || !depth) throw new Error("取价失败:" + GetLastError()) var bid = walk(depth.Bids, QTY), ask = walk(depth.Asks, QTY) var ops = [] if (buy.price && bid) { // A:DEX 买 → CEX 卖 var netA = bid.avg * QTY * (1 - CEX_FEE) - buy.price * QTY - DEX_GAS_USD ops.push({ dir: "A", name: "DEX 买 → CEX 卖", dexPrice: buy.price, route: buy.best || buy.route, cexAvg: bid.avg, cexWorst: bid.worst, net: netA, netBps: 0 }) } if (sell.price && ask) { // B:CEX 买 → DEX 卖 var netB = sell.price * QTY - ask.avg * QTY * (1 + CEX_FEE) - DEX_GAS_USD ops.push({ dir: "B", name: "CEX 买 → DEX 卖", dexPrice: sell.price, route: sell.best || sell.route, cexAvg: ask.avg, cexWorst: ask.worst, net: netB, netBps: 0 }) } var inv = inventory() var rows = ops.map(function (op) { op.netBps = op.net / (op.cexAvg * QTY) * 10000 var gross = (op.dir === "A" ? op.cexAvg / op.dexPrice - 1 : op.dexPrice / op.cexAvg - 1) * 10000 var block = "" if (op.netBps - SAFETY_BPS < MIN_NET_BPS) block = "未达门槛" else if (Date.now() - lastTrade < COOLDOWN_SEC * 1000) block = "冷却中" else if (op.dir === "A" && (inv.dex.quote < op.dexPrice * QTY * (1 + SLIPPAGE) || inv.cex.base < QTY || inv.dex.base + QTY > MAX_BASE)) block = "库存不足/超上限" else if (op.dir === "B" && (inv.cex.quote < op.cexAvg * QTY * (1 + CEX_FEE + SLIPPAGE) || inv.dex.base < QTY || inv.cex.base + QTY > MAX_BASE)) block = "库存不足/超上限" if (!block) { execute(op, inv); block = DRY_RUN ? "已记录(DRY_RUN)" : "已执行" } return [op.name, fmt(op.dexPrice, 4), fmt(op.cexAvg, 4), fmt(gross, 1), fmt(op.net, 4), fmt(op.netBps, 1), block, Array.isArray(op.route) ? op.route.join("+") : op.route] }) _G("arb_stats", stats) var t1 = { type: "table", title: "价差(每笔 " + QTY + ",DEX 报价含池子手续费,净利扣 CEX 手续费与 gas " + DEX_GAS_USD + ")", cols: ["方向", "DEX 价", "CEX 均价", "毛价差 bps", "净利", "净利 bps", "结论", "DEX 路径"], rows: rows } var t2 = { type: "table", title: "库存" + (DRY_RUN ? "(DRY_RUN 纸面)" : ""), cols: ["", "基础币", "计价币"], rows: [ [dex.GetName() + " " + DEX_SYMBOL, fmt(inv.dex.base, 6), fmt(inv.dex.quote, 4)], [cex.GetName() + " " + CEX_SYMBOL, fmt(inv.cex.base, 6), fmt(inv.cex.quote, 4)]] } LogStatus(_D(), "搬砖次数", stats.trades, "累计预计净利", fmt(stats.pnl, 4), "\n`" + JSON.stringify([t1, t2]) + "`") } function main() { if (exchanges.length < 2) throw new Error("需要两个交易所对象:exchanges[0] 为 DEX,exchanges[1] 为 CEX 现货") dex = exchanges[0]; cex = exchanges[1] dex.SetCurrency(DEX_SYMBOL); cex.SetCurrency(CEX_SYMBOL) var m = (cex.GetMarkets() || {})[CEX_SYMBOL] if (m) cexDecimals = m.AmountPrecision if (DRY_RUN) paper = { dex: { base: PAPER_BASE, quote: PAPER_QUOTE }, cex: { base: PAPER_BASE, quote: PAPER_QUOTE } } Log("DEX", dex.GetName(), DEX_SYMBOL, "CEX", cex.GetName(), CEX_SYMBOL, DRY_RUN ? "DRY_RUN:只记录不下单" : "实盘") while (true) { try { scan() } catch (e) { Log("本轮出错:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

参考