
QuaScript Documentation
Language guide, engine API, 601 builtins and the preset library · v3.0.0-phase1
Bots
A bot is a strategy that trades for real (or on paper). It runs the same simulated broker on history, so you see how it would have behaved, and from the first live bar on, every order it places is also published as a signal.
bot "EMA Cross Bot" {
capital = 10000
sizing = fixed_qty
size = 1
on = close
mode = paper
}
f = ta.ema(close, 9)
s = ta.ema(close, 21)
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") }Signals
Each signal has an action (market, limit, stop, stop_limit, cancel, close, close_all, bracket), the order id, side, quantity and prices, the bar and its time, and a signalId: a SHA-256 based id of (user, strategy, script, parameters, symbol, interval, bar time, order id, action). Re-running the same script on the same data gives the same ids, so a consumer that remembers the ids it has executed never executes a signal twice, even after a restart.
When signals are emitted
- Only on live bars: bars that arrive after the bot was opened. The bars it was opened with are history and never emit, including a forming last bar.
on = close(default): when a live bar closes.on = update: on every update of the forming bar (orders placed and then withdrawn within the bar are not repeated).- The CLI reproduces this with
quas run bot.qs data.csv --live-from N: bars with index >= N are live.
Paper and live
mode = paper (default) sends signals to the paper broker; mode = live routes them to the connected broker account. Start every bot on paper; the Strategy Tester compares the paper fills with the simulated ones.
Who does what
| The script | bot-api |
|---|---|
| measures indicators and generates signals | logs in to the broker with the account connected in Manage Broker |
| decides what to place, modify, cancel, protect or close | places, modifies and cancels the orders, with the script's client order id |
| reads the confirmations and the account | confirms every order (sent, open, partial, filled, rejected, cancelled) and reports fills, the position and the balance back |
| manages risk, retries and re-entries | enforces server-side limits the script cannot change, and the kill switch |
The script never sees a password, token or API secret: it names an account alias, and bot-api uses the credential stored for it. On history, in backtests and on paper the same calls run on the simulated broker, so a bot behaves the same before and after it goes live.
Broker libraries
bot "Zerodha bot" {
capital = 100000
mode = paper
client_id_prefix = "zb"
}
import zerodha
from zerodha import Product, Exchange
zerodha.login("main") // the Zerodha account called "main" in Manage Broker
f = ta.ema(close, 9)
s = ta.ema(close, 21)
if ta.cross_up(f, s) and account.position == 0 {
zerodha.limit("L", side.long, 25, close, "INFY", product = Product.mis)
}
if order.is_filled("L") and position.size > 0 {
px = order.avg_fill("L") // the broker's fill price, not the bar's close
zerodha.protect("L exit", "L", stop = px * 0.99, target = px * 1.02)
}
if ta.cross_down(f, s) { zerodha.square_off("L") }
render.line(f, "Fast")| Library | What it covers |
|---|---|
zerodha | login, place_order (MARKET / LIMIT / SL / SL-M, every variety, product, validity, TTL, iceberg, disclosed quantity), market, limit, stop_loss, stop_market, cover, protect (stop / target OCO), gtt_oco, cancel, cancel_all, square_off, square_off_all; enums Exchange, Product, Variety, Validity, OrderType |
delta | login (India, Global, testnet), leverage (and margin mode), market, limit (post-only, reduce-only, time in force), stop_market, stop_limit, take_profit, bracket (exchange-held stop / target / trailing), cancel, cancel_all, close_position, close_all_positions |
broker | the same order calls for AngelOne, Upstox, Dhan, 5Paisa, Flattrade, Kotak, ICICI Direct, HDFC, Motilal Oswal, IIFL, Paytm Money, Axis Direct, Alice Blue, Bajaj, TradeSmart and Groww; broker-native fields go in options = "k=v;k=v" |
bots/execution | settled, retry_after_reject, stale, cancel_if_stale, reentry_ready, loss_streak_ok, risk_qty |
zerodha.protect and delta.bracket exit an open position. zerodha.stop_loss and stop_market are entries in the opposite direction: they would reverse the position, so use them to enter on a breakout, not to protect a trade.
Routing
Library calls set an order's route with order.route(id, broker, symbol, account, options, client_id); you can call it directly too. The route with id "" applies to every order without its own route (the libraries' login sets it). Header settings broker and account are the defaults. A bracket without a route of its own trades the instrument of the entry it protects.
Client order ids
Every live signal carries a client order id, sent to the broker as Zerodha's tag, Delta's client_order_id and the matching field of other brokers, so every order at the broker can be traced to the bot and the signal that placed it.
- Generated ids are
client_id_prefix(header, defaultqs, up to 8 characters) followed by the hex signal id, 20 characters of[A-Za-z0-9]. They are deterministic: the same script on the same bar gives the same id, so a restarted bot never places an order twice. order.route(id, client_id = "myTag01")fixes the id of an order.order.client_id(id)is the client id of the order's latest placement;order.next_client_id(id, action)is the id a placement on this bar would get (for logs).- Re-placing an order id on a later bar gives a new client id: a rejected order retried with
order.market("L", ...)is a new order, and its status starts again fromsent.
Confirmations
| Read | Meaning |
|---|---|
order.status(id) | none, pending (simulated), sent, open, partial, filled, rejected, cancelled |
order.is_confirmed(id) | the broker accepted the order (open, partly or fully filled) |
order.is_open(id) / is_filled / is_rejected / is_cancelled | the status in one word |
order.filled_qty(id), order.avg_fill(id) | quantity and average price filled so far |
order.broker_order_id(id), order.message(id) | the broker's order id and its message (rejection reason) |
account.is_connected, account.position, account.avg_price, account.equity, account.available, account.realized_pnl | the broker account; the simulated broker's values without a live executor |
A confirmation arrives between bars and applies from the next execution on. With a live executor attached, an order is sent until the broker answers; the simulated fill is not used.
bot "Confirm and retry" { mode = paper }
import delta
import bots.execution as ex
delta.login("main")
a = ta.atr(14)
entry = ta.cross_up(ta.ema(close, 9), ta.ema(close, 21)) and ex.reentry_ready(5, 3)
if entry { delta.limit("L", side.long, 10, close, "BTCUSD", post_only = true) }
// a post-only order the exchange rejected comes back as a market order, once
if ex.retry_after_reject("L", 1) { delta.market("L", side.long, 10, "BTCUSD") }
// a limit that has not filled in 3 bars is withdrawn
ex.cancel_if_stale("L", 3)
// protect from the actual fill, once
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 + 3 * a)
}
render.line(a, "ATR")Running a bot (bot-api)
POST /qs-bot with { script, symbol, exchange, interval, brokerSymbol?, broker?, params?, limits?, live?, confirmLive? } compiles the script and starts it on paper. Live trading needs three things: mode = live in the header, live: true and confirmLive: true in the request. GET /qs-bot/:id lists the tracked orders with their confirmations, GET /qs-bot/:id/events the execution log, POST /qs-bot/:id/stop stops it (working orders stay at the broker) and POST /qs-bot/:id/kill cancels every working order, flattens the position and stops.
Server-side limits (PUT /qs-bot/:id/limits) are checked before every order and cannot be changed by the script: maxOrderValue, maxPosition, maxOrdersPerMinute (default 30) and maxDailyLoss (stops new entries for the day). A blocked order is reported to the script as rejected with the reason in order.message.
The executor records each signal id before it calls the broker, so a crash in the middle of a call never repeats the order, and a restart re-opens the script on history (no signals) and resumes from the stored orders.