Skip to main content

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.

  • period default: 20
  • Needs: period candles
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).

  • period default: 20
  • Needs: period candles
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 * period candles

Q.wma(candles, period) → number | null​

Weighted Moving Average (linearly weighted, most recent gets highest weight).

  • period default: 20
  • Needs: period candles

Q.rma(candles, period) → number | null​

Wilder's Smoothed Moving Average (same smoothing used inside ATR/RSI).

  • period default: 14
  • Needs: period candles

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.

  • period default: 14
  • Needs: period + 1 candles
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.

  • step default: 0.02, max default: 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 candle
  • slope — 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.

  • field default: '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.

  • field default: 'low'
const lowest20 = Q.lowest(history, 20, 'low');   // 20-period low

Momentum indicators​

Q.rsi(candles, period) → number | null​

Relative Strength Index (0–100).

  • period default: 14. Needs period + 1 candles.
  • 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).

  • kPeriod default: 14, dPeriod default: 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 + signal candles
  • 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.

  • period default: 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.

  • period default: 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).

  • period default: 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].

  • period default: 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.

  • period default: 10

Q.rei(candles, period) → number | null​

Range Expansion Index (−100 to +100). Measures directional range expansion.

  • period default: 8

Q.cg(candles, period) → number | null​

Center of Gravity oscillator.

  • period default: 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: period candles
  • 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.

  • period default: 14
  • Needs: period + 1 candles
  • 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.

  • period default: 20

Q.mad(candles, period) → number | null​

Mean Absolute Deviation of closes. Another robust volatility measure.

  • period default: 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.

  • period default: 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.

  • period default: 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.

  • period default: 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.

  • pipSize default: 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:

FunctionMin 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 };