Step-by-step guide to writing automated trading strategies in QuantScript. Learn to build indicators, backtest on historical data, and deploy live trading bots with JavaScript-like syntax.
QuantScript is a domain-specific language for creating, testing, and deploying automated trading strategies. You can start writing and running QuantScript code directly in your browser at quantscript.com/playground — no installation required.
QuantScript code can be executed in two channels, each suited to a different stage of your strategy's lifecycle. Every channel supports two execution semantics — Run1 and Run — and each mode has specific rules about where it can run.
| Channel | Description | When to use |
|---|---|---|
| Playground | Browser-based IDE at quantscript.com/playground with live chart rendering | Development, debugging, visual inspection, backtesting |
| Cloud Deployment | Cloud deployment via the QuantScript platform | Continuous forwardtest/live trading 24/7 |
| Run1 | Run | |
|---|---|---|
| Behaviour | Execute the script once, then exit | Loop: execute, wait for next candle close, execute again |
| Lifecycle | Parse → execute → output → exit | Parse → execute → poll/wait → execute → poll/wait → ... |
| Use case | Backtests, indicator calculations, one-shot trades | Continuous forwardtest or live trading |
| Playground | ✅ "Run" button | ✅ "Run (Live Loop)" button |
| Cloud | ❌ Not applicable | ✅ Continuous deployment |
QuantScript has four execution modes, declared with mode():
| Mode | Declaration | Run1 | Run | Cloud |
|---|---|---|---|---|
| Indicator | mode("indicator") or omitted | ✅ | ❌ | ❌ |
| Backtest | mode("backtest") | ✅ | ❌ | ❌ |
| Forwardtest | mode("forwardtest") | ✅ | ✅ | ✅ |
| Live | mode("live") | ✅ (auto-downgrades to forwardtest) | ✅ (connects to real broker) | ✅ (real broker) |
Cloud deployment is available via the QuantScript platform. Each deployed strategy runs in its own isolated environment with persistent storage.
mode("forwardtest") and mode("live") are accepted for cloud deployment.mode("indicator") and mode("backtest") are rejected at deploy time.Create a file called hello.qs in the Playground:
console.log("Hello, World!");Click "Run" to execute. Output:
── QuantScript Indicator ──
Hello, World!
Execution complete.// 1. Mode declaration (optional — defaults to indicator mode)
mode(mode_type: "indicator", script_name: "My Script Name");
// 2. Global code (executed once)
let myVariable = 10;
// 3. DataFrame iteration (executed per bar)
event.onCandle((candle) => {
// Process each bar
});mode(mode_type: "indicator", script_name: "Math Example");
let x = 10;
const PI = 3.14159;
let sum = x + 5;
let product = x * 2;
let power = 2 ** 8;
console.log("Sum: " + sum);
console.log("Product: " + product);
console.log("Power: " + power);QuantScript includes the standard JavaScript utility namespaces and instance methods with full ES6 parity. If you know JavaScript, you already know these:
Math.* — Math.abs(), Math.max(), Math.sqrt(), Math.round(), etc.JSON.* — JSON.parse(text), JSON.stringify(value)Date.* — Date.now(), Date.parse(str), Date.UTC(y, m, d)"hello".toUpperCase(), .includes(), .split(), .replace(), etc.arr.push(), arr.map(fn), arr.filter(fn), arr.reduce(fn, init), etc.See the API Reference for the full list, or consult any JavaScript reference (e.g. MDN) — the behaviour is identical.
event.onCandle((candle) => {
let openPrice = candle.open;
let highPrice = candle.high;
let lowPrice = candle.low;
let closePrice = candle.close;
let volume = candle.volume;
});event.onCandle((candle) => {
let midpoint = candle.hl2; // (high + low) / 2
let avgThree = candle.hlc3; // (high + low + close) / 3
let avgFour = candle.ohlc4; // (open + high + low + close) / 4
});let currentClose = candle.close;
let previousClose = close[1]; // 1 bar ago
let twoBarsAgo = close[2]; // 2 bars agomode(mode_type: "indicator", script_name: "SMA Indicator");
const SMA_LENGTH = 20;
event.onCandle((candle) => {
let sma = ta.sma(candle.close, SMA_LENGTH);
chart.plot(sma, "SMA", "#FF0000");
});event.onCandle((candle) => {
let fastSMA = ta.sma(candle.close, 10);
let slowSMA = ta.sma(candle.close, 20);
let ema = ta.ema(candle.close, 20);
chart.plot(fastSMA, "Fast SMA", "#00FF00");
chart.plot(slowSMA, "Slow SMA", "#FF0000");
chart.plot(ema, "EMA", "#0000FF");
if (ta.crossover(fastSMA, slowSMA)) {
console.log("BULLISH CROSSOVER");
}
});candles(symbol: "BTCUSD", resolution_type: "1h", bars: 500);
mode(mode_type: "backtest", script_name: "SMA Crossover");
broker(adapter: "sim", config: { initial_capital: 100000 });
event.onCandle((candle) => {
let fastSMA = ta.sma(candle.close, 10);
let slowSMA = ta.sma(candle.close, 20);
if (ta.crossover(fastSMA, slowSMA)) {
trade.buy({ symbol: "BTCUSD", qty: 1 });
}
if (ta.crossunder(fastSMA, slowSMA)) {
trade.closePosition({});
}
});candles(symbol: "BTCUSD", resolution_type: "1h", bars: 500);
mode(mode_type: "backtest", script_name: "Strategy with Exits");
broker(adapter: "sim", config: { initial_capital: 100000 });
event.onCandle((candle) => {
let sma = ta.sma(candle.close, 20);
let crossUp = ta.crossover(candle.close, sma);
if (crossUp && strategy.position_size == 0) {
trade.buy({ symbol: "BTCUSD", qty: 1, stopLoss: 2.0, takeProfit: 4.0 });
}
});Instead of specifying a raw qty, you can use the size object for more intuitive position sizing across different asset classes:
// Trade 0.5 lots of EURUSD
trade.buy({ symbol: "EURUSD", size: { lots: 0.5 } });
// Crypto: spend exactly $500
trade.buy({ symbol: "BTCUSDT", size: { cash: 500 } });
// Risk 2% of account equity
trade.buy({ symbol: "AAPL", size: { equity_percent: 2 } });Available sizing modes: units (direct quantity), lots (standardized lot units), cash (quote-currency amount), equity_percent (% of equity). Exactly one must be set. See the API Reference for details.
QuantScript supports analyzing multiple symbols and resolutions in a single script using data.loadCandles() with a callback. This evaluates indicators on a secondary resolution's native bars, then aligns results back to your primary chart — like Pine Script's request.security().
mode(mode_type: "indicator", script_name: "Multi-TF");
candles(symbol: "BTCUSD", resolution_type: "1h", bars: 300, adapter: "demotrader.net");
// ETH 4h SMA computed on native 4h bars
var ethSma = data.loadCandles(adapter: "demotrader.net", symbol: "ETHUSD", resolution_type: "4h", callback: (c) => ta.sma(c.close, 50));
event.onCandle((candle) => {
chart.plot(candle.close, "BTC Close");
chart.plot(ethSma, "ETH 4h SMA", "#FF9800");
});Use a higher timeframe for trend filtering and a lower timeframe for entry timing:
mode(mode_type: "forwardtest", script_name: "Cross-TF Strategy");
candles(symbol: "ETHUSD", resolution_type: "15m", bars: 200, adapter: "demotrader.net");
broker(adapter: "sim", config: { initial_capital: 10000 });
// Daily SMA as trend filter
var dailySma = data.loadCandles(adapter: "demotrader.net", symbol: "ETHUSD", resolution_type: "1D", callback: (c) => ta.sma(c.close, 50));
event.onCandle((candle) => {
const rsi = ta.rsi(candle.close, 14);
if (dailySma !== null && candle.close > dailySma && rsi !== null && rsi < 35) {
trade.buy({ symbol: "ETHUSD", qty: 0.1 });
}
});Results support [n] historical lookback and are forward-filled to align with your primary chart.
event.onCandle((candle) => { ... }).close directly. QuantScript uses candle.close inside the event handler.close[1] syntax."#0000FF".ta. for technical analysis.For more on the design philosophy, see the About QuantScript page.
When running multiple strategies on a single broker account, each strategy needs its own isolated view of capital. Virtual Trading Accounts solve this by giving each strategy an independent balance, equity, and margin — even though they all share the same real broker account.
Set allocated_capital in your broker() config to opt in:
broker(adapter: "demotrader.net", email: "...", password: "...", config: {
allocated_capital: 10000
});This creates a VirtualAccount for the strategy, seeded with $10,000. From there:
When a virtual account is active, all risk() parameters are enforced against the virtual account — not the broker-wide account:
min_balance and min_equity check against virtual valuesmax_position_pct and max_risk_pct_per_trade compute percentages from virtual equitymax_margin_pct uses virtual margin and balanceThis means one strategy hitting its risk limit does not block the other strategies sharing the same broker account.
Two strategies sharing one $50,000 account, each with independent capital and risk limits:
// Strategy A: $30,000 allocated for BTC momentum
candles(symbol: "BTCUSD", resolution_type: "1h", bars: 200);
mode(mode_type: "live", script_name: "Momentum");
broker(adapter: "demotrader.net", email: "...", password: "...", config: {
allocated_capital: 30000
});
risk(min_equity: 25000, max_position_pct: 20);// Strategy B: $20,000 allocated for ETH mean reversion
candles(symbol: "ETHUSD", resolution_type: "15m", bars: 200);
mode(mode_type: "live", script_name: "Mean Reversion");
broker(adapter: "demotrader.net", email: "...", password: "...", config: {
allocated_capital: 20000
});
risk(min_equity: 17000, max_position_pct: 15);Each strategy’s performance (equity curve, drawdown, ROI) is measured against its own allocated_capital, giving you accurate per-strategy metrics.
Virtual accounts are automatically rebuilt from your strategy’s historical fills and open positions on cold start (e.g., after a server restart). This ensures balance and equity tracking continues seamlessly without manual intervention.
When your strategy runs in forwardtest or live mode, the runtime keeps an incomplete (forming) candle that updates with every incoming tick. This gives you access to the freshest market price between confirmed bar closes.
Confirmed candle (candle.close) | Forming candle (trade.last()) | |
|---|---|---|
| Mutability | Immutable — never changes after bar close | Updates with every tick until the bar closes |
| Use for | Indicators, signal logic, trend detection | Entry checks, SL/TP placement, position sizing |
| Repainting risk | None | Value changes intra-bar (by design) |
Rule of thumb: Compute your signals from confirmed data, then execute using the live price.
trade.last(symbol?) — Freshest last-traded price (forming bar close, or latest quote)trade.formingCandle(symbol?) — Full forming candle object (open, high, low, close, volume, incomplete)trade.bid(symbol?) — Current best bid from the quote streamtrade.ask(symbol?) — Current best ask from the quote streamSee the API Reference for full details on each function.
In backtest mode no forming bar exists, so trade.last() returns 0. Always include a fallback so the same script works everywhere:
let livePrice = trade.last();
if (livePrice == 0) { livePrice = candle.close; }Signal from confirmed data, execution at live price:
event.onCandle((candle) => {
let sma = ta.sma(candle.close, 20); // signal: confirmed data
if (candle.isLive) {
let livePrice = trade.last();
if (livePrice == 0) { livePrice = candle.close; }
if (candle.close > sma) {
let sl = livePrice * 0.995; // SL relative to live price
trade.buy({ symbol: "ETHUSD", size: { equity_percent: 5 }, stopLoss: sl });
}
}
});Notice: the SMA is computed from candle.close (confirmed), but the stop-loss is anchored to livePrice (forming). This gives you stable signals with accurate execution.