PatternFoundry
← Back to Docs

Custom Indicators

Overview

PatternFoundry includes a powerful custom indicator system that lets you write your own technical indicators using JavaScript. Custom indicators can be displayed as overlays on the price chart or as separate oscillator panels below the chart.

The indicator engine provides access to all candle data and a library of helper functions for common calculations. You write a function that receives candle data and returns computed values - PatternFoundry handles the rendering.

Opening the Editor

To open the custom indicator editor in the Simulator:

  1. Click the { } button in the indicator toolbar (top of chart)
  2. The code editor panel opens on the right side of the chart
  3. Write your indicator code in the editor
  4. Click Apply (or press Ctrl+Enter) to render the indicator

Writing Indicator Code

Your indicator code is a JavaScript function body that has access to the following data arrays:

VariableTypeDescription
candlesArrayFull array of candle objects: {time, open, high, low, close, volume}
closeArrayArray of {time, value} pairs for closing prices
openArrayArray of {time, value} pairs for opening prices
highArrayArray of {time, value} pairs for high prices
lowArrayArray of {time, value} pairs for low prices
volumeArrayArray of {time, value} pairs for volume

Your code must return a result in one of the supported formats (see Return Formats below).

Helper Functions

The following helper functions are available in the indicator environment:

sma(candles, period) Simple Moving Average

Computes the arithmetic mean of closing prices over the specified period.

// Returns: [{time, value}, ...]
const ma20 = sma(candles, 20);
// Each value = (sum of last 20 closes) / 20

ema(candles, period) Exponential Moving Average

Computes an exponentially weighted moving average. More responsive to recent price changes than SMA. Uses smoothing factor k = 2 / (period + 1).

// Returns: [{time, value}, ...]
const fast = ema(candles, 9);
const slow = ema(candles, 21);

rsi(candles, period) Relative Strength Index

Computes the RSI oscillator (0–100). Measures the ratio of average gains to average losses over the period. Values above 70 indicate overbought; below 30 indicate oversold.

// Returns: [{time, value}, ...] where value is 0-100
const rsiData = rsi(candles, 14);

atr(candles, period) Average True Range

Computes the average true range - a volatility measure. True range is the greatest of: current high minus low, absolute value of high minus previous close, absolute value of low minus previous close.

// Returns: [{time, value}, ...]
const atrData = atr(candles, 14);

stdev(candles, period) Standard Deviation

Computes the rolling standard deviation of closing prices over the period. Useful for Bollinger Bands and volatility analysis.

// Returns: [{time, value}, ...]
const sd = stdev(candles, 20);

highest(candles, period) Highest Value

Returns the highest closing price over the lookback period at each point. Useful for Donchian channels and breakout detection.

// Returns: [{time, value}, ...]
const hh = highest(candles, 20);

lowest(candles, period) Lowest Value

Returns the lowest closing price over the lookback period at each point.

// Returns: [{time, value}, ...]
const ll = lowest(candles, 20);

crossover(a, b) Cross Above Detection

Returns an array of {time, value} where value is 1 when array a crosses above array b, and 0 otherwise. Both arrays must have {time, value} format.

const fast = ema(candles, 9);
const slow = ema(candles, 21);
const bullCross = crossover(fast, slow);
// bullCross[i].value === 1 when fast crosses above slow

crossunder(a, b) Cross Below Detection

Returns an array of {time, value} where value is 1 when array a crosses below array b, and 0 otherwise.

const bearCross = crossunder(fast, slow);

align(a, b) Timestamp Alignment

Aligns two arrays by their timestamps, returning only points where both arrays have data. Useful when combining indicators with different start points (due to different lookback periods).

// Returns: [{time, a: valueA, b: valueB}, ...]
const aligned = align(fast, slow);
// aligned[i] = {time: ..., a: fast_value, b: slow_value}

Return Formats

Your indicator must return data in one of these formats:

Single Line

An array of {time, value} objects. Rendered as a single line on the chart.

