DEX DCA
Buys the same token at regular intervals to average down the position cost: on Jupiter it uses native DCA, and on other DEXs the strategy places market buys on a schedule. When the price is below the moving average, that period's buy amount is doubled.
How It Works
The strategy picks its execution mode based on the exchange object:
- Jupiter with
NATIVEset totrue: uses native DCA throughexchange.IO("dca.*", ...).dca.createdeposits the fullTOTALup front, and Jupiter's keeper buys onePER_ORDERtranche everyINTERVAL_HOURShours, so the strategy does not need to keep running.MIN_PRICEandMAX_PRICErestrict buys to that price range (when the price is outside the range, the round is skipped or deferred). On startup, the strategy first callsdca.listto check whether the trading pair already has an active DCA order and creates one only if none exists (the returned ID is also saved via_Gto prevent duplicate creation). After that, it callsdca.listevery minute to display execution progress. Recurring v1 (no API Key configured) requires at least $50 per tranche and at least 2 tranches; Trigger V2 (API Key configured and quote currency USDC or USDT) requires at least $10 per tranche, with funds held in a Jupiter-managed vault. - Other DEXs (Uniswap, PancakeSwap, Aerodrome, Velodrome, etc.), or
NATIVEset tofalse: everyINTERVAL_HOURShours the strategy buysPER_ORDERwith a market order (exchange.CreateOrder(交易对, "buy", -1, 金额); for market buy orders the amount is denominated in the quote currency). When the best ask isDIP_PCT% below theMA_DAYS-day moving average, that period's amount is multiplied byDIP_MULT(buy more on dips), and total spending never exceedsTOTAL. Each fill is booked after confirmation viaexchange.GetOrder, and progress is saved with_G()so execution can resume after a restart.
Exchange Object
One DEX exchange object (a dedicated wallet is recommended). The wallet must hold the quote currency plus a small amount of the native coin to pay fees. When SYMBOL is null, the trading pair configured on the exchange object is used.
Parameters
| Parameter | Default | Description |
|---|---|---|
SYMBOL | null | Trading pair; null means the trading pair configured on the exchange object |
TOTAL | 500 | Total budget (in quote currency) |
PER_ORDER | 50 | Amount per tranche (in quote currency) |
INTERVAL_HOURS | 24 | Interval between tranche buys (hours) |
NATIVE | true | Use native DCA on Jupiter (dca.create) |
MIN_PRICE | null | Native DCA: skip the round when the price is below this value (null means no limit) |
MAX_PRICE | null | Native DCA: skip the round when the price is above this value (null means no limit) |
DIP_PCT | 5 | Strategy-driven buys: buy extra when the price is this percentage below the moving average |
DIP_MULT | 2 | Strategy-driven buys: multiplier for dip buys |
MA_DAYS | 20 | Strategy-driven buys: moving average period in days |
DRY_RUN | true | When true, only simulates and spends no real funds |
Risks
- DCA only averages your entry cost; it cannot prevent losses from a falling market.
- Every on-chain transaction incurs gas or network fees, so small, high-frequency buys are not cost-effective.
- Market orders are subject to slippage (see
exchange.IO("slippage")).
DRY_RUN vs. Live Trading
When DRY_RUN is true (the default), no real funds are spent: native DCA uses dca.simulate to simulate the creation transaction (reason gives the reason it failed, if any). Strategy-driven buys use exchange.IO("route") to get a quote and record a paper fill, and EVM DEXs also use exchange.IO("simulate") to simulate the swap transaction (the simulation errors out when the wallet balance is insufficient, which is expected). Set DRY_RUN to false to go live. Once created, a native DCA order is executed by Jupiter, so stopping the bot does not stop the DCA; to cancel it, use dca.cancel.
Examples
Complete strategy code (DRY_RUN = true: simulation only, no real funds are spent).
javascript
/*
* DEX DCA (dollar-cost averaging, example strategy)
*
* Two implementations, chosen automatically based on the exchange object:
* 1. Jupiter (Solana) with NATIVE = true: uses Jupiter's native DCA exchange.IO("dca.create", …) — the full amount is deposited up front,
* and Jupiter's keeper buys one tranche every INTERVAL_HOURS hours, so the strategy does not need to keep running; MIN_PRICE / MAX_PRICE can restrict
* buys to a price range (a round outside the range is skipped or deferred). The strategy only creates the DCA order and shows its progress via dca.list.
* Recurring v1 (without an API Key) requires at least $50 per order and at least 2 orders; Trigger V2 (with an API Key and a quote currency of
* USDC / USDT) requires at least $10 per order, with funds held in a vault custodied by Jupiter.
* 2. Other DEXs (Uniswap, PancakeSwap, Aerodrome, Velodrome, etc.), or NATIVE = false: the strategy itself buys, every
* INTERVAL_HOURS hours, coins worth PER_ORDER of the quote currency with a market order; when the price is DIP_PCT% below the MA_DAYS-day moving average, that order's amount
* is multiplied by DIP_MULT (buying the dip). Total spending never exceeds TOTAL.
* Progress is saved in _G, so execution can resume after a restart.
*
* With DRY_RUN = true (default) no funds are spent: native DCA uses IO("dca.simulate", …) to simulate the creation transaction; the self-buying mode uses
* IO("route", …) to get quotes and records paper fills, and on EVM DEXs also uses IO("simulate", …) to simulate the swap transaction (simulation errors are normal when the wallet balance is insufficient).
*
* Risk warning: DCA only averages out the entry cost and cannot prevent losses in a downtrend; every on-chain transaction incurs gas / network fees, so small high-frequency buys are not cost-effective;
* market orders are subject to slippage (see exchange.IO("slippage")).
*
* Usage: add a DEX exchange object (a dedicated wallet is recommended), and fund the wallet with the quote currency plus a small amount of the native coin for fees.
*/
// ---------------- Parameters ----------------
var SYMBOL = null // Trading pair; null = the pair configured on the exchange object
var TOTAL = 500 // Total budget (quote currency)
var PER_ORDER = 50 // Amount per order (quote currency)
var INTERVAL_HOURS = 24 // Interval between buys (hours)
var NATIVE = true // Use native DCA (dca.create) on Jupiter
var MIN_PRICE = null // Native DCA: skip the round when the price is below this value (null = no limit)
var MAX_PRICE = null // Native DCA: skip the round when the price is above this value (null = no limit)
var DIP_PCT = 5 // Self-buying: buy more when the price is this % below the moving average
var DIP_MULT = 2 // Self-buying: multiplier for dip buys
var MA_DAYS = 20 // Self-buying: moving average period in days
var DRY_RUN = true // true = simulation only, no funds are spent
function fmt(x, d) {
return x === null || x === undefined || isNaN(x) ? "-" : Number(x).toFixed(d === undefined ? 2 : d)
}
// ---------------- Method 1: Jupiter native DCA ----------------
function nativeDca() {
var key = "dca_native_" + SYMBOL // ID of the created DCA order, prevents duplicate creation after a restart
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 error:", GetLastError())
else Log("dca.simulate (" + sim.version + ")", sim.ok ? "passed" : "failed: " + sim.reason, "plan:", JSON.stringify(sim.plan))
} else {
var r = exchange.IO("dca.create", params)
if (!r) throw new Error("dca.create failed: " + GetLastError())
Log("DCA created", r.version, r.id, "per order", r.perOrder, "total", r.numberOfOrders, "orders", "#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 native DCA", DRY_RUN ? "(DRY_RUN: creation was only simulated; the table below shows DCA orders already in the wallet)" : "",
"\n`" + JSON.stringify({ type: "table", title: "DCA", cols: ["ID", "Version", "State", "Per Order", "Orders Filled", "Spent", "Bought", "Avg Price", "Next"], rows: rows }) + "`")
} catch (e) {
Log("Error in this round:", e.message || e, "#FF0000")
}
Sleep(60000)
}
}
// ---------------- Method 2: the strategy buys on its own schedule ----------------
function buyOnce(D, amount, price) {
var q = exchange.IO("route", SYMBOL, "buy", amount / price) // Quote only: how much it costs to buy this quantity
if (DRY_RUN) {
if (exchange.GetName() !== "Jupiter") {
var sim = exchange.IO("simulate", SYMBOL, "buy", amount) // Market buy: amount is denominated in the quote currency
Log(sim ? "simulate passed: " + sim.route : "simulate failed (normal when the wallet balance is insufficient): " + GetLastError())
}
if (!q || !q.price) { Log("Quote failed:", GetLastError()); return false }
D.spent += amount; D.bought += amount / q.price
Log("DRY_RUN buy", 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("Buy failed:", 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("Bought", fmt(amount, 2), "→", o.DealAmount, "@", fmt(o.AvgPrice, 4), "#00AA00")
return true
}
if (o && o.Status === ORDER_STATE_CANCELED) { Log("Buy order not filled (canceled / failed on-chain)", id, "#FF0000"); return false }
Sleep(2000)
}
Log("Buy order not confirmed within two minutes; recorded as spent, please verify later", 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("DCA order #", D.buys + 1, "amount", fmt(amount, 2), dip ? "(below MA by " + 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 + " DCA" + (DRY_RUN ? " (DRY_RUN paper)" : ""), cols: ["Item", "Value"], rows: [
["Orders Filled", D.buys], ["Spent / Total Budget", fmt(D.spent, 2) + " / " + TOTAL], ["Bought", fmt(D.bought, 6)],
["Avg Entry Price", fmt(avg, 4)], ["Current Price (Best Bid)", fmt(t.Buy, 4)], [MA_DAYS + "-day MA", fmt(ma, 4) + (dip ? " (dip)" : "")],
["Market Value / PnL", fmt(value, 2) + " / " + fmt(value - D.spent, 2)],
["Next Buy", D.spent >= TOTAL - 1e-9 ? "Budget exhausted" : _D(D.lastBuy + INTERVAL_HOURS * 3600000)]] }
LogStatus(_D(), "\n`" + JSON.stringify(table) + "`")
} catch (e) {
Log("Error in this round:", e.message || e, "#FF0000")
}
Sleep(60000)
}
}
function main() {
SYMBOL = SYMBOL || exchange.GetCurrency()
if (exchange.GetName() === "Jupiter" && NATIVE) nativeDca()
else loopDca()
}See Also