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.
The functions you define
Section titled “The functions you define”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.
What onIntervalClose returns
Section titled “What onIntervalClose returns”- 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
valueand display settings:name,color,type,overlay(falsedraws it below the price),dashStyle,precision. The settings matchplot(). - 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.
What an indicator can read
Section titled “What an indicator can read”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,errorandconsole.
An indicator cannot read the current tick (BID, ASK, SPREAD), the account, or positions, and cannot trade or plot().
Example
Section titled “Example”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.
Writing it in TypeScript
Section titled “Writing it in TypeScript”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.
Calling it from an algorithm
Section titled “Calling it from an algorithm”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.