DEX定投
定期买入同一币种以摊平持仓成本:在Jupiter上使用原生定投,在其他DEX上由策略定时市价买入;价格低于均线时,当期买入金额加倍。
做法
策略根据交易所对象自动选择执行方式:
- Jupiter且
NATIVE为true:使用exchange.IO("dca.*", ...)的原生定投。dca.create一次性存入总额TOTAL,由Jupiter的keeper每隔INTERVAL_HOURS小时买入一份PER_ORDER,无需策略持续运行;MIN_PRICE、MAX_PRICE限定仅在该价格区间内买入(价格在区间外时,该轮跳过或顺延)。策略启动时先用dca.list查询该交易对是否有进行中的定投,没有时才创建(创建得到的ID同时存入_G,防止重复创建),之后每分钟用dca.list显示执行进度。Recurring v1(未配置API Key时)每份至少50美元、至少2份;Trigger V2(已配置API Key且计价币为USDC或USDT)每份至少10美元,资金存放在Jupiter托管的vault中。 - 其他DEX(Uniswap、PancakeSwap、Aerodrome、Velodrome等),或
NATIVE为false:策略每隔INTERVAL_HOURS小时用市价单(exchange.CreateOrder(交易对, "buy", -1, 金额),市价买单的数量以计价币计)买入PER_ORDER;当卖一价低于MA_DAYS日均线DIP_PCT%时,当期金额乘以DIP_MULT(逢低多买),累计花费不超过TOTAL。通过exchange.GetOrder确认成交后记账,进度存入_G(),重启后可继续执行。
交易所对象
一个DEX交易所对象(建议使用专用钱包),钱包中需存入计价币以及少量原生币用于支付手续费。SYMBOL为null时,使用交易所对象配置的交易对。
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
SYMBOL | null | 交易对;null表示使用交易所对象配置的交易对 |
TOTAL | 500 | 总预算(以计价币计) |
PER_ORDER | 50 | 每份金额(以计价币计) |
INTERVAL_HOURS | 24 | 每份买入的间隔(小时) |
NATIVE | true | 在Jupiter上使用原生定投(dca.create) |
MIN_PRICE | null | 原生定投:价格低于该值时跳过本轮(null表示不限) |
MAX_PRICE | null | 原生定投:价格高于该值时跳过本轮(null表示不限) |
DIP_PCT | 5 | 策略自行买入:价格低于均线该百分比时加倍买入 |
DIP_MULT | 2 | 策略自行买入:逢低买入的倍数 |
MA_DAYS | 20 | 策略自行买入:均线天数 |
DRY_RUN | true | 为true时仅模拟,不实际花费资金 |
风险
- 定投只能摊平买入成本,无法避免下跌带来的亏损。
- 链上每笔交易都需支付gas或网络费用,小额高频买入并不划算。
- 市价单存在滑点(参见
exchange.IO("slippage"))。
DRY_RUN与实盘
DRY_RUN为true(默认)时不实际花费资金:原生定投使用dca.simulate模拟创建交易(reason给出未通过的原因);策略自行买入的方式使用exchange.IO("route")询价并记录纸面成交,EVM DEX还会使用exchange.IO("simulate")模拟兑换交易(钱包余额不足时模拟会报错,属正常现象)。将DRY_RUN改为false即进入实盘。原生定投创建后由Jupiter执行,停止机器人并不会停止定投,如需撤销,请使用dca.cancel。
示例
完整策略代码(DRY_RUN = true:仅模拟运行,不实际花费资金)。
javascript
/*
* DEX 定投(DCA,范例策略)
*
* 两种做法,按交易所对象自动选择:
* 1. Jupiter(Solana)且 NATIVE = true:用 Jupiter 原生定投 exchange.IO("dca.create", …)——总额一次存入,
* 由 Jupiter 的 keeper 每隔 INTERVAL_HOURS 小时买一份,不需要策略一直运行;MIN_PRICE / MAX_PRICE 可限定
* 只在价格区间内买(区间外的那一轮跳过或顺延)。策略只负责创建和用 dca.list 显示进度。
* Recurring v1(没有 API Key 时)每份至少 50 美元、至少 2 份;Trigger V2(有 API Key 且计价币为
* USDC / USDT)每份至少 10 美元,资金存放在 Jupiter 托管的 vault。
* 2. 其它 DEX(Uniswap、PancakeSwap、Aerodrome、Velodrome 等),或 NATIVE = false:策略自己每隔
* INTERVAL_HOURS 小时用市价单买入 PER_ORDER 计价币的币;价格低于 MA_DAYS 日均线 DIP_PCT% 时这一份
* 乘以 DIP_MULT(逢低多买)。累计花费不超过 TOTAL。
* 进度存进 _G,重启后接着来。
*
* DRY_RUN = true(默认)不花钱:原生定投用 IO("dca.simulate", …) 模拟创建交易;自己买的做法用
* IO("route", …) 询价记纸面成交,EVM DEX 另用 IO("simulate", …) 模拟兑换交易(钱包余额不够时模拟报错,属正常)。
*
* 风险:定投只是摊平买入成本,不能避免下跌亏损;链上每笔都有 gas / 网络费,小额高频不划算;
* 市价单有滑点(见 exchange.IO("slippage"))。
*
* 用法:添加一个 DEX 交易所对象(专用钱包),钱包里放计价币和少量原生币付手续费。
*/
// ---------------- 参数 ----------------
var SYMBOL = null // 交易对;null = 交易所对象配置的交易对
var TOTAL = 500 // 总预算(计价币)
var PER_ORDER = 50 // 每份金额(计价币)
var INTERVAL_HOURS = 24 // 每份间隔(小时)
var NATIVE = true // Jupiter 上用原生定投(dca.create)
var MIN_PRICE = null // 原生定投:价格低于它跳过这一轮(null = 不限)
var MAX_PRICE = null // 原生定投:价格高于它跳过这一轮(null = 不限)
var DIP_PCT = 5 // 自己买:价格低于均线这么多 % 时加倍
var DIP_MULT = 2 // 自己买:逢低的倍数
var MA_DAYS = 20 // 自己买:均线天数
var DRY_RUN = true // true 只模拟,不花钱
function fmt(x, d) {
return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d)
}
// ---------------- 做法 1:Jupiter 原生定投 ----------------
function nativeDca() {
var key = "dca_native_" + SYMBOL // 已创建的定投 ID,防止重启后重复创建
var params = { symbol: SYMBOL, side: "buy", total: TOTAL, perOrder: PER_ORDER, interval: INTERVAL_HOURS * 3600 }
if (MIN_PRICE) params.minPrice = MIN_PRICE
if (MAX_PRICE) params.maxPrice = MAX_PRICE
var active = exchange.IO("dca.list", { status: "active", symbol: SYMBOL })
if (!active) throw new Error("dca.list failed: " + GetLastError())
if (active.length === 0 && !_G(key)) {
if (DRY_RUN) {
var sim = exchange.IO("dca.simulate", params)
if (!sim) Log("dca.simulate 出错:", GetLastError())
else Log("dca.simulate(" + sim.version + ")", sim.ok ? "通过" : "未通过:" + sim.reason, "计划:", JSON.stringify(sim.plan))
} else {
var r = exchange.IO("dca.create", params)
if (!r) throw new Error("dca.create failed: " + GetLastError())
Log("已创建定投", r.version, r.id, "每份", r.perOrder, "共", r.numberOfOrders, "份", "#00AA00")
_G(key, r.id)
}
}
while (true) {
try {
var rows = []
;["active", "history"].forEach(function (st) {
var list = exchange.IO("dca.list", { status: st, symbol: SYMBOL })
if (!list) throw new Error("dca.list failed: " + GetLastError())
list.forEach(function (d) {
rows.push([d.id, d.version, d.state, fmt(d.perOrder, 4), (d.ordersFilled === null ? "?" : d.ordersFilled) + " / " + d.numberOfOrders,
fmt(d.spent, 4), fmt(d.filledQty, 6), fmt(d.avgPrice, 4), d.nextFillAt ? _D(d.nextFillAt) : "-"])
})
})
LogStatus(_D(), SYMBOL, "Jupiter 原生定投", DRY_RUN ? "(DRY_RUN:只模拟了创建,下表是钱包里已有的定投)" : "",
"\n`" + JSON.stringify({ type: "table", title: "定投", cols: ["ID", "版本", "状态", "每份", "已买份数", "已花费", "已买到", "均价", "下次"], rows: rows }) + "`")
} catch (e) {
Log("本轮出错:", e.message || e, "#FF0000")
}
Sleep(60000)
}
}
// ---------------- 做法 2:策略自己定时买 ----------------
function buyOnce(D, amount, price) {
var q = exchange.IO("route", SYMBOL, "buy", amount / price) // 只询价:买到这么多要花多少
if (DRY_RUN) {
if (exchange.GetName() !== "Jupiter") {
var sim = exchange.IO("simulate", SYMBOL, "buy", amount) // 市价买:数量为计价币
Log(sim ? "simulate 通过:" + sim.route : "simulate 未通过(钱包余额不够时正常):" + GetLastError())
}
if (!q || !q.price) { Log("询价失败:", GetLastError()); return false }
D.spent += amount; D.bought += amount / q.price
Log("DRY_RUN 买入", fmt(amount, 2), "→", fmt(amount / q.price, 6), "@", fmt(q.price, 4))
return true
}
var id = exchange.CreateOrder(SYMBOL, "buy", -1, amount)
if (!id) { Log("买入失败:", GetLastError(), "#FF0000"); return false }
for (var i = 0; i < 60; i++) {
var o = exchange.GetOrder(id)
if (o && o.Status === ORDER_STATE_CLOSED) {
D.spent += amount; D.bought += o.DealAmount
Log("买入", fmt(amount, 2), "→", o.DealAmount, "@", fmt(o.AvgPrice, 4), "#00AA00")
return true
}
if (o && o.Status === ORDER_STATE_CANCELED) { Log("买单未成交(撤销 / 链上失败)", id, "#FF0000"); return false }
Sleep(2000)
}
Log("买单两分钟未确认,记为已花费,请稍后核对", id, "#FF0000")
D.spent += amount
return true
}
function loopDca() {
var key = DRY_RUN ? "dca_loop_paper" : "dca_loop"
var D = _G(key)
if (!D || D.symbol !== SYMBOL) D = { symbol: SYMBOL, spent: 0, bought: 0, buys: 0, lastBuy: 0 }
while (true) {
try {
var t = exchange.GetTicker(SYMBOL)
if (!t) throw new Error("GetTicker failed: " + GetLastError())
var r = exchange.GetRecords(SYMBOL, PERIOD_D1)
var ma = r && r.length >= MA_DAYS ? TA.MA(r, MA_DAYS)[r.length - 1] : null
var dip = ma !== null && t.Sell < ma * (1 - DIP_PCT / 100)
var due = Date.now() - D.lastBuy >= INTERVAL_HOURS * 3600000
if (due && D.spent < TOTAL - 1e-9) {
var amount = Math.min(PER_ORDER * (dip ? DIP_MULT : 1), TOTAL - D.spent)
Log("定投第", D.buys + 1, "份", fmt(amount, 2), dip ? "(低于均线 " + DIP_PCT + "%,×" + DIP_MULT + ")" : "")
if (buyOnce(D, amount, t.Sell)) { D.buys++; D.lastBuy = Date.now() }
_G(key, D)
}
var avg = D.bought > 0 ? D.spent / D.bought : null
var value = D.bought * t.Buy
var table = { type: "table", title: SYMBOL + " 定投" + (DRY_RUN ? "(DRY_RUN 纸面)" : ""), cols: ["项目", "值"], rows: [
["已买份数", D.buys], ["已花费 / 总预算", fmt(D.spent, 2) + " / " + TOTAL], ["已买到", fmt(D.bought, 6)],
["持仓均价", fmt(avg, 4)], ["现价(买一)", fmt(t.Buy, 4)], [MA_DAYS + " 日均线", fmt(ma, 4) + (dip ? "(逢低)" : "")],
["市值 / 盈亏", fmt(value, 2) + " / " + fmt(value - D.spent, 2)],
["下次买入", D.spent >= TOTAL - 1e-9 ? "预算已用完" : _D(D.lastBuy + INTERVAL_HOURS * 3600000)]] }
LogStatus(_D(), "\n`" + JSON.stringify(table) + "`")
} catch (e) {
Log("本轮出错:", e.message || e, "#FF0000")
}
Sleep(60000)
}
}
function main() {
SYMBOL = SYMBOL || exchange.GetCurrency()
if (exchange.GetName() === "Jupiter" && NATIVE) nativeDca()
else loopDca()
}参考