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

在Aerodrome或Velodrome上按奖励年化收益率选择池子,建仓后将仓位质押到该池的gauge中领取AERO或VELO奖励,并定期领取奖励、兑换为USDC后追加到仓位(复投)。

做法
  1. 选池:从exchange.IO("lp.pools")中筛选gauge仍在发放奖励(gaugeAlive)、非空池、TVL不低于MIN_TVL_USD且类型符合POOL_KIND的池子,按奖励年化rewardAprPct从高到低排序后取第一个;前5个候选池会写入日志(含奖励年化及不质押时的手续费年化)。POOL_KIND为slipstream时使用Slipstream集中流动性池(±RANGE_PCT%区间),为basic时使用基础池(全区间)。
  2. 建仓并质押:调用exchange.IO("lp.plan")时传入stake: true,exchange.IO("lp.add")建仓完成后直接将仓位质押到gauge。
  3. 复投:每隔CLAIM_HOURS小时检查一次,当待领奖励不少于MIN_CLAIM_USD美元且超过一轮复投gas成本的两倍时,用exchange.IO("lp.claim")领取奖励并兑换为USDC(lp.claim(ids, true)),待卖单上链确认并拿到所得金额后,再追加到仓位:基础池使用lp.increase(池子, 金额, {stake: true});质押中的Slipstream仓位不能直接加仓,需依次执行exchange.IO("lp.unstake") → exchange.IO("lp.increase") → exchange.IO("lp.stake")。
  4. 状态栏:显示池子的奖励年化与每日排放量(每轮通过exchange.IO("lp.pool")读取)、是否已质押及是否在区间内、仓位价值、待领奖励、累计领取与复投金额、gas估算,以及按实际领取奖励折算的年化收益率。

交易所对象

一个Aerodrome / Velodrome交易所对象:Aerodrome(Base链,奖励AERO)或Velodrome(OP Mainnet,奖励VELO),使用专用钱包,钱包中需存入USDC及少量ETH用于支付gas。

参数
参数默认值说明
SYMBOLETH_USDC交易对
BUDGET100建仓预算(USDC)
POOL_KINDslipstreamslipstream或basic
RANGE_PCT10Slipstream区间半宽(%);质押仓位超出区间后不发放奖励,建议设置得宽一些
MIN_TVL_USD100000仅考虑TVL不低于此值的池子
CLAIM_HOURS24领取并复投的间隔(小时)
MIN_CLAIM_USD2待领奖励低于此值(美元)时不领取
GAS_USD0.05每笔链上交易的gas估算(美元)
COMPOUND_TXS5每轮复投的交易笔数:领取、卖出、解除质押、加仓、质押
INTERVAL300检查间隔(秒)
DRY_RUNtrue为true时只规划与模拟,不发送交易

风险
  • 奖励年化rewardAprPct是按当前每秒排放量外推的瞬时值,会随奖励代币价格、排放量和质押总量而变化;Slipstream池子按当前价格所在tick上的质押流动性计算,价格一变动就会跳变,几分钟内可能相差数倍,选池时最好间隔一段时间多观察几次。AERO、VELO价格波动较大,卖出时机会影响实际收益。
  • 质押期间交易手续费归投票人所有,仓位只获得奖励;Slipstream仓位超出区间后不发放奖励,同时存在无常损失;基础池为全区间,无常损失按恒定乘积曲线计算。
  • 每次领取、卖出、解除质押、加仓、质押都是链上交易(每轮复投约COMPOUND_TXS笔),奖励过少时支付gas并不划算。

DRY_RUN与实盘

DRY_RUN为true(默认)时不发送任何交易:用exchange.IO("lp.simulate")模拟建仓(钱包余额不足时会报错,属正常现象),纸面仓位按池子当前的rewardAprPct累计奖励(假设始终处于区间内;每轮取一次当前值,相当于按时间加权平均),到期后用exchange.IO("route")询价,将奖励兑换为USDC的可得数量计入纸面本金。将DRY_RUN改为false即切换为实盘,并需根据所在链调整GAS_USD。

示例

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

