DocsGetting Started

Strategy Lifecycle

strategy-lifecycle.qslValidated
QSL v1.0

function define(ctx)

Declare optimizable parameters with type, default, min, max

ctx.param('period', {...})

Called once

function init(ctx)

Register indicators using parameters from define()

ctx.indicator('ema', ...)

Called once

function onBar(ctx, i)

Execute trading logic for each candle

ctx.order.market('long')

Called N times · ↻ loop

0 errors0 warnings
3 lifecycle hooks

Understanding when your code runs is essential for writing correct strategies.

The Three Functions

Every QSL strategy has exactly three functions:

FunctionPurposeCalled
define(ctx)Declare parametersOnce when strategy loads
init(ctx)Declare indicators, initialize stateOnce before backtest
onBar(ctx, i)Trading logicOnce per candle

Execution Order

1. define(ctx)     — Parameters registered
2. init(ctx)       — Indicators declared, state initialized
3. [Engine computes all indicators]
4. For each bar i:
   a. onBar(ctx, i) — Your logic runs with pre-computed data
   b. Orders queued
5. [Engine processes all orders with fill rules]

Lifecycle Functions

define(ctx)

Declare your strategy parameters. Called once when the strategy loads.

function define(ctx) { ctx.param('fastLength', { type: 'int', label: 'Fast EMA', default: 9, min: 5, max: 50, optimize: true, group: 'Trend' }); ctx.param('slowLength', { type: 'int', label: 'Slow EMA', default: 21, min: 10, max: 200, optimize: true, group: 'Trend' }); }

init(ctx)

Declare indicators and initialize state. Called once before the backtest starts.

function init(ctx) { // Declare indicators (computed by engine, not your code) ctx.indicator('fastEma', 'EMA', { period: ctx.p.fastLength, source: 'close' }); ctx.indicator('slowEma', 'EMA', { period: ctx.p.slowLength, source: 'close' }); // Initialize state ctx.state.tradeCount = 0; ctx.state.lastSignalBar = -1; }

Key insight: Indicators are computed by the engine on the host side. By the time onBar runs, all indicator values for all bars are already calculated.

onBar(ctx, i)

Your trading logic. Called for every bar in the dataset.

function onBar(ctx, i) { const fast = ctx.ind.fastEma[i]; const slow = ctx.ind.slowEma[i]; // Skip if indicators not ready if (q.isNaN(fast) || q.isNaN(slow)) return; // Check for crossover if (q.crossOver(ctx.ind.fastEma, ctx.ind.slowEma, i)) { ctx.order.market('ASSET', 1, { signal: 'buy' }); ctx.state.tradeCount++; } if (q.crossUnder(ctx.ind.fastEma, ctx.ind.slowEma, i)) { ctx.order.close('ASSET', { signal: 'sell' }); } }

Important rules:

  • onBar runs after the bar closes
  • You see the bar's OHLCV data, but cannot act within it
  • Orders placed here execute on the next bar's open

Accessing Data in onBar

function onBar(ctx, i) { // Parameters const period = ctx.p.period; // Indicators (pre-computed arrays) const ema = ctx.ind.ema[i]; const macd = ctx.ind.macd.macd[i]; // Multi-output indicators // Price series const close = ctx.series.close[i]; const prevClose = ctx.series.close[i - 1]; const high = ctx.series.high[i]; // Position info const pos = ctx.position('ASSET'); if (pos.qty > 0) { /* in position */ } // Mutable state ctx.state.lastSignalBar = i; }

Stateless vs Stateful

Strategies can be stateless (logic based only on current bar and indicators) or stateful (maintaining custom state across bars).

Stateless Example

function onBar(ctx, i) { // Only uses current values — no memory needed if (ctx.ind.rsi[i] < 30 && ctx.position('ASSET').qty === 0) { ctx.order.market('ASSET', 1, { signal: 'buy' }); } }

Stateful Example

function onBar(ctx, i) { const rsi = ctx.ind.rsi[i]; // Track consecutive oversold bars if (rsi < 30) { ctx.state.oversoldCount = (ctx.state.oversoldCount || 0) + 1; } else { ctx.state.oversoldCount = 0; } // Enter after 3 consecutive oversold bars if (ctx.state.oversoldCount >= 3 && ctx.position('ASSET').qty === 0) { ctx.order.market('ASSET', 1, { signal: 'buy', reason: 'oversold_3bars' }); } }

Common Mistakes

  1. Assuming orders fill immediately — They don't. Orders fill on the next bar.
  2. Computing indicators in onBar — Use ctx.indicator() in init() instead.
  3. Forgetting to check q.isNaN() — Indicators have warm-up periods.
  4. Using Math.random() — Non-deterministic logic breaks reproducibility.

Related

lifecycleexecutiondefineinitonBarqsl