Every QSL strategy follows the same lifecycle. Understanding this lifecycle is the single most important concept in QSL, because it determines when your code runs and what data is available at each stage.
1. define() runs once when the strategy loads. Its only job is to declare parameters — the values you want to adjust and optimize. Think of it as filling out a form: "My strategy has a fast EMA period, a slow EMA period, and a stop-loss multiplier."
function define(ctx) {
ctx.param('fastLength', {
type: 'int', label: 'Fast EMA', default: 9,
min: 5, max: 50, step: 5, optimize: true
});
ctx.param('slowLength', {
type: 'int', label: 'Slow EMA', default: 21,
min: 10, max: 200, step: 10, optimize: true
});
}
Each parameter has a type, a default value, and an allowed range. The optimize: true flag tells the engine this parameter should be included in optimization and Walk-Forward Analysis. For the full parameter API, see the define() reference.
2. init() runs once before the backtest begins. This is where you declare which indicators to compute. You do not calculate them yourself — you tell the engine what you need, and it pre-computes them across the entire price series.
function init(ctx) {
ctx.indicator('fastEma', 'EMA', {
period: ctx.p.fastLength, source: 'close', display: true
});
ctx.indicator('slowEma', 'EMA', {
period: ctx.p.slowLength, source: 'close', display: true
});
}
Notice that indicator periods reference ctx.p.fastLength — the parameter declared in define(). This connection is what makes optimization work: when the engine varies fastLength from 5 to 50, the EMA recalculates automatically. The full list of available indicators is in the indicator reference.
3. onBar() runs once per candle, from the first bar to the last. This is your trading logic — the part that decides whether to buy, sell, or do nothing.
function onBar(ctx, i) {
const { fastEma, slowEma } = ctx.ind;
if (q.isNaN(fastEma[i]) || q.isNaN(slowEma[i])) return;
if (q.crossOver(fastEma, slowEma, i)) {
ctx.order.market('ASSET', 1, {
signal: 'buy', reason: 'fast_crosses_above_slow'
});
}
if (q.crossUnder(fastEma, slowEma, i)) {
ctx.order.close('ASSET', {
signal: 'sell', reason: 'fast_crosses_below_slow'
});
}
}
The i parameter is the current bar index. You access indicator values with ctx.ind.fastEma[i] and price data with ctx.series.close[i]. The q namespace provides helper functions like crossOver, crossUnder, and isNaN. For the full order and series API, see the onBar() reference.
Key idea: define() sets the knobs. init() requests the data. onBar() makes the decisions. Everything else is handled by the engine.