Quantago Q Library Reference
The Q object is available as a global inside every strategy sandbox. All
functions are pure, deterministic, and return null when there is
insufficient candle data — they never throw. Always null-check before using
a result.
Critical rules
// ✅ Q.* returns a SCALAR (single number or object) for the CURRENT candle
const ema = Q.ema(history, 50); // → number | null
const rsi = Q.rsi(history, 14); // → number | null
const bb = Q.bb(history, 20, 2); // → { lower, middle, upper, width, std } | null
// ✅ To get the PREVIOUS value, slice history
const emaPrev = Q.ema(history.slice(0, history.length - 1), 50);
// ❌ NEVER treat the result as an array
const v = Q.ema(history, 50);
v[v.length - 1]; // always undefined — v is a number, not an array
// ❌ NEVER use .mid or .avg on Bollinger Bands — use .middle
bb.mid; // undefined
bb.middle; // ✅ correct
Trend indicators
Q.sma(candles, period) → number | null
Simple Moving Average of close prices.
perioddefault: 20- Needs:
periodcandles
const sma20 = Q.sma(history, 20);
if (sma20 === null) return { action: 'HOLD', reason: 'Warming up', checks: [] };
Q.ema(candles, period) → number | null
Exponential Moving Average. Multiplier = 2/(period+1).
perioddefault: 20- Needs:
periodcandles
const ema50 = Q.ema(history, 50);
const ema200 = Q.ema(history, 200);
const trendUp = ema50 !== null && ema200 !== null && ema50 > ema200;
Q.dema(candles, period) → number | null
Double EMA — reduces lag vs EMA.
- Needs:
2 * periodcandles
Q.wma(candles, period) → number | null
Weighted Moving Average (linearly weighted, most recent gets highest weight).
perioddefault: 20- Needs:
periodcandles
Q.rma(candles, period) → number | null
Wilder's Smoothed Moving Average (same smoothing used inside ATR/RSI).
perioddefault: 14- Needs:
periodcandles
Q.wsma(candles, period) → number | null
Alias for Q.rma.
Q.vwap(candles) → number | null
Volume Weighted Average Price over all supplied candles.
Returns null if any candle has zero volume.
const vwap = Q.vwap(history);
const aboveVwap = vwap !== null && price > vwap;
Q.adx(candles, period) → number | null
Average Directional Index (0–100). Above 25 = trending, above 50 = strong trend.
perioddefault: 14- Needs:
period + 1candles
const adx = Q.adx(history, 14);
const isTrending = adx !== null && adx > 25;
Q.dx(candles, period) → { pdi, mdi, dx } | null
Directional Movement components.
pdi— positive DI (bullish pressure)mdi— negative DI (bearish pressure)dx— raw DX value
const dm = Q.dx(history, 14);
const bullish = dm !== null && dm.pdi > dm.mdi;
Q.psar(candles, step, max) → number | null
Parabolic SAR. Returns the current SAR price level.
stepdefault: 0.02,maxdefault: 0.2- Price above SAR → uptrend; price below SAR → downtrend
const sar = Q.psar(history, 0.02, 0.2);
const uptrend = sar !== null && price > sar;
Q.dma(candles, fastPeriod, slowPeriod) → { fast, slow, signal } | null
Dual Moving Average crossover.
signal:'BULLISH'|'BEARISH'|'SIDEWAYS'
const dma = Q.dma(history, 10, 50);
if (dma !== null && dma.signal === 'BULLISH') { /* buy */ }
Q.linreg(candles, period) → { value, slope, intercept } | null
Linear regression over period candles.
value— projected value at the current candleslope— positive = upward trend
const lr = Q.linreg(history, 20);
const trendingUp = lr !== null && lr.slope > 0;
Q.swingHigh(candles, lookback) → number | null
Price of the most recent confirmed swing high within lookback candles.
Returns null if no swing high is found.
const swHigh = Q.swingHigh(history, 20);
Q.swingLow(candles, lookback) → number | null
Price of the most recent confirmed swing low within lookback candles.
const swLow = Q.swingLow(history, 20);
Q.highest(candles, period, field) → number | null
Highest value of field over last period candles.
fielddefault:'high'. Other options:'close','open','low'
const highest20 = Q.highest(history, 20, 'high'); // 20-period high
const highestClose = Q.highest(history, 14, 'close');
Q.lowest(candles, period, field) → number | null
Lowest value of field over last period candles.
fielddefault:'low'
const lowest20 = Q.lowest(history, 20, 'low'); // 20-period low
Momentum indicators
Q.rsi(candles, period) → number | null
Relative Strength Index (0–100).
perioddefault: 14. Needsperiod + 1candles.- Below 30 = oversold (buy signal), above 70 = overbought (sell signal)
- For stricter strategies use 40/60 thresholds
const rsi = Q.rsi(history, 14);
if (rsi === null) return { action: 'HOLD', reason: 'Warming up', checks: [] };
const oversold = rsi < 40; // buy bias
const overbought = rsi > 60; // sell bias
Q.stoch(candles, kPeriod, dPeriod) → { k, d } | null
Stochastic Oscillator (0–100).
kPerioddefault: 14,dPerioddefault: 3- Below 20 = oversold, above 80 = overbought
const stoch = Q.stoch(history, 14, 3);
const stochOversold = stoch !== null && stoch.k < 20 && stoch.d < 20;
Q.macd(candles, fast, slow, signal) → { macd, signal, histogram } | null
MACD — Moving Average Convergence Divergence.
- Defaults: fast=12, slow=26, signal=9
- Needs:
slow + signalcandles histogram > 0= bullish momentum
const macd = Q.macd(history, 12, 26, 9);
const macdPrev = Q.macd(history.slice(0, history.length - 1), 12, 26, 9);
if (macd === null || macdPrev === null) return { action: 'HOLD', reason: 'Warming up', checks: [] };
// Bullish crossover: MACD line crosses above signal line
const bullCross = macdPrev.macd <= macdPrev.signal && macd.macd > macd.signal;
// Bearish crossover
const bearCross = macdPrev.macd >= macdPrev.signal && macd.macd < macd.signal;
// Positive histogram = bullish momentum
const bullMomentum = macd.histogram > 0;
Q.cci(candles, period) → number | null
Commodity Channel Index.
perioddefault: 20- Above +100 = overbought, below -100 = oversold
const cci = Q.cci(history, 20);
const cciOversold = cci !== null && cci < -100;
Q.roc(candles, period) → number | null
Rate of Change — % price change over period candles.
perioddefault: 12- Positive = upward momentum
Q.stochRsi(candles, rsiPeriod, stochPeriod, smoothK, smoothD) → { k, d } | null
Stochastic RSI. More sensitive than plain RSI or Stoch.
- Defaults: rsiPeriod=14, stochPeriod=14, smoothK=3, smoothD=3
const srsi = Q.stochRsi(history, 14, 14, 3, 3);
const srsiOversold = srsi !== null && srsi.k < 20;
Q.williamsR(candles, period) → number | null
Williams %R (−100 to 0).
perioddefault: 14- Below −80 = oversold, above −20 = overbought
const wr = Q.williamsR(history, 14);
const wrOversold = wr !== null && wr < -80;
Q.ao(candles, fastPeriod, slowPeriod) → number | null
Awesome Oscillator — difference of fast/slow SMAs of midpoints.
- Defaults: fast=5, slow=34
- Positive = bullish momentum
Q.ac(candles, fastPeriod, slowPeriod, smaPeriod) → number | null
Accelerator Oscillator — measures acceleration of momentum.
- Defaults: fast=5, slow=34, sma=5
Q.mom(candles, period) → number | null
Momentum — simple close difference: close[now] - close[now - period].
perioddefault: 10
Q.obv(candles) → number | null
On-Balance Volume — cumulative volume. Rising OBV = accumulation.
Returns null if any candle has zero volume.
Q.er(candles, period) → number | null
Kaufman Efficiency Ratio (0–1). 1 = perfectly trending, 0 = pure noise.
perioddefault: 10
Q.rei(candles, period) → number | null
Range Expansion Index (−100 to +100). Measures directional range expansion.
perioddefault: 8
Q.cg(candles, period) → number | null
Center of Gravity oscillator.
perioddefault: 10
Q.tds(candles, lookback) → { count, signal } | null
Tom DeMark Sequential setup counter.
signal:'BUY_SETUP'|'SELL_SETUP'|null- Signal fires when count reaches 9
const td = Q.tds(history, 4);
if (td !== null && td.signal === 'BUY_SETUP') { /* potential reversal up */ }
Volatility indicators
Q.bb(candles, period, dev) → { lower, middle, upper, width, std } | null
Bollinger Bands.
- Defaults: period=20, dev=2
- Needs:
periodcandles - Always use
.middle— never.mid,.avg, or.mean
const bb = Q.bb(history, 20, 2);
if (bb === null) return { action: 'HOLD', reason: 'Warming up', checks: [] };
const atLower = price <= bb.lower; // potential BUY
const atUpper = price >= bb.upper; // potential SELL
const squeeze = bb.width < 0.002; // low volatility squeeze
Q.atr(candles, period) → number | null
Average True Range — measures volatility.
perioddefault: 14- Needs:
period + 1candles - Use for stop loss sizing:
entry - atr * 1.5
const atr = Q.atr(history, 14);
if (atr === null) return { action: 'HOLD', reason: 'Warming up', checks: [] };
// ATR-based stop: 1.5× ATR below entry for longs
const stopLoss = Q.atrStop(price, atr, 1.5, 'LONG');
Q.tr(candles) → number | null
True Range for the most recent candle only. Needs at least 2 candles.
Q.abands(candles, period, factor) → { lower, middle, upper } | null
Acceleration Bands — wider than Bollinger in trending markets.
- Defaults: period=20, factor=0.001
Q.bbw(candles, period, dev) → number | null
Bollinger Band Width — (upper - lower) / middle. Measures relative volatility.
Q.iqr(candles, period) → number | null
Interquartile Range of closes. Robust volatility measure, outlier-resistant.
perioddefault: 20
Q.mad(candles, period) → number | null
Mean Absolute Deviation of closes. Another robust volatility measure.
perioddefault: 20
Volume indicators
All volume indicators return null when candles have zero volume.
Q.cmf(candles, period) → number | null
Chaikin Money Flow (−1 to +1). Above 0 = accumulation, below 0 = distribution.
perioddefault: 20
const cmf = Q.cmf(history, 20);
const accumulation = cmf !== null && cmf > 0.1;
Q.vwma(candles, period) → number | null
Volume Weighted Moving Average — like SMA but weights by volume.
perioddefault: 20
Q.ad(candles) → number | null
Accumulation/Distribution — cumulative money flow. Rising = accumulation.
Q.pvt(candles) → number | null
Price Volume Trend — cumulative. Needs at least 2 candles.
Q.emv(candles, period, divisor) → number | null
Ease of Movement — positive = rising with low volume (easy up move).
- Defaults: period=14, divisor=10000
Q.vroc(candles, period) → number | null
Volume Rate of Change (%). Large positive = volume spike.
perioddefault: 14
Utility functions
Q.crossover(prev, cur, level) → boolean
Returns true when a value crosses above a level between two ticks.
const rsiNow = Q.rsi(history, 14);
const rsiPrev = Q.rsi(history.slice(0, history.length - 1), 14);
const crossedAbove30 = Q.crossover(rsiPrev, rsiNow, 30); // RSI crossed up through 30
Q.crossunder(prev, cur, level) → boolean
Returns true when a value crosses below a level.
const crossedBelow70 = Q.crossunder(rsiPrev, rsiNow, 70); // RSI crossed down through 70
Q.atrStop(entry, atrVal, mult, side) → number
Calculates an ATR-based stop loss price.
side:'LONG'or'SHORT'- For LONG:
entry - atrVal * mult; for SHORT:entry + atrVal * mult
const atr = Q.atr(history, 14);
const slLong = Q.atrStop(price, atr, 1.5, 'LONG'); // stop below entry
const slShort = Q.atrStop(price, atr, 1.5, 'SHORT'); // stop above entry
Q.highest(candles, period, field) / Q.lowest(candles, period, field) → number | null
See Trend section above.
Q.grid(basePrice, stepPips, levels, pipSize) → number[]
Returns an array of price levels centred on basePrice.
pipSizedefault: 0.0001
const levels = Q.grid(price, 50, 5); // 11 levels, 50 pips apart
Q.streaks(candles) → { bullStreak, bearStreak }
Current consecutive up/down candle run. Never returns null.
const { bullStreak, bearStreak } = Q.streaks(history);
const strongUpMove = bullStreak >= 3;
Q.clamp(v, min, max) → number
Clamps v to [min, max]. Returns min if v is non-finite.
Q.avg(values) → number | null
Average of a plain number array (not candles).
Q.median(values) → number | null
Median of a plain number array.
Q.stdDev(values) → number | null
Standard deviation of a plain number array.
Q.max(values) / Q.min(values) → number | null
Max/min of a plain number array.
Q.quartile(values) → { q1, q2, q3 } | null
Quartiles of a plain number array. Needs at least 4 values.
Q.pipVal(price, pipSize) → number
Converts a price to pips. pipSize default: 0.0001.
Warmup reference
Minimum history length needed before a function returns a non-null value:
| Function | Min candles |
|---|---|
Q.sma(h, p) | p |
Q.ema(h, p) | p |
Q.rsi(h, p) | p + 1 |
Q.macd(h, 12, 26, 9) | 35 (slow + signal) |
Q.bb(h, p, d) | p |
Q.atr(h, p) | p + 1 |
Q.adx(h, p) | p + 1 |
Q.stoch(h, kP, dP) | kP + dP |
Q.stochRsi(h, r, s, k, d) | r + s + k + d |
Q.ema(h, 200) | 200 (de-facto warmup floor) |
Tip: gate every strategy with a warmup check based on the slowest indicator:
const WARMUP = 200;
if (history.length < WARMUP) return {
action: 'HOLD', reason: `Warming up (${history.length}/${WARMUP})`,
checks: [{ name: 'warmup', label: `Sufficient history (≥ ${WARMUP})`, passed: false, value: history.length }],
};
Common patterns
Trend-filtered momentum entry
const ema50 = Q.ema(history, 50);
const ema200 = Q.ema(history, 200);
const rsi = Q.rsi(history, 14);
const atr = Q.atr(history, 14);
if (!ema50 || !ema200 || !rsi || !atr) return { action: 'HOLD', reason: 'Warming up', checks: [] };
const trendUp = ema50 > ema200;
const trendDown = ema50 < ema200;
const oversold = rsi < 40;
const overbought = rsi > 60;
const checks = [
{ name: 'ema_ready', label: 'EMA(50/200) computed', passed: true },
{ name: 'rsi_ready', label: 'RSI(14) computed', passed: true },
{ name: 'trend_up', label: 'EMA(50) > EMA(200) — uptrend', passed: trendUp, decision: 'BUY' },
{ name: 'rsi_oversold', label: 'RSI(14) < 40 — oversold', passed: oversold, decision: 'BUY', value: rsi },
{ name: 'trend_down', label: 'EMA(50) < EMA(200) — downtrend', passed: trendDown, decision: 'SELL' },
{ name: 'rsi_overbought', label: 'RSI(14) > 60 — overbought', passed: overbought, decision: 'SELL', value: rsi },
];
if (trendUp && oversold) return { action: 'BUY', reason: 'Uptrend + RSI oversold', checks, metadata: { rsi, ema50, ema200 } };
if (trendDown && overbought) return { action: 'SELL', reason: 'Downtrend + RSI overbought', checks, metadata: { rsi, ema50, ema200 } };
return { action: 'HOLD', reason: 'No confluence', checks };
MACD crossover with ATR stop
const macd = Q.macd(history, 12, 26, 9);
const macdPrev = Q.macd(history.slice(0, history.length - 1), 12, 26, 9);
const atr = Q.atr(history, 14);
if (!macd || !macdPrev || !atr) return { action: 'HOLD', reason: 'Warming up', checks: [] };
const bullCross = macdPrev.macd <= macdPrev.signal && macd.macd > macd.signal;
const bearCross = macdPrev.macd >= macdPrev.signal && macd.macd < macd.signal;
const checks = [
{ name: 'macd_bull_cross', label: 'MACD bullish crossover', passed: bullCross, decision: 'BUY' },
{ name: 'macd_bear_cross', label: 'MACD bearish crossover', passed: bearCross, decision: 'SELL' },
];
if (bullCross) return { action: 'BUY', reason: 'MACD bull cross', checks,
metadata: { stopLoss: Q.atrStop(price, atr, 1.5, 'LONG') } };
if (bearCross) return { action: 'SELL', reason: 'MACD bear cross', checks,
metadata: { stopLoss: Q.atrStop(price, atr, 1.5, 'SHORT') } };
return { action: 'HOLD', reason: 'No crossover', checks };
Bollinger Band mean-reversion
const bb = Q.bb(history, 20, 2);
const rsi = Q.rsi(history, 14);
if (!bb || !rsi) return { action: 'HOLD', reason: 'Warming up', checks: [] };
const atLower = price <= bb.lower;
const atUpper = price >= bb.upper;
const checks = [
{ name: 'bb_lower', label: 'Price at/below lower Bollinger Band', passed: atLower, value: bb.lower, decision: 'BUY' },
{ name: 'rsi_oversold', label: 'RSI(14) < 35', passed: rsi < 35, value: rsi, decision: 'BUY' },
{ name: 'bb_upper', label: 'Price at/above upper Bollinger Band', passed: atUpper, value: bb.upper, decision: 'SELL' },
{ name: 'rsi_overbought', label: 'RSI(14) > 65', passed: rsi > 65, value: rsi, decision: 'SELL' },
];
if (atLower && rsi < 35) return { action: 'BUY', reason: 'Price at lower BB + oversold', checks };
if (atUpper && rsi > 65) return { action: 'SELL', reason: 'Price at upper BB + overbought', checks };
return { action: 'HOLD', reason: 'Price within bands', checks };