javascript
/* * Aerodrome / Velodrome gauge 质押复投(范例策略) * * Aerodrome(Base)和 Velodrome(OP Mainnet)的流动性仓位可以质押进池子的 gauge,按排放领取 AERO / VELO * 奖励(质押期间交易手续费归投票人,仓位只拿奖励)。本策略: * 1. 在 lp.pools 里按奖励年化(rewardAprPct)挑一个 gauge 还在发奖励、TVL 足够的池子: * POOL_KIND = "slipstream" 用 Slipstream 集中流动性池(±RANGE_PCT% 区间),"basic" 用基础池(全区间); * 2. 建仓并直接质押(lp.plan 的 stake: true → lp.add); * 3. 每 CLAIM_HOURS 小时,奖励价值够了就领取并卖成 USDC(lp.claim(ids, true)),再加回仓位(复投): * Slipstream 质押着的仓位不能加仓,要 lp.unstake → lp.increase → lp.stake;基础池 lp.increase(池子, 金额, {stake: true}); * 4. 状态栏显示池子奖励年化、仓位价值、待领奖励、累计领取与复投金额、按实际领取折算的年化。 * * DRY_RUN = true(默认)不发任何交易:lp.simulate 模拟建仓(钱包余额不够时报错,属正常),用纸面仓位按 * 池子当前 rewardAprPct 累计奖励(假设一直在区间内),到点用 IO("route") 询价把奖励卖成 USDC 的可得数量, * 计入纸面本金(复投)。 * * 风险: * - 奖励年化随奖励币价格、排放量和质押总量变化,rewardAprPct 只是按当前每秒排放外推的瞬时值;Slipstream 池子按 * 现价所在 tick 上的质押流动性算,价格一动就跳,几分钟内能差好几倍(纸面累计每轮取一次,相当于按时间平均); * AERO / VELO 价格波动大,卖出时机影响实际收益; * - Slipstream 仓位出区间后不发奖励,也有无常损失;基础池全区间、无常损失按恒定乘积曲线; * - 每次领取、卖出、取回、加仓、质押都是链上交易(复投一轮约 COMPOUND_TXS 笔),奖励太少时 gas 不划算。 * * 用法:添加 Aerodrome 或 Velodrome 交易所对象(专用钱包),钱包里放 USDC 和少量 ETH 付 gas。 */ // ---------------- 参数 ---------------- var SYMBOL = "ETH_USDC" // 交易对 var BUDGET = 100 // 建仓预算(USDC) var POOL_KIND = "slipstream" // slipstream 或 basic var RANGE_PCT = 10 // Slipstream 区间半宽(%);质押仓位出区间不发奖励,宜宽一些 var MIN_TVL_USD = 100000 // 只考虑 TVL 不低于此值的池子 var CLAIM_HOURS = 24 // 领取 + 复投的间隔(小时) var MIN_CLAIM_USD = 2 // 待领奖励少于这个数不领 var GAS_USD = 0.05 // 每笔链上交易的 gas 估计(美元) var COMPOUND_TXS = 5 // 复投一轮的交易笔数:领取、卖出、取回、加仓、质押 var INTERVAL = 300 // 检查间隔(秒) var DRY_RUN = true // true 只规划与模拟,不发交易 var KEY = DRY_RUN ? "gauge_compound_paper" : "gauge_compound" var S = null var REWARD = null function fmt(x, d) { return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d) } function kindOf(p) { return /^slipstream/.test(p.protocol) ? "slipstream" : "basic" } function rewardPrice() { var t = exchange.GetTicker(REWARD + "_USDC") if (!t) throw new Error("GetTicker " + REWARD + "_USDC failed: " + GetLastError()) return (t.Buy + t.Sell) / 2 } function listPools() { var pair = SYMBOL.split("_") var pools = exchange.IO("lp.pools", pair[0], pair[1]) if (!pools) throw new Error("lp.pools failed: " + GetLastError()) return pools } // 池子当前状态(lp.pool 的 gauge 字段与 lp.pools 同一算法) function poolRow(id) { var row = exchange.IO("lp.pool", id) if (!row) throw new Error("lp.pool failed: " + GetLastError()) return row } // 按奖励年化选池,并在日志列出候选 function choosePool() { var ok = listPools().filter(function (p) { return p.symbol === SYMBOL && p.gaugeAlive && !p.empty && p.tvlUsd >= MIN_TVL_USD && kindOf(p) === POOL_KIND }).sort(function (a, b) { return b.rewardAprPct - a.rewardAprPct }) ok.slice(0, 5).forEach(function (p) { Log("候选", p.protocol, p.id, "tick间距", p.tickSpacing, "TVL $" + fmt(p.tvlUsd, 0), "质押占比", fmt(p.stakedShare * 100, 1) + "%", "奖励年化", fmt(p.rewardAprPct, 2) + "%", "手续费年化(未质押)", fmt(p.fees24hUsd * 365 / p.tvlUsd * 100, 2) + "%") }) if (!ok.length) throw new Error("没有符合条件的 " + POOL_KIND + " 池子(TVL ≥ " + MIN_TVL_USD + "、gauge 正常)") return ok[0] } function enter(pool) { var params = { symbol: SYMBOL, budget: BUDGET, pool: pool.id, stake: true } if (POOL_KIND === "slipstream") { params.range = { pct: [-RANGE_PCT, RANGE_PCT] }; params.shape = "spot" } var plan = exchange.IO("lp.plan", params) if (!plan) throw new Error("lp.plan failed: " + GetLastError()) Log("建仓计划:", plan.protocol, plan.pool, "需要", fmt(plan.needBase, 6), "+", fmt(plan.needQuote, 4), plan.shortfall ? "(" + plan.shortfall + ")" : "") if (DRY_RUN) { var sim = exchange.IO("lp.simulate", plan) Log(sim ? "lp.simulate 通过" : "lp.simulate 未通过(钱包余额不够时正常):" + GetLastError()) return ["paper"] } var r = exchange.IO("lp.add", plan) if (!r) throw new Error("lp.add failed: " + GetLastError()) var ids = r.positions ? r.positions.map(function (p) { return String(p.id) }) : [String(r.id)] Log("建仓并质押完成", r.txHash, "仓位", ids.join(","), r.stake ? "" : "(质押结果缺失,请检查)", "#00AA00") exchange.IO("lp.track", ids) return ids } // 仓位现状:{value, rewards(奖励币数量), rewardsValue, staked, inRange} function readPosition(pool) { if (DRY_RUN) { var now = Date.now() S.paperRewardsUsd += S.principal * (pool.rewardAprPct || 0) / 100 * (now - S.lastCheck) / (365 * 86400000) S.lastCheck = now var px = rewardPrice() return { value: S.principal, rewards: S.paperRewardsUsd / px, rewardsValue: S.paperRewardsUsd, staked: true, inRange: true } } var ps = exchange.IO("lp.positions", SYMBOL) if (!ps) throw new Error("lp.positions failed: " + GetLastError()) var out = { value: 0, rewards: 0, rewardsValue: 0, staked: true, inRange: false } ps.filter(function (p) { return !p.closed && S.ids.indexOf(String(p.id)) >= 0 }).forEach(function (p) { out.value += p.value; out.rewards += p.rewards; out.rewardsValue += p.rewardsValue if (!p.staked) out.staked = false if (p.inRange !== false) out.inRange = true // 基础池没有区间 }) return out } // 等奖励卖单上链,返回到手的 USDC(卖单 = 市价兑换,GetOrder 按回执给出成交) function proceeds(orderId) { for (var i = 0; i < 30; i++) { var o = exchange.GetOrder(orderId) if (o && o.Status === ORDER_STATE_CLOSED) return o.DealAmount * o.AvgPrice if (o && o.Status === ORDER_STATE_CANCELED) return 0 Sleep(2000) } return 0 } // 领取奖励 → 卖成 USDC → 加回仓位 function compound(cur) { Log("领取", fmt(cur.rewards, 4), REWARD, "≈ $" + fmt(cur.rewardsValue, 4), "并复投") var got if (DRY_RUN) { var q = exchange.IO("route", REWARD + "_USDC", "sell", Number(cur.rewards.toFixed(6))) if (!q || q.quote === null) { Log("询价失败:", GetLastError()); return false } got = q.quote Log("DRY_RUN:卖出可得", fmt(got, 4), "USDC(路径", q.best + "),将 lp.unstake → lp.increase → lp.stake 加回仓位") S.paperRewardsUsd = 0 S.principal += got } else { var r = exchange.IO("lp.claim", S.ids, true) if (!r) { Log("领取失败:", GetLastError(), "#FF0000"); return false } S.claimed += r.rewards S.lastClaim = Date.now() if (!r.sell || r.sell.error) { Log("奖励已领取,卖出失败:", r.sell ? r.sell.error : "-", "#FF0000"); return true } got = proceeds(r.sell.txHash) if (got <= 1) { Log("卖出所得", fmt(got, 4), "USDC 太少,不复投"); return true } var ok if (POOL_KIND === "basic") { ok = exchange.IO("lp.increase", S.ids[0], got, { stake: true }) } else { ok = exchange.IO("lp.unstake", S.ids) && exchange.IO("lp.increase", S.ids[0], got) && exchange.IO("lp.stake", S.ids) } if (!ok) { Log("复投未完成:", GetLastError(), "(USDC 留在钱包,仓位可能处于未质押状态)", "#FF0000"); return true } } S.claimedUsd += got S.compounded += got S.gasUsd += COMPOUND_TXS * GAS_USD S.lastClaim = Date.now() return true } function main() { var name = exchange.GetName() if (name !== "Aerodrome" && name !== "Velodrome") throw new Error("请添加 Aerodrome 或 Velodrome 交易所对象,当前是 " + name) REWARD = name === "Aerodrome" ? "AERO" : "VELO" S = _G(KEY) if (!S || S.symbol !== SYMBOL || S.dex !== name) { var pool = choosePool() S = { symbol: SYMBOL, dex: name, pool: pool.id, ids: [], principal: BUDGET, paperRewardsUsd: 0, claimed: 0, claimedUsd: 0, compounded: 0, gasUsd: 0, start: Date.now(), lastClaim: Date.now(), lastCheck: Date.now() } S.ids = enter(pool) S.gasUsd += 3 * GAS_USD _G(KEY, S) } else { Log("从 _G 恢复:池子", S.pool, "仓位", S.ids.join(",")) if (!DRY_RUN) exchange.IO("lp.track", S.ids) } while (true) { try { var pool = poolRow(S.pool) var cur = readPosition(pool) var due = Date.now() - S.lastClaim >= CLAIM_HOURS * 3600000 if (due && cur.rewardsValue >= MIN_CLAIM_USD && cur.rewardsValue > 2 * COMPOUND_TXS * GAS_USD && compound(cur)) { cur.rewards = 0; cur.rewardsValue = 0 } _G(KEY, S) var days = Math.max((Date.now() - S.start) / 86400000, 1e-6) var realized = (S.claimedUsd + cur.rewardsValue) / BUDGET / days * 365 * 100 var table = { type: "table", title: SYMBOL + " gauge 质押复投(" + REWARD + ")" + (DRY_RUN ? "(DRY_RUN 纸面仓位)" : ""), cols: ["项目", "值"], rows: [ ["池子", pool.protocol + " " + S.pool], ["池子奖励年化 rewardAprPct", fmt(pool.rewardAprPct, 2) + "%"], ["每天排放", fmt(pool.rewardsPerDay, 0) + " " + REWARD + "($" + fmt(pool.rewardsPerDayUsd, 0) + ")"], ["仓位", S.ids.join(",")], ["质押中 / 在区间内", (cur.staked ? "是" : "否") + " / " + (cur.inRange ? "是" : "否")], ["仓位价值", fmt(cur.value, 4)], ["待领奖励", fmt(cur.rewards, 4) + " " + REWARD + "($" + fmt(cur.rewardsValue, 4) + ")"], ["累计领取 / 复投(USDC)", fmt(S.claimedUsd, 4) + " / " + fmt(S.compounded, 4)], ["gas 估计", fmt(S.gasUsd, 4)], ["按实际奖励折算年化", fmt(realized, 2) + "%"], ["下次领取", due ? "到期(等奖励 ≥ $" + MIN_CLAIM_USD + ")" : _D(S.lastClaim + CLAIM_HOURS * 3600000)]] } LogStatus(_D(), "\n`" + JSON.stringify(table) + "`") } catch (e) { Log("本轮出错:", e.message || e, "#FF0000") } Sleep(INTERVAL * 1000) } }

参考