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

在现价±RANGE_PCT%范围内开设单区间仓位赚取手续费;价格连续多次处于区间外时撤出,并以新价格为中心重新建仓,持续做市。定期领取手续费,并设有调仓频率、gas与资金上限护栏。

做法
  1. 建仓:exchange.IO("lp.plan")(shape为spot、range为{pct: [-RANGE_PCT, RANGE_PCT]})→ exchange.IO("lp.add")。首次建仓按lp.select的规则选池(可通过FEE指定费率),之后的调仓始终使用同一个池子。仓位ID保存在_G中,重启后通过exchange.IO("lp.track")重新登记。
  2. 每隔INTERVAL秒通过exchange.IO("lp.positions")查询仓位:是否处于区间内、当前价值及未领取的手续费。
  3. 连续OUT_CONFIRM次处于区间外时执行调仓:先用exchange.IO("lp.remove")全部撤出(lp.remove(ids, 1, false),不卖回计价币),再按min(仓位现值, BUDGET)以新价格为中心重新规划并建仓,超出BUDGET的部分留在钱包中。
  4. 护栏:两次调仓至少间隔MIN_REBALANCE_MIN分钟,24小时内最多调仓MAX_REBALANCE_PER_DAY次(达到上限后仅监控);新区间的预估日手续费(计划中的estDailyFeesUsd)×PAYBACK_DAYS须能覆盖一次调仓的gas(REBALANCE_TXS×GAS_USD),否则本次不调仓,继续等待。
  5. 每隔COLLECT_HOURS小时,且未领取手续费不少于COLLECT_MIN_USD时,通过exchange.IO("lp.collect")领取。
  6. 状态栏:显示区间、现价、仓位价值、未领取与已领取手续费、调仓次数、gas估计,以及相对于持币不动(HODL:首次建仓时存入的两种代币始终不动)的盈亏。

与「建仓 → 监控 → 出区间或止损即撤出离场」的做法不同,本策略在价格出区间后会跟随价格重新居中,持续做市;离场型策略的简单版本见exchange.IO("lp.add")页面中的完整LP策略示例。

交易所对象

一个Uniswap / PancakeSwap或Aerodrome / Velodrome交易所对象(使用专用钱包),钱包中存放计价币以及少量用于支付gas的原生币。Aerodrome、Velodrome的仓位不做质押,仅赚取交易手续费;质押领取奖励的做法见Gauge质押复投。

参数
参数默认值说明
SYMBOLETH_USDC交易对,格式为基础币_计价币
BUDGET100资金上限(计价币):每次建仓的预算 = min(仓位现值, BUDGET)
RANGE_PCT5区间半宽:现价±5%
FEEnull指定费率(如500);为null时按lp.select规则选池
INTERVAL60检查间隔(秒)
OUT_CONFIRM5连续多少次处于区间外才触发调仓
MIN_REBALANCE_MIN60两次调仓的最小间隔(分钟)
MAX_REBALANCE_PER_DAY424小时内最多调仓次数
GAS_USD0.3每笔链上交易的gas估计(美元):Base、Arbitrum约0.01~0.3,以太坊主网约2~10
REBALANCE_TXS3一次调仓的交易笔数:撤出、换币、建仓
PAYBACK_DAYS3新区间的预估手续费需在多少天内覆盖调仓gas
COLLECT_HOURS24领取手续费的间隔(小时)
COLLECT_MIN_USD1未领取手续费低于此值时不领取
DRY_RUNtrue为true时仅规划与模拟,不发送交易

风险
  • 无常损失:价格偏离建仓价越远,仓位价值相对于持币不动就越低。每次调仓都发生在价格单边运动之后,撤出时仓位已基本转换为单一代币,重新建仓又需换回一半,频繁调仓会将无常损失兑现为实际亏损。
  • gas:每次调仓和领取都是链上交易,gas成本可能超过手续费收入。请在Base、Arbitrum、BNB Chain等低gas链上运行,并按所在链修改GAS_USD。
  • 预估日手续费是根据过去24小时的成交量与仓位占池比例外推得出的,不代表未来收益。

DRY_RUN与实盘

DRY_RUN为true(默认)时不发送任何交易:使用lp.plan进行规划,使用exchange.IO("lp.simulate")模拟建仓交易(钱包余额不足时模拟会报错,属正常现象),并用一个纸面仓位按V3公式跟踪价值;价格处于区间内时,手续费按计划中的预估日手续费累计(仅为估算)。将DRY_RUN改为false即为实盘,建议先以较小的BUDGET运行一段时间,确认调仓频率与gas成本符合预期。