// Simple SMA overlay
return sma(candles, 20);

Colored Histogram

An array of {time, value, color} objects. Rendered as a histogram with per-bar coloring.

// Momentum histogram with color
const result = candles.map((c, i) => ({
  time: c.time,
  value: c.close - c.open,
  color: c.close > c.open ? '#4caf82' : '#e05c5c'
}));
return result;

Multi-Line

An object with a lines array, where each line has its own data, label, color, style, and width.

return {
  lines: [
    { data: upperBand, label: 'Upper', color: '#f97316', style: 'solid', width: 1 },
    { data: middle,    label: 'Mid',   color: '#888',    style: 'dashed', width: 1 },
    { data: lowerBand, label: 'Lower', color: '#f97316', style: 'solid', width: 1 }
  ]
};

Available line styles: 'solid', 'dashed', 'dotted'. Width is in pixels (1–4).

Display Types

Set the display type using the dropdown in the editor toolbar (next to the indicator name). Choose:

The display type is NOT set in your code - it's selected from the dropdown before clicking Apply.

Examples

EMA Crossover Oscillator

Shows the difference between fast and slow EMA as a colored histogram:

const fast = ema(candles, 9);
const slow = ema(candles, 21);
const aligned = align(fast, slow);

const hist = aligned.map(p => ({
  time: p.time,
  value: p.a - p.b,
  color: p.a > p.b ? '#4caf82' : '#e05c5c'
}));

return hist;

Bollinger Bands (Multi-Line)

Classic Bollinger Bands with upper, middle, and lower bands:

const period = 20;
const mult = 2;
const mid = sma(candles, period);
const sd = stdev(candles, period);
const aligned = align(mid, sd);

const upper = aligned.map(p => ({ time: p.time, value: p.a + mult * p.b }));
const lower = aligned.map(p => ({ time: p.time, value: p.a - mult * p.b }));
const middle = aligned.map(p => ({ time: p.time, value: p.a }));

return {
  type: 'overlay',
  lines: [
    { data: upper, label: 'BB Upper', color: '#f97316', style: 'solid', width: 1 },
    { data: middle, label: 'BB Mid', color: '#666', style: 'dashed', width: 1 },
    { data: lower, label: 'BB Lower', color: '#f97316', style: 'solid', width: 1 }
  ]
};

RSI with Overbought/Oversold Coloring

RSI histogram that changes color at extreme levels:

const rsiData = rsi(candles, 14);

const colored = rsiData.map(p => ({
  time: p.time,
  value: p.value,
  color: p.value > 70 ? '#e05c5c' :
         p.value < 30 ? '#4caf82' : '#888'
}));

return colored;

Custom Momentum Indicator

Rate of change smoothed with an EMA, displayed as a zero-line oscillator:

const period = 10;
const smoothing = 3;

// Calculate rate of change
const roc = [];
for (let i = period; i < candles.length; i++) {
  roc.push({
    time: candles[i].time,
    open: candles[i].open, high: candles[i].high,
    low: candles[i].low, volume: candles[i].volume,
    close: ((candles[i].close - candles[i - period].close) / candles[i - period].close) * 100
  });
}

// Smooth with EMA
const smoothed = ema(roc, smoothing);

const result = smoothed.map(p => ({
  time: p.time,
  value: p.value,
  color: p.value > 0 ? '#4caf82' : '#e05c5c'
}));

return result;

Saving & Loading Indicators

To save a custom indicator for reuse:

  1. Write and test your indicator in the editor
  2. Click the Save button (floppy disk icon) in the editor toolbar
  3. Enter a name for your indicator (e.g., "My Bollinger Bands", "Momentum Oscillator")
  4. The indicator is saved to your account

To load a saved indicator:

  1. Click the indicator dropdown in the chart toolbar
  2. Your saved indicators appear under the "Custom" section
  3. Click to add it to the chart - it renders immediately

The indicator legend (top-left of chart or oscillator panel) shows all active indicators. For each indicator you can: