Getting Started#

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.

Execution Channels & Modes#

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.

The Two Channels

ChannelDescriptionWhen to use
PlaygroundBrowser-based IDE at quantscript.com/playground with live chart renderingDevelopment, debugging, visual inspection, backtesting
Cloud DeploymentCloud deployment via the QuantScript platformContinuous forwardtest/live trading 24/7

Run1 vs Run

Run1Run
BehaviourExecute the script once, then exitLoop: execute, wait for next candle close, execute again
LifecycleParse → execute → output → exitParse → execute → poll/wait → execute → poll/wait → ...
Use caseBacktests, indicator calculations, one-shot tradesContinuous forwardtest or live trading
Playground✅ "Run" button✅ "Run (Live Loop)" button
Cloud❌ Not applicable✅ Continuous deployment

Modes and Where They Are Valid

QuantScript has four execution modes, declared with mode():

ModeDeclarationRun1RunCloud
Indicatormode("indicator") or omitted
Backtestmode("backtest")
Forwardtestmode("forwardtest")
Livemode("live")✅ (auto-downgrades to forwardtest)✅ (connects to real broker)✅ (real broker)

Cloud Deployment

Cloud deployment is available via the QuantScript platform. Each deployed strategy runs in its own isolated environment with persistent storage.

  • Only mode("forwardtest") and mode("live") are accepted for cloud deployment.
  • mode("indicator") and mode("backtest") are rejected at deploy time.
  • Each deployment wakes on its own resolution_type schedule.
  • State persists across executions via runtime snapshots.

Hello World#

Create a file called hello.qs in the Playground:

QuantScript
console.log("Hello, World!");

Click "Run" to execute. Output:

text
── QuantScript Indicator ──
Hello, World!
Execution complete.

Running Your First Script#

Script Structure

QuantScript
// 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
});

Variables and Math

QuantScript
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);

JavaScript Standard Library#

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)
  • String methods"hello".toUpperCase(), .includes(), .split(), .replace(), etc.
  • Array methodsarr.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.

Working with OHLCV Data#

Accessing Bar Data

QuantScript
event.onCandle((candle) => {
    let openPrice = candle.open;
    let highPrice = candle.high;
    let lowPrice = candle.low;
    let closePrice = candle.close;
    let volume = candle.volume;
});

Derived Bar Variables

QuantScript
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
});

Historical Data Access

QuantScript
let currentClose = candle.close;
let previousClose = close[1];  // 1 bar ago
let twoBarsAgo = close[2];     // 2 bars ago

Building a Simple Indicator#

Simple Moving Average

QuantScript
mode(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");
});

Multiple Indicators

QuantScript
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");
    }
});

Creating a Trading Strategy#

SMA Crossover Strategy

QuantScript
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({});
    }
});

Strategy with Exit Orders

QuantScript
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 });
    }
});

Trade Sizing

Instead of specifying a raw qty, you can use the size object for more intuitive position sizing across different asset classes:

QuantScript
// 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.

Declarative Signal Style (recommended)

Structure each event.onCandle body as two sections: named boolean signals computed fresh from series data on every bar, then a mechanical signal-to-order mapping. No flags, no counters, no cross-frame memory — entry/exit edges come from ta.crossover / ta.crossunder / ta.barssince, which are true only on the bar where the event occurs.

QuantScript
event.onCandle((candle) => {
    // Section 1: signals (named booleans over series)
    const sma = ta.sma(candle.close, 20);
    const longEntry = ta.crossover(candle.close, sma);
    const longExit = ta.crossunder(candle.close, sma);

    // Section 2: execution (mechanical signal → order mapping)
    let longPosition = null;
    for (const pos of trade.positions()) {
        if (pos.symbol == "BTCUSD" && pos.side == "buy") longPosition = pos;
    }
    if (longPosition !== null && longExit) {
        trade.closePosition({ positionId: longPosition.id });
    }
    if (longPosition === null && longEntry) {
        trade.buy({ symbol: "BTCUSD", qty: 1 });
    }
});

Avoid setting boolean flags on one bar and checking them on later bars, or counting bars manually — ta.barssince(condition) expresses the same fact as data. See the Declarative Signals example for a complete three-dataset strategy in this style.

Filtered Introspection & Bulk Operations

Grid and basket strategies often need to query specific subsets of positions and orders. Use filter objects to narrow results, then bulk operations to act on them:

QuantScript
// Ownership tag for this strategy's entities
const posTag = "MyGrid|";

// Filtered introspection: only this strategy's tagged positions
let basket = trade.positions({ tag: posTag });

// Working limit orders only (terminal statuses excluded by default)
let ladder = trade.orders({ tag: posTag, type: "limit" });

// Basket aggregation via native array methods
let qty = basket.reduce((s, p) => s + p.qty, 0);
let vwap = basket.reduce((s, p) => s + p.avgEntryPrice * p.qty, 0) / qty;

