Language guide, engine API, 601 builtins and the preset library · v3.0.0-phase1
Bots
Delta Breakout Bot
bot · presets/bot/delta_breakout_bot.qs
Donchian breakout on a Delta Exchange perpetual. Entries are post-only limits at the breakout level; a post-only rejection falls back to a market order. Once the exchange confirms the fill, an exchange-held bracket (stop, target, trailing amount) protects the position. Flow toxicity (VPIN) above its 90th percentile blocks new entries.
QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
delta_breakout_bot.qs
bot "Delta Breakout Bot" {
capital = 10000
sizing = fixed_qty
size = 1
on = close
mode = paper
client_id_prefix = "dbo"
}
/// Donchian breakout on a Delta Exchange perpetual. Entries are post-only limits at the breakout
/// level; a post-only rejection falls back to a market order. Once the exchange confirms the
/// fill, an exchange-held bracket (stop, target, trailing amount) protects the position. Flow
/// toxicity (VPIN) above its 90th percentile blocks new entries.
import delta
from delta import Venue
import bots.execution as ex
symbol = param.text("BTCUSD", title = "Product symbol")
alias = param.text("", title = "Delta account alias")
len = param.int(20, title = "Channel length", min = 5, max = 200)
lev = param.float(5, title = "Leverage", min = 1, max = 50, step = 1)
contracts = param.float(10, title = "Contracts", min = 1, max = 10000, step = 1)
delta.login(alias, venue = Venue.india)
delta.leverage(symbol, lev)
risk.max_drawdown(15)
(mid, upper, lower) = ta.donchian(len)
a = ta.atr(14)
vpin = hft.vpin(50)
calm = not (vpin > stat.percentile(vpin, 500, 90))
can_enter = calm and ex.reentry_ready(3, 4) and ex.loss_streak_ok(4, 30)
long_sig = close > upper[1] and can_enter
short_sig = close < lower[1] and can_enter
if long_sig { delta.limit("L", side.long, contracts, upper[1], symbol, post_only = true) }
if short_sig { delta.limit("S", side.short, contracts, lower[1], symbol, post_only = true) }
if ex.retry_after_reject("L", 1, 1) and can_enter { delta.market("L", side.long, contracts, symbol) }
if ex.retry_after_reject("S", 1, 1) and can_enter { delta.market("S", side.short, contracts, symbol) }
ex.cancel_if_stale("L", 2)
ex.cancel_if_stale("S", 2)
if ex.settled("L") and order.is_filled("L") {
px = order.avg_fill("L")
delta.bracket("L exit", "L", stop = px - 2 * a, target = px + 4 * a, trail = 1.5 * a)
}
if ex.settled("S") and order.is_filled("S") {
px = order.avg_fill("S")
delta.bracket("S exit", "S", stop = px + 2 * a, target = px - 4 * a, trail = 1.5 * a)
}
pu = render.line(upper, "Upper", color = color.emerald.500)
pl = render.line(lower, "Lower", color = color.rose.500)
render.band(pu, pl, color = color.slate.500, opacity = 0.06)
render.background(calm ? na : color.translucent(color.amber.500, 88))
EMA Cross Bot
bot · presets/bot/ema_cross_bot.qs
Paper-trading bot: one position at a time, long on a fast / slow EMA cross up, flat on the cross down, with a hard ATR stop. Orders on confirmed live bars become bot signals.
QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
ema_cross_bot.qs
bot "EMA Cross Bot" {
capital = 10000
sizing = fixed_qty
size = 1
on = close
mode = paper
}
/// Paper-trading bot: one position at a time, long on a fast / slow EMA cross up, flat on the
/// cross down, with a hard ATR stop. Orders on confirmed live bars become bot signals.
fast = param.int(9, title = "Fast EMA", min = 2)
slow = param.int(21, title = "Slow EMA", min = 3)
f = ta.ema(close, fast)
s = ta.ema(close, slow)
a = ta.atr(14)
if position.size == 0 and ta.cross_up(f, s) {
order.market("L", side.long)
exit.bracket("L stop", entry = "L", stop = close - 2 * a)
}
if position.size > 0 and ta.cross_down(f, s) { exit.close("L") }
render.line(f, "Fast", color = color.sky.400)
render.line(s, "Slow", color = color.orange.400)
Range Grid Bot
bot · presets/bot/grid_bot.qs
Buys each grid level below the range midpoint and sells the lots one level higher. The range is the Donchian channel of len bars; trading pauses when price leaves it.
QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
grid_bot.qs
bot "Range Grid Bot" {
capital = 20000
sizing = fixed_cash
size = 1000
pyramiding = 5
on = close
mode = paper
}
/// Buys each grid level below the range midpoint and sells the lots one level higher. The range
/// is the Donchian channel of `len` bars; trading pauses when price leaves it.
len = param.int(100, title = "Range length", min = 20)
levels = param.int(5, title = "Grid levels", min = 2, max = 10)
(mid, up, dn) = ta.donchian(len)
spacing = (up - dn) / (levels * 2)
inside = close < up[1] and close > dn[1]
persist last_level = na
level = spacing > 0 ? math.floor((close - dn) / spacing) : na
if inside and not math.is_na(last_level) and level < last_level and close < mid { order.market("G", side.long) }
if inside and position.size > 0 and not math.is_na(last_level) and level > last_level { exit.close("G", qty_pct = 100 / math.max(1, position.size)) }
if not inside and position.size > 0 { exit.close_all() }
last_level := level
render.line(up, "Range top", color = color.rose.500)
render.line(dn, "Range bottom", color = color.teal.500)
render.line(mid, "Mid", color = color.slate.400, stroke = style.dashed)
Zerodha EMA Bot
bot · presets/bot/zerodha_ema_bot.qs
Trades one NSE stock on Zerodha through bot-api. Entries are sized from the account equity and an ATR stop; the stop-loss / target exits go in only after the broker confirms the entry fill, priced from the actual fill. A rejected entry is retried up to three times, a resting limit entry is cancelled after three bars, and new entries wait out a cool-down after each exit and pause after a losing streak.
QuaScriptengine result
Loading the engine result…
quatick-charts · QuaScript engine on synthetic 1-minute bars · drag to pan · scroll to zoom · Run executes the script on the engine (⌘/Ctrl+Enter while editing)
zerodha_ema_bot.qs
bot "Zerodha EMA Bot" {
capital = 100000
sizing = fixed_qty
size = 1
on = close
mode = paper
client_id_prefix = "zema"
}
/// Trades one NSE stock on Zerodha through bot-api. Entries are sized from the account equity
/// and an ATR stop; the stop-loss / target exits go in only after the broker confirms the entry
/// fill, priced from the actual fill.
/// A rejected entry is retried up to three times, a resting limit entry is cancelled after three
/// bars, and new entries wait out a cool-down after each exit and pause after a losing streak.
import zerodha
from zerodha import Product
import bots.execution as ex
symbol = param.text("INFY", title = "NSE symbol")
alias = param.text("", title = "Zerodha account alias")
fast = param.int(9, title = "Fast EMA", min = 2, max = 100)
slow = param.int(21, title = "Slow EMA", min = 5, max = 300)
risk_pct = param.float(0.5, title = "Risk per trade %", min = 0.1, max = 5, step = 0.1)
atr_k = param.float(2.0, title = "Stop (ATR)", min = 0.5, max = 6, step = 0.25)
zerodha.login(alias)
risk.max_daily_loss(2000)
risk.max_trades_per_day(6)
f = ta.ema(close, fast)
s = ta.ema(close, slow)
a = ta.atr(14)
stop_px = close - atr_k * a
qty = math.max(1, ex.risk_qty(close, stop_px, risk_pct))
can_enter = ex.reentry_ready(5, 3) and ex.loss_streak_ok(3, 20)
if ta.cross_up(f, s) and can_enter {
zerodha.limit("L", side.long, qty, close, symbol, product = Product.mis)
}
// confirmations from the broker drive the rest
if ex.retry_after_reject("L", 3, 1) and can_enter {
zerodha.market("L", side.long, qty, symbol, product = Product.mis)
}
ex.cancel_if_stale("L", 3)
if ex.settled("L") and order.is_filled("L") {
entry_px = order.avg_fill("L")
zerodha.protect("L exit", "L", stop = entry_px - atr_k * a, target = entry_px + 2 * atr_k * a)
}
if ta.cross_down(f, s) and account.position > 0 {
zerodha.square_off("L")
}
render.line(f, "Fast EMA", color = color.sky.400)
render.line(s, "Slow EMA", color = color.amber.400)
rejected = order.is_rejected("L")
render.mark(rejected and not rejected[1], shape = shape.cross, at = anchor.below_bar, color = color.red.500, text = "Rejected")