Skip to content

Indicator SDK

Custom indicators

A custom indicator is a script, written in the same editor as an algorithm, that Cloud9Trader runs once per bar. You give it a key, such as myMomentum, and algorithms call it by that key like a built-in: myMomentum(10). For a walk-through, see Write a custom indicator, and for what each return shape looks like on a chart, Writing your own indicators.

Each function is called with the indicator’s settings, the arguments the algorithm passed, in order. Only onIntervalClose is required.

Function Called Return
validate(...settings) Once, before the run. Nothing. Call error(message) to reject the settings, or return false (“Validation failed”).
getRunUpCount(...settings) Once, before the run. How many bars before the start to run the indicator over, so it has a value from the first bar. Default 1, at most 1000.
getBufferSize(...settings) Once, before the run. How many past bars price() and prices() need. Default 0, at most 1000.
getStudyAxisConfig(...settings) Once, before the run. Optional chart settings for the indicator’s own axis, such as reference lines.
onStart(...settings) Once, before the first bar. Nothing. Set up labels or running totals here.
onIntervalClose(...settings) When each bar closes. The indicator’s value for that bar (below).

A run-up or buffer size that is not a whole number is rounded up, with a warning in the log. One above 1000 stops the run.

  • A number: one line.
  • null: no value for this bar yet, for example while a running average fills up.
  • An array of two numbers: a shaded range.
  • An object with a value and display settings: name, color, type, overlay (false draws it below the price), dashStyle, precision. The settings match plot().
  • An array of the above: several lines. An algorithm gets back an array of their values, in the same order.

An algorithm receives the numbers: each object becomes its value. A value that is not a number, null, a two-number array or an object stops the run with “Value returned by onIntervalClose must be a number, an array containing 2 numbers, a config object or null”. NaN is skipped on a chart but stops a test, backtest or live session (“NaN value returned by onIntervalClose”), so check your indicator with New Test before you use it.

During onIntervalClose, the bar that just closed:

  • TIME: the bar’s close time. OPEN, HIGH, LOW, CLOSE: its mid prices. PRICE: its mid, bid and ask prices. VOLUME: its volume.
  • VALUE: the value this indicator returned for the previous bar.
  • INSTRUMENT, PRECISION, PIP_SIZE, INTERVAL, INTERVAL_MS.
  • price(), prices(), the timeframes (D1.prices.high(5)), the Math helpers, and other indicators by key.
  • log, error and console.

An indicator cannot read the current tick (BID, ASK, SPREAD), the account, or positions, and cannot trade or plot().

let label;
function validate(periods) {
if (typeof periods !== "number" || periods % 1 !== 0 || periods < 1 || periods > 100) {
error("periods must be a whole number from 1 to 100");
}
}
function getBufferSize(periods) {
return periods + 1;
}
function onStart(periods) {
label = "Momentum(" + periods + ")";
}
function onIntervalClose(periods) {
const closes = prices(periods + 1);
return { name: label, value: closes[0] - closes[periods], overlay: false };
}

prices() lists the newest close first, so closes[0] - closes[periods] is the change over periods bars.

You can write the same indicator in TypeScript through the API. The type definitions are served at GET /api/indicator-sdk. They are the ones the 67 built-in indicators are compiled against, so a built-in you call returns number | null | undefined until it has warmed up, and the compiler makes you check it:

let label: string;
function getRunUpCount(periods: number): number {
return periods * 2;
}
function getBufferSize(periods: number): number {
return periods;
}
function onStart(periods: number): void {
label = "Distance from EMA(" + periods + ")";
}
function onIntervalClose(periods: number): IndicatorResult {
const average = ema(periods);
if (average == null) {
return null;
}
return { name: label, value: CLOSE - average, overlay: false };
}

POST /api/technical-indicators/compile with {"tsSource": "..."} checks it without saving and returns {ok, diagnostics, js}. PUT /api/technical-indicator/{id} with {"name": "...", "tsSource": "..."} compiles it and saves it; if it does not compile, it answers 400 with each error’s line, column and message, and nothing is saved. The source can be up to 200,000 characters and cannot import or export anything. GET /api/technical-indicator/{id} returns tsSource beside the compiled script. The editor in the app edits JavaScript, and saving from it replaces the TypeScript source.

When your indicator calls a built-in, give it a getBufferSize at least as large as the built-in’s period, as above, so the prices it needs are loaded before the run starts.

function onIntervalClose() {
if (myMomentum(10) > 0 && OPEN_POSITIONS_COUNT === 0) {
openPosition("BUY", { riskPercentage: 1, stopLossPips: 20 });
}
}

In a TypeScript algorithm, declare it first: declare function myMomentum(periods: number): number;. Each set of settings counts towards the limit of 10 indicators per algorithm.