// Bulk operations: cancel all ladder limits, then flatten basket
if (shouldExit) {
  trade.cancelAll(ladder);
  trade.closeAll(basket, { reason: "trend flatten", tags: ["exit"] });
}

Filter fields: tag (comment-prefix match), symbol, side. Orders additionally: type ("limit", "stop", etc.), includeTerminal (boolean). All fields are ANDed; unset fields match everything. Filtered orders exclude terminal statuses by default.

Bulk operations: trade.cancelAll(orders) returns { cancelled, cancelFailed }; trade.closeAll(positions, meta?) returns { closed, closeFailed }. Both are best-effort: individual failures are logged and counted, but the call succeeds. HISTORICAL_BAR rejections (replay artifacts) are skipped silently.

See the API Reference for full signatures.

Multi-Timeframe Analysis#

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().

Basic Example

QuantScript
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");
});

Cross-Timeframe Strategy

Use a higher timeframe for trend filtering and a lower timeframe for entry timing:

QuantScript
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.

Alignment & Staleness

Secondary values align using closed-bar visibility — a secondary bar's values become visible only once that bar has fully closed — and are forward-filled by default. Declare how stale a feed may get with fill / fill_limit, and query staleness as data via feed.age (0 on the arrival bar, incrementing while carried, null when invisible or expired):

QuantScript
// Daily regime feed: carry each value at most 3 primary bars past arrival
var xau = data.loadCandles({ adapter: "demotrader.net", symbol: "XAUUSD", resolution_type: "1D", fill_limit: 3 });

event.onCandle((candle) => {
    const fresh = xau.age !== null;   // visible AND within the staleness cap
    const calm = fresh && xau.close < 3000;
});

Raw feeds (loaded without a callback) can also be re-viewed under a different contract via data.align(feed, { fill_limit: 3 }), and current staleness queried with data.age(feed). See the API Reference for details.

For a full walkthrough with two complete multi-dataset strategies (ETHUSD + XAUUSD regime, and XAUUSD + VIX volatility gating), see the Multi-Dataset Analysis feature page.

Pine Script to QuantScript#

Key Translation Notes

  1. Frame iteration: Pine iterates bar-by-bar automatically. QuantScript requires explicit event.onCandle((candle) => { ... }).
  2. Variable access: Pine uses close directly. QuantScript uses candle.close inside the event handler.
  3. Historical references: Both use close[1] syntax.
  4. Colors: QuantScript uses hex strings: "#0000FF".
  5. Function namespaces: Both use ta. for technical analysis.
  6. Semicolons: Required, just like JavaScript.

For more on the design philosophy, see the About QuantScript page.

Virtual Trading Accounts#

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.

How It Works

Set allocated_capital in your broker() config to opt in:

QuantScript
broker(adapter: "demotrader.net", email: "...", password: "...", config: {
  allocated_capital: 10000
});

This creates a VirtualAccount for the strategy, seeded with $10,000. From there:

  • Virtual Balance tracks closed PnL and fees from the strategy’s own fills
  • Virtual Equity adds open PnL from the strategy’s open positions
  • Virtual Margin tracks margin consumed by the strategy’s positions

Risk Guard Integration

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 values
  • max_position_pct and max_risk_pct_per_trade compute percentages from virtual equity
  • max_margin_pct uses virtual margin and balance

This means one strategy hitting its risk limit does not block the other strategies sharing the same broker account.

Multi-Strategy Example

Two strategies sharing one $50,000 account, each with independent capital and risk limits:

QuantScript
// 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);
QuantScript
// 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.

State Recovery

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.

Forming Candle & Live Price#

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 vs. Forming Candle

Confirmed candle (candle.close)Forming candle (trade.last())
MutabilityImmutable — never changes after bar closeUpdates with every tick until the bar closes
Use forIndicators, signal logic, trend detectionEntry checks, SL/TP placement, position sizing
Repainting riskNoneValue changes intra-bar (by design)

Rule of thumb: Compute your signals from confirmed data, then execute using the live price.

Available Functions

  • 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 stream
  • trade.ask(symbol?) — Current best ask from the quote stream

See the API Reference for full details on each function.

The Fallback Pattern

In backtest mode no forming bar exists, so trade.last() returns 0. Always include a fallback so the same script works everywhere:

QuantScript
let livePrice = trade.last();
if (livePrice == 0) { livePrice = candle.close; }

Live Strategy Example

Signal from confirmed data, execution at live price:

QuantScript
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.

Next Steps

  1. Try the Playground — Write and run QuantScript code in your browser
  2. Explore the Language Reference — See all available functions and syntax
  3. Browse the Examples — Run example scripts from this guide
  4. Build your own indicators and strategies
  5. Deploy your strategies to the cloud at quantscript.com