Appearance

Personalize your experience

Mode
Accent Color
Direction
Building Custom Indicators

Building Custom Indicators

Coding

Composing new indicators from existing ones, the recursive shapes that need care, warm-up handling, and how to check a custom indicator is actually correct.

Most custom indicators are compositions

Before writing anything, check whether the idea is already expressible. A surprising share of “custom indicators” are arithmetic over two built-ins, and arithmetic is available directly in a condition.

no custom code required
# oscillator spread — momentum divergence between horizons
RSI(14) - RSI(28)

# normalised volatility — comparable across instruments
ATR(14) / Close * 100

# Bollinger %B — where price sits inside the bands
(Close - BB_Lower(20,2)) / (BB_Upper(20,2) - BB_Lower(20,2) + 0.0001)

# volatility regime — is volatility expanding?
ATR(14) / ATR(14)[20]

# trend quality — slope measured against noise
LinRegSlope(Close, 20) / StdDev(Close, 20)
★
Pro tip:Compose first. A composition inherits correct warm-up handling, correct bar alignment and correct theming from its parts, and there is nothing to get wrong. Write a genuine custom indicator only when the calculation cannot be expressed as arithmetic over existing series.

The shape of an indicator

An indicator is a function from one or more series plus parameters to a new series of the same length. Two obligations come with that: the output must align bar-for-bar with the input, and the warm-up region must be marked rather than with a plausible-looking number.