示例

完整策略代码(DRY_RUN = true 时只做规划与模拟,不发送交易)。

javascript
/* * 集中流动性自动调仓做市(Uniswap / PancakeSwap / Aerodrome / Velodrome 通用,范例策略) * * 在现价 ±RANGE_PCT% 开一个单区间仓位赚手续费;价格连续 OUT_CONFIRM 次检查都在区间外时,撤出并以新价格 * 为中心重新建仓(调仓),定期领取手续费。与 uniswap-lp-monitor.js(建仓 → 监控 → 出区间就撤退离场) * 不同,这里出区间后跟随价格重新居中,持续做市。 * * 护栏: * - 两次调仓至少间隔 MIN_REBALANCE_MIN 分钟,24 小时内最多 MAX_REBALANCE_PER_DAY 次(达到后只监控); * - 成本检查:新区间的预估日手续费(lp.plan 的 estDailyFeesUsd)× PAYBACK_DAYS 要能覆盖一次调仓的 gas * (REBALANCE_TXS 笔 × GAS_USD),否则这次不调、继续等; * - 资金上限:每次建仓的预算 = min(仓位现值, BUDGET),投入的资金不会超过 BUDGET,多出的留在钱包; * - 领手续费:每 COLLECT_HOURS 小时、且未领手续费不少于 COLLECT_MIN_USD 时才领(领取也是一笔交易)。 * * 状态栏:区间、现价、是否在区间内、仓位价值、未领 / 已领手续费、调仓次数、gas 估计、相对持币(HODL)的 * 盈亏。HODL 基准 = 第一次建仓时存入的两种代币数量一直不动。 * * DRY_RUN = true(默认)不发任何交易:用 lp.plan 规划、lp.simulate 模拟建仓交易(钱包余额不够时模拟报错, * 属正常),用一个"纸面仓位"按 V3 公式跟踪价值,手续费按计划的预估日手续费在区间内时累计(只是估算)。 * * 风险:区间流动性有无常损失;每次调仓都在价格单边运动之后"高买低卖"地换币,频繁调仓会把无常损失兑现; * 每次调仓、领取都是链上交易,gas 可能超过手续费收入——请在 Base、Arbitrum、BNB Chain 等低 gas 链上运行, * 按链修改 GAS_USD,预算从小额开始。 * * 用法:添加 Uniswap、PancakeSwap、Aerodrome 或 Velodrome 交易所对象之一(专用钱包),钱包里放计价币和 * 少量原生币付 gas。Aerodrome / Velodrome 的仓位不质押,赚交易手续费(质押领奖励见 aerodrome-gauge-compound.js)。 */ // ---------------- 参数 ---------------- var SYMBOL = "ETH_USDC" // 交易对:基础币_计价币 var BUDGET = 100 // 资金上限(计价币) var RANGE_PCT = 5 // 区间半宽:现价 ±5% var FEE = null // 指定费率(如 500);null = 按 lp.select 规则选池 var INTERVAL = 60 // 检查间隔(秒) var OUT_CONFIRM = 5 // 连续几次在区间外才调仓 var MIN_REBALANCE_MIN = 60 // 两次调仓的最小间隔(分钟) var MAX_REBALANCE_PER_DAY = 4 // 24 小时内最多调仓次数 var GAS_USD = 0.3 // 每笔链上交易的 gas 估计(美元):Base/Arbitrum 约 0.01~0.3,以太坊主网 2~10 var REBALANCE_TXS = 3 // 一次调仓的交易笔数:撤出 + 换币 + 建仓 var PAYBACK_DAYS = 3 // 新区间的预估手续费要在几天内覆盖调仓 gas var COLLECT_HOURS = 24 // 领手续费的间隔(小时) var COLLECT_MIN_USD = 1 // 未领手续费少于这个数不领 var DRY_RUN = true // true 只规划与模拟,不发交易 var DEXES = ["Uniswap", "PancakeSwap", "Aerodrome", "Velodrome"] var KEY = DRY_RUN ? "lp_rebalance_paper" : "lp_rebalance" var S = null // 持久状态(_G) function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } function save() { _G(KEY, S) } // 纸面仓位:按 V3 公式由流动性 L 算两种代币数量(价格 = 计价币/基础币) function paperAmounts(p) { var sp = Math.sqrt(Math.min(Math.max(p, S.lower), S.upper)) return { base: S.L * (1 / sp - 1 / Math.sqrt(S.upper)), quote: S.L * (sp - Math.sqrt(S.lower)) } } function makePlan(budget) { var params = { symbol: SYMBOL, budget: budget, range: { pct: [-RANGE_PCT, RANGE_PCT] }, shape: "spot" } if (S && S.pool) params.pool = S.pool // 调仓留在同一个池子 else if (FEE) params.fee = FEE var plan = exchange.IO("lp.plan", params) if (!plan) throw new Error("lp.plan failed: " + GetLastError()) return plan } // 建仓(首次或调仓),返回是否成功 function open(plan) { var x = plan.positions[0] Log("建仓计划:池子", plan.pool, "费率", plan.fee, "现价", fmt(plan.price, 4), "区间", fmt(x.priceLower, 4), "~", fmt(x.priceUpper, 4), "存入", fmt(x.amountBase, 6), "+", fmt(x.amountQuote, 4), "预估日手续费 $" + fmt(plan.estDailyFeesUsd, 4), plan.swap ? "(先" + (plan.swap.side === "buy" ? "买入 " : "卖出 ") + plan.swap.amount + " " + plan.swap.currency + ")" : "") var ids, r = null, base = x.amountBase, quote = x.amountQuote if (DRY_RUN) { var sim = exchange.IO("lp.simulate", plan) Log(sim ? "lp.simulate 通过,价值 " + fmt(sim.value, 4) : "lp.simulate 未通过(钱包余额不够时正常):" + GetLastError()) ids = ["paper"] S.L = x.amountQuote > 0 ? x.amountQuote / (Math.sqrt(plan.price) - Math.sqrt(x.priceLower)) : x.amountBase / (1 / Math.sqrt(plan.price) - 1 / Math.sqrt(x.priceUpper)) } else { r = exchange.IO("lp.add", plan) // 执行时按最新池价与余额重算 if (!r) { Log("建仓失败:", GetLastError(), "#FF0000"); return false } ids = r.positions.map(function (p) { return String(p.id) }) x = r.positions[0] base = 0; quote = 0 r.positions.forEach(function (p) { base += p.amountBase; quote += p.amountQuote }) exchange.IO("lp.track", ids) Log("建仓完成", r.txHash, "仓位", ids.join(","), "#00AA00") } S.ids = ids; S.pool = (r && r.pool) || plan.pool; S.lower = x.priceLower; S.upper = x.priceUpper S.estDaily = plan.estDailyFeesUsd || 0 S.gasUsd += GAS_USD * (plan.swap ? 2 : 1) if (S.hodlBase === null) { S.hodlBase = base; S.hodlQuote = quote } save() return true } // 当前仓位:{price, inRange, value, fees} function readPosition() { if (DRY_RUN) { var t = exchange.GetTicker(SYMBOL) if (!t) throw new Error("GetTicker failed: " + GetLastError()) var p = (t.Buy + t.Sell) / 2, a = paperAmounts(p), now = Date.now() var inRange = p >= S.lower && p < S.upper if (inRange && S.lastCheck) S.fees += S.estDaily * (now - S.lastCheck) / 86400000 S.lastCheck = now return { price: p, inRange: inRange, value: a.base * p + a.quote, fees: S.fees } } var ps = exchange.IO("lp.positions", SYMBOL) if (!ps) throw new Error("lp.positions failed: " + GetLastError()) ps = ps.filter(function (x) { return !x.closed && S.ids.indexOf(String(x.id)) >= 0 }) if (ps.length === 0) return null var out = { price: ps[0].price, inRange: false, value: 0, fees: 0 } ps.forEach(function (x) { out.value += x.value; out.fees += x.feesValue; if (x.inRange) out.inRange = true }) return out } function rebalance(cur) { var now = Date.now() S.rebalances = S.rebalances.filter(function (t) { return now - t < 86400000 }) if (S.rebalances.length >= MAX_REBALANCE_PER_DAY) return "24 小时内已调仓 " + S.rebalances.length + " 次,暂停调仓" if (S.lastRebalance && now - S.lastRebalance < MIN_REBALANCE_MIN * 60000) return "距上次调仓不足 " + MIN_REBALANCE_MIN + " 分钟" var budget = Math.min(cur.value, BUDGET) // 手续费另计入已领,不再投入 var plan = makePlan(budget) var cost = REBALANCE_TXS * GAS_USD if (plan.estDailyFeesUsd !== null && plan.estDailyFeesUsd * PAYBACK_DAYS < cost) { return "新区间预估 " + PAYBACK_DAYS + " 天手续费 $" + fmt(plan.estDailyFeesUsd * PAYBACK_DAYS, 4) + " 不够调仓 gas $" + fmt(cost, 2) } Log("调仓:价格", fmt(cur.price, 4), "已连续", S.outStreak, "次在区间外", "#FF9900") if (DRY_RUN) { S.collected += S.fees } else { var r = exchange.IO("lp.remove", S.ids, 1, false) if (!r) { Log("撤出失败:", GetLastError(), "#FF0000"); return "撤出失败" } r.positions.forEach(function (p) { S.collected += p.feesBase * cur.price + p.feesQuote }) } S.leftover += cur.value - budget S.fees = 0; S.outStreak = 0; S.lastRebalance = now; S.rebalances.push(now); S.count++ S.gasUsd += GAS_USD S.ids = []; S.budget = budget // 撤出后先清空:重新建仓失败时下一轮接着重试 if (!open(plan)) return "重新建仓失败,下一轮重试" return "已调仓" } function collect(cur) { if (Date.now() - S.lastCollect < COLLECT_HOURS * 3600000 || cur.fees < COLLECT_MIN_USD) return Log("领取手续费 $" + fmt(cur.fees, 4)) if (!DRY_RUN) { var r = exchange.IO("lp.collect", S.ids, false) if (!r) { Log("领取失败:", GetLastError(), "#FF0000"); return } } S.collected += cur.fees S.fees = 0 S.gasUsd += GAS_USD S.lastCollect = Date.now() save() } function main() { if (DEXES.indexOf(exchange.GetName()) < 0) throw new Error("请添加 " + DEXES.join(" / ") + " 交易所对象,当前是 " + exchange.GetName()) S = _G(KEY) if (!S || S.symbol !== SYMBOL || S.dex !== exchange.GetName()) { S = { symbol: SYMBOL, dex: exchange.GetName(), ids: [], budget: BUDGET, count: 0, pool: null, lower: 0, upper: 0, L: 0, estDaily: 0, fees: 0, collected: 0, gasUsd: 0, leftover: 0, hodlBase: null, hodlQuote: null, rebalances: [], lastRebalance: 0, lastCollect: Date.now(), outStreak: 0, lastCheck: 0 } } if (S.ids.length && !DRY_RUN) exchange.IO("lp.track", S.ids) if (S.ids.length) Log("从 _G 恢复仓位", S.ids.join(","), "区间", fmt(S.lower, 4), "~", fmt(S.upper, 4)) var note = "" while (true) { try { if (!S.ids.length && !open(makePlan(S.budget))) throw new Error("建仓失败,下一轮重试") var cur = readPosition() if (!cur) { Log("仓位已不在(手动撤出?),停止", "#FF0000"); _G(KEY, null); return } S.outStreak = cur.inRange ? 0 : S.outStreak + 1 note = cur.inRange ? "" : "区间外 " + S.outStreak + "/" + OUT_CONFIRM if (S.outStreak >= OUT_CONFIRM) note = rebalance(cur) else collect(cur) save() var hodl = S.hodlBase * cur.price + S.hodlQuote var total = cur.value + cur.fees + S.collected + S.leftover - S.gasUsd var day = S.rebalances.filter(function (t) { return Date.now() - t < 86400000 }).length var table = { type: "table", title: SYMBOL + " 自动调仓 LP" + (DRY_RUN ? "(DRY_RUN 纸面仓位)" : ""), cols: ["项目", "值"], rows: [ ["仓位", S.ids.join(",")], ["池子", S.pool], ["区间", fmt(S.lower, 4) + " ~ " + fmt(S.upper, 4)], ["现价", fmt(cur.price, 4)], ["在区间内", cur.inRange ? "是" : (cur.price < S.lower ? "否(下方)" : "否(上方)")], ["仓位价值", fmt(cur.value, 4)], ["未领手续费" + (DRY_RUN ? "(估算)" : ""), fmt(cur.fees, 4)], ["已领手续费", fmt(S.collected, 4)], ["调仓次数(近 24h / 每日上限)", S.count + "(" + day + " / " + MAX_REBALANCE_PER_DAY + ")"], ["gas 估计", fmt(S.gasUsd, 4)], ["LP 合计(含已领、未再投入、扣 gas)", fmt(total, 4)], ["持币不动(HODL)", fmt(hodl, 4)], ["相对 HODL 盈亏", fmt(total - hodl, 4)], ["说明", note || "-"]] } LogStatus(_D(), "\n`" + JSON.stringify(table) + "`") } catch (e) { Log("本轮出错:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

参考