输入/搜索内容
内置函数
结构体
内置变量与常量
通用约定
交易所
能力矩阵
中心化交易所
去中心化交易所
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打新策略
预测市场
证券与期货
链上与自定义接入

使用Jupiter原生限价单在现价上下各挂若干档;某张单成交后,在相邻一格挂反向单,赚取一格价差;价格离开范围时撤销全部挂单,并以新价格为中心重新布设网格。

做法
  1. 布设网格:买单挂在现价 × (1 − k × STEP_PCT%),卖单挂在现价 × (1 + k × STEP_PCT%),k = 1…LEVELS。使用exchange.CreateOrder(交易对, 方向, 价格, 数量, {tif: "gtc"})下单,价格到达后由Jupiter的keeper执行成交,策略无需持续盯盘。
  2. 每INTERVAL秒检查一次:对不在exchange.GetOrders返回结果中的挂单,用exchange.GetOrder查询状态;已成交的,在相邻一格挂反向单补位(买单成交 → 高一格挂卖单,卖单成交 → 低一格挂买单)。
  3. 中间价偏离网格中心超过±BAND_PCT%时,撤销全部挂单,并以新价格为中心重新布设网格。
  4. 挂单ID保存在_G中,重启后可继续跟踪;状态栏列出当前挂单,以及已成交部分按现价估算的盈亏。

每张单的金额ORDER_USD必须满足Jupiter的最低金额要求:Trigger v1至少5美元,V2至少10美元。启动时通过exchange.IO("trigger.api")读取接口模式,若不是v1,则按10美元检查(预留5%余量)。若配置了Jupiter API Key且计价币为USDC或USDT,默认使用托管的V2,详见exchange.IO("trigger.*", ...)。

交易所对象

一个Jupiter交易所对象(使用专用钱包)。钱包中需存放基础币(用于卖单)、计价币(用于买单),以及少量SOL用于支付手续费。

参数
参数默认值说明
SYMBOLSOL_USDC交易对
LEVELS3每边挂单档数
STEP_PCT1格距(%)
ORDER_USD12每张单的金额(以计价币计,约等于美元)
BAND_PCT5价格偏离中心超过多少%时重新布设网格
EXPIRY_HOURS0挂单有效期(小时);为0时v1不过期,V2使用默认的30天
INTERVAL60检查间隔(秒)
DRY_RUNtrue为true时仅模拟,不实际挂单

风险
  • 单边行情中,网格会持续买入(下跌时)或卖光(上涨时),重新居中会使浮亏兑现。
  • v1限价单按兑换比率成交;V2在美元价格触发后以市价兑换,成交价不作保证;V2的资金存放在Jupiter托管的vault中。
  • 每次挂单、撤单都是一笔Solana交易,需支付网络费和优先费。

DRY_RUN与实盘

DRY_RUN为true(默认)时不实际挂单:每张「将要挂出」的单用trigger.simulate模拟挂单交易(钱包余额不足时模拟不通过属正常现象,reason会给出原因);纸面挂单根据行情是否穿价判定成交(买单:卖一 ≤ 挂单价;卖单:买一 ≥ 挂单价)。将DRY_RUN改为false即为实盘,实盘挂单会占用钱包中的币。

示例

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

javascript
/* * Jupiter 原生限价单网格(Solana,范例策略) * * 用 Jupiter 的原生限价单(exchange.CreateOrder(…, {tif: "gtc"}),由 Jupiter 的 keeper 在价格到达后成交, * 不用一直盯盘)在现价上下各挂 LEVELS 档:买单在 现价 × (1 − k × STEP_PCT%),卖单在 现价 × (1 + k × STEP_PCT%)。 * - 某张单成交后,在相邻一格挂反向单补上(买单成交 → 高一格挂卖,卖单成交 → 低一格挂买),赚一格价差; * - 价格离开中心 ±BAND_PCT% 的范围时,撤掉全部挂单,以新价格为中心重新布网格; * - 每张单的金额 ORDER_USD 必须满足 Jupiter 的最低金额:Trigger v1 至少 5 美元,V2 至少 10 美元 * (配置了 Jupiter API Key 且计价币为 USDC / USDT 时默认走托管的 V2,见交易所文档「限价单」)。 * 挂单 ID 存进 _G,重启后接着跟踪。 * * DRY_RUN = true(默认)不挂单:每张"会挂的"单用 IO("trigger.simulate", …) 模拟挂单交易(钱包余额不够时 * 模拟报错,属正常),纸面挂单按行情穿价判定成交(买单:卖一 ≤ 挂单价;卖单:买一 ≥ 挂单价)。 * * 风险:单边行情里网格会一路买入(下跌)或卖光(上涨),重新居中会把浮亏兑现;v1 限价单按兑换比率成交, * V2 按美元价触发后市价兑换、成交价不保证;V2 资金存放在 Jupiter 托管的 vault;挂单、撤单各是一笔 * Solana 交易,要付网络费与优先费。 * * 用法:添加 Jupiter 交易所对象(专用钱包),钱包里放基础币(卖单)、计价币(买单)和少量 SOL 付手续费。 */ // ---------------- 参数 ---------------- var SYMBOL = "SOL_USDC" // 交易对 var LEVELS = 3 // 每边挂几档 var STEP_PCT = 1 // 格距(%) var ORDER_USD = 12 // 每张单的金额(计价币,约等于美元) var BAND_PCT = 5 // 价格离开中心多少 % 时重新布网格 var EXPIRY_HOURS = 0 // 挂单有效期(小时);0 = v1 不过期、V2 用默认 30 天 var INTERVAL = 60 // 检查间隔(秒) var DRY_RUN = true // true 只模拟,不挂单 var KEY = DRY_RUN ? "jup_grid_paper" : "jup_grid" var G = null // {center, seq, orders: {id: {side, price, amount}}, buys, sells, spent, received, bought, sold} function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } function roundPx(p) { return Number(p.toPrecision(6)) } function place(side, price) { price = roundPx(price) var amount = Math.ceil(ORDER_USD / price * 1e6) / 1e6 var opts = { tif: "gtc" } if (EXPIRY_HOURS > 0) opts.expiry = Date.now() + EXPIRY_HOURS * 3600000 var id if (DRY_RUN) { var p = { symbol: SYMBOL, side: side, price: price, amount: amount } if (opts.expiry) p.expiry = opts.expiry var sim = exchange.IO("trigger.simulate", p) Log("挂单", side, amount, "@", price, sim ? (sim.ok ? "模拟通过" : "模拟未通过:" + sim.reason) : "模拟出错:" + GetLastError()) id = "paper-" + (++G.seq) } else { id = exchange.CreateOrder(SYMBOL, side, price, amount, opts) if (!id) { Log("挂单失败", side, amount, "@", price, GetLastError(), "#FF0000"); return } Log("挂单", side, amount, "@", price, id) } G.orders[id] = { side: side, price: price, amount: amount } } function buildGrid(price) { G.center = price for (var k = 1; k <= LEVELS; k++) { place("buy", price * (1 - k * STEP_PCT / 100)) place("sell", price * (1 + k * STEP_PCT / 100)) } _G(KEY, G) } function cancelAll() { for (var id in G.orders) { if (!DRY_RUN && !exchange.CancelOrder(id)) Log("撤单未成功", id, GetLastError()) } G.orders = {} } // 找出已成交的单:返回 [id] function filledIds(t) { var out = [] if (DRY_RUN) { for (var id in G.orders) { var o = G.orders[id] if ((o.side === "buy" && t.Sell <= o.price) || (o.side === "sell" && t.Buy >= o.price)) out.push(id) } return out } var open = exchange.GetOrders(SYMBOL) if (!open) throw new Error("GetOrders failed: " + GetLastError()) var openIds = open.map(function (o) { return o.Id }) for (var oid in G.orders) { if (openIds.indexOf(oid) >= 0) continue var r = exchange.GetOrder(oid) if (!r) continue // 暂时查不到,下轮再看 if (r.Status === ORDER_STATE_CLOSED) out.push(oid) else if (r.Status === ORDER_STATE_CANCELED) { Log("订单已撤销", oid); delete G.orders[oid] } } return out } function onFill(id) { var o = G.orders[id] delete G.orders[id] Log("成交", o.side, o.amount, "@", o.price, "#00AA00") if (o.side === "buy") { G.buys++; G.spent += o.amount * o.price; G.bought += o.amount place("sell", o.price * (1 + STEP_PCT / 100)) } else { G.sells++; G.received += o.amount * o.price; G.sold += o.amount place("buy", o.price * (1 - STEP_PCT / 100)) } } function main() { if (exchange.GetName() !== "Jupiter") throw new Error("请添加 Jupiter 交易所对象,当前是 " + exchange.GetName()) var mode = exchange.IO("trigger.api") var minUsd = mode === "v1" ? 5 : 10 // auto 可能走 V2,按 10 美元算 Log("限价单接口:", mode, ",每张单最低", minUsd, "美元") if (ORDER_USD < minUsd * 1.05) throw new Error("ORDER_USD 至少 " + (minUsd * 1.05) + "(最低 " + minUsd + " 美元,留 5% 余量)") G = _G(KEY) if (!G || G.symbol !== SYMBOL) G = { symbol: SYMBOL, center: 0, seq: 0, orders: {}, buys: 0, sells: 0, spent: 0, received: 0, bought: 0, sold: 0 } else Log("从 _G 恢复", Object.keys(G.orders).length, "张挂单,中心价", G.center) while (true) { try { var t = exchange.GetTicker(SYMBOL) if (!t) throw new Error("GetTicker failed: " + GetLastError()) var mid = (t.Buy + t.Sell) / 2 if (!G.center) { buildGrid(mid) } else if (Math.abs(mid / G.center - 1) * 100 > BAND_PCT) { Log("价格", fmt(mid, 4), "离开中心", fmt(G.center, 4), "±" + BAND_PCT + "%,撤单并重新布网格", "#FF9900") cancelAll() buildGrid(mid) } else { filledIds(t).forEach(onFill) } _G(KEY, G) var rows = Object.keys(G.orders).map(function (id) { var o = G.orders[id] return [id.length > 24 ? id.slice(0, 12) + "…" + id.slice(-8) : id, o.side === "buy" ? "买" : "卖", o.price, o.amount, fmt((o.price / mid - 1) * 100, 2) + "%"] }).sort(function (a, b) { return b[2] - a[2] }) var pnl = G.received - G.spent + (G.bought - G.sold) * mid LogStatus(_D(), SYMBOL, "现价", fmt(mid, 4), "中心", fmt(G.center, 4), "重新布网格边界 ±" + BAND_PCT + "%", "\n成交 买", G.buys, "卖", G.sells, ",已成交部分按现价估值盈亏", fmt(pnl, 4), DRY_RUN ? "(DRY_RUN 纸面挂单)" : "", "\n`" + JSON.stringify({ type: "table", title: "挂单", cols: ["订单", "方向", "价格", "数量", "距现价"], rows: rows }) + "`") } catch (e) { Log("本轮出错:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

参考