the contract
type Series = number[];/** * Every indicator obeys three rules: *   1. output.length === input.length      (bar alignment) *   2. output[i] depends only on input[0..i]  (causality) *   3. output[i] is NaN while there is insufficient history (warm-up) */type Indicator = (source: Series, ...params: number[]) => Series;const sma: Indicator = (source, period) =>  source.map((_, i) => {    if (i < period - 1) return NaN;                     // rule 3    let sum = 0;    for (let k = i - period + 1; k <= i; k++) sum += source[k];  // rule 2    return sum / period;  });                                                    // rule 1 by construction
!
Caution:Rule 2 is the one that quietly breaks. Any calculation that reads source[i + 1] — a centred average, a “smoothed” series computed in both directions, a peak detector that confirms with the next bar — is look-ahead bias. It will not error. It will produce the best backtest you have ever seen.

Recursive indicators

and anything built on it are recursive: each depends on the previous output rather than on a . That makes them cheap — constant work per bar — and introduces one subtlety worth understanding: the seed.

ema, and why the seed matters
const ema = (source: Series, period: number): Series => {  const k = 2 / (period + 1);          // smoothing factor  const out: Series = [];  let prev = NaN;  source.forEach((value, i) => {    if (i < period - 1) { out.push(NaN); return; }       // warm-up    if (Number.isNaN(prev)) {      // seed with the SMA of the first full window, not with source[0]:      // seeding from a single bar makes the early output depend heavily on      // one arbitrary value, and that bias takes ~3× period bars to decay      let sum = 0;      for (let k2 = i - period + 1; k2 <= i; k2++) sum += source[k2];      prev = sum / period;    } else {      prev = value * k + prev * (1 - k);    }    out.push(prev);  });  return out;};
  • Seeding matters more than it looks. An seeded from a single bar carries that bar's influence for roughly three times the before it decays below noise. On a short backtest that region can be a meaningful share of the .
  • Recursive indicators never fully forget. Unlike a windowed average, an retains a decaying trace of all history. Two backtests over different start dates will produce slightly different EMA values on the same bar — which is a legitimate reason for two runs to disagree.
  • Cost is constant per bar. An does the same work whether the is 9 or 900, where an SMA does proportionally more. On long backtests with long periods that difference is measurable.

A worked custom indicator

Something not in the and not expressible as simple arithmetic: an efficiency ratio measuring how directly price travelled, which distinguishes a clean trend from a volatile that ended in the same place.

efficiency ratio
/** * Efficiency ratio — net movement divided by total path travelled. * *   1.0  price moved in a straight line *   0.1  price wandered ten times as far as it progressed * * The same net change can be a clean trend or a noisy chop, and a * trend-following strategy wants to know which. Kaufman uses this * ratio to adapt smoothing; it also works as a standalone filter. */function efficiencyRatio(close: Series, period: number): Series {  return close.map((_, i) => {    if (i < period) return NaN;    const net = Math.abs(close[i] - close[i - period]);    let path = 0;    for (let k = i - period + 1; k <= i; k++) {      path += Math.abs(close[k] - close[k - 1]);    }    // a completely flat window has zero path; guard the division    return path === 0 ? 0 : net / path;  });}
ℹ
Note:Read the guard on the last line. A where price did not move at all gives a zero denominator, which happens on halted instruments and illiquid far more often than it sounds. Returning 0 — “no ” — is the honest answer; returning NaN would also be defensible, but returning infinity is not.
using it as a filter
# only take trend entries when price is actually trending, not chopping
AND {
  EMA(21)  >  EMA(55)
  Close  crossesAbove  EMA(21)
  EfficiencyRatio(20)  >  0.3
}

Verifying correctness

A custom indicator that is subtly wrong is worse than none, because every built on it inherits the error. Four checks catch nearly everything.

CheckTypeDescription
LengthstructuralOutput length equals input length. A shorter output silently misaligns every bar after the offset, so a condition compares this bar’s price against a different bar’s indicator.
Warm-upstructuralThe first N−1 values are NaN, not zero. Zero is a number a comparison can succeed against, which produces in a region where the indicator has no meaning.
Known valuesnumericalCompute a few values by on a short series and . Tedious once, and it catches off-by-one boundaries that no amount of inspection will.
Prefix stabilitycausalityRun it on the first 100 bars, then on the first 200, and the first 100 outputs are identical. If they differ, the indicator is reading the .

Practical guidance

Keep parameters few

Every parameter on a custom indicator is a parameter you will tune, and every tuned parameter is a of freedom fitted against your test . An indicator with four parameters offers far more ways to fit noise than it offers insight.

Normalise the output

An indicator returning price units is not comparable across instruments, so any threshold you set on it is -specific. Prefer output that is a ratio, a or a Z-score — then one condition works everywhere.

Prefer boring over clever

A composition of two built-ins that you fully understand beats a novel calculation you do not. The edge in a almost never comes from the indicator being unusual; it comes from the conditions around it being sound.

!
Caution:A custom indicator is also a maintenance liability. It has no reference implementation to check against, no community expectations about its behaviour, and nobody but you knows what it should do at the boundaries. Weigh that against what it actually adds before committing to one.

See also

Related articles

  • Coding for Trading — Fundamentals

    Coding / Fundamentals

    The concepts every strategy rests on, whether you write it in the condition builder or in code: series, bar indexing, the difference between a value and a…

  • Script Best Practices

    Coding / IDE / Best Practices

    Follow these guidelines to write performant, reliable, and maintainable trading scripts.

  • Strategy DSL — Execution Model

    Coding / Strategy DSL

    What the engine does on every bar: protective exits, exit signals, entries, layers and re-entries, sizing and costs, and where every statistic in the result…

  • Writing Indicators in the IDE

    Coding / IDE / Indicators

    Copy the template below into a new Indicator file, click Run to calculate, then Apply to draw it on the chart.

  • Condition Evaluator

    Automation / Strategy Builder / Reference

    How a condition becomes true on a bar: series computed up front, no value during warm-up, the exact cross and rising rules, and the order of work on each…

  • Built-in Indicators & Overlays

    Coding / IDE / Reference

    Quatick ships with a full library of pre-built indicators and overlays. You can use these out of the box, or study their implementations as reference for…

  • Condition Recipes

    Coding / Recipes

    Patterns worth knowing by heart: regime detection, confirmation, divergence, volatility gating, session control, and the idioms that express each one cleanly.

  • Debugging Strategies

    Coding / Debugging

    A method for diagnosing strategies that do nothing, do too much, or do the wrong thing — plus the specific symptoms that have specific causes.

  • Evaluation Performance

    Coding / Performance

    Why a backtest is slow and what actually fixes it: condition ordering, indicator reuse, incremental computation, and the difference between an O(n) and an…

Browse all knowledge base articles →