Building Indicators#
TracerAI can write an indicator for you from a plain-English description. When you want full control, you write one yourself in Tracer Script, a JavaScript dialect that runs against your chart. This page builds one from scratch, adds settings and live data, then covers the limits worth knowing. For the full list of functions and data sources, see the Scripting Reference.
How an indicator works#
An indicator is a function that runs once per candle. Tracer calls your onBar(i, ctx)
for each bar i. You read the candle, compute a value, and plot it. Each plotted value
lines up with its bar, so plotting across every bar draws a line on the chart.
The whole script re-runs from the top on every update, so treat it as a pure function of
the candles rather than a long-running program. Anything you need to remember between bars
goes in ctx.state, which is covered below.
Your first indicator#
An indicator has three parts: a header line that names it, an onBar function, and at
least one plot. Here is the smallest one that works. It draws the close price back onto
the chart.
// @indicator { name: "My First Indicator", overlay: true }
function onBar(i, ctx) {
plot("close", candles[i].close, { color: "#4ea1ff" });
}- The header is a comment on line 1.
overlay: truedraws on the price pane, andfalsedraws in a pane below the chart. candlesis always in scope. Eachcandles[i]hastime,open,high,low,close, andvolume.plot(label, value, style)adds a point to the line namedlabel.
To run it, open TracerAI, open the editor, paste the code, and click Attach. It shows up on your chart right away. See the TracerAI Overview for the editor and the attach flow.
TODO: screenshot of a simple custom indicator running on the chart (the result of attaching the example above).
Add settings#
Hard-coded numbers are fine to start, but you will want knobs. Declare them in a top-level
getSettings() and Tracer builds the settings panel for you. Read the chosen values from
ctx.settings. This version plots a moving average with an adjustable length and color.
// @indicator { name: "My EMA", overlay: true }
const closes = candles.map(c => c.close);
function getSettings() {
return [
{ key: "length", type: "number", default: 20, min: 1, max: 400, step: 1, label: "Length" },
{ key: "color", type: "color", default: "#4ea1ff", label: "Line color" },
];
}
function onBar(i, ctx) {
const value = ta.ema(closes, ctx.settings.length, i);
if (value === null) { return; }
plot("ema", value, { color: ctx.settings.color });
}- Setting types are
number(with optionalmin,max,step),color,boolean, andselect(which takesoptions: [{ value, label }]). - Build a plain array once at the top with
candles.map(...)and pass it to theta.*helpers. The candle data is read-only, so map it into your own array before transforming. - The
ta.*helpers returnnulluntil they have enough bars to warm up. Always null-check before plotting, or you draw an invisible gap. - Pass your color setting on every
plotcall so restyling from the panel takes effect.
TODO: screenshot of the auto-generated settings panel for the indicator (the Length and Line color inputs).
Keep state across bars#
Because the script re-runs from the top each time, top-level variables are rebuilt on every
run. The one thing that survives between bars is ctx.state. Set it up on the first bar,
which you detect with ctx.isFirst.
There is one subtlety with the live candle. While a bar is still forming, Tracer re-runs
onBar for that same bar on every tick, with the same ctx.state. A running total like
state.sum += delta would double-count the live bar. Bank the closed total instead, and
recompute the live bar from it.
function onBar(i, ctx) {
if (ctx.isFirst) {
ctx.state.base = 0;
ctx.state.sum = 0;
ctx.state.lastTime = null;
}
const c = candles[i];
const delta = c.close >= c.open ? c.volume : -c.volume;
if (c.time !== ctx.state.lastTime) { // a new bar opened
if (ctx.state.sum != null) { ctx.state.base = ctx.state.sum; }
ctx.state.lastTime = c.time;
}
ctx.state.sum = ctx.state.base + delta; // live bar recomputed from scratch
plot("cvd", ctx.state.sum, { color: "#a78bfa" });
}Treat ctx.state as a cache, not permanent storage. It is cleared whenever the script
warms up again, which happens on any settings, symbol, or interval change. Your first-bar
setup has to rebuild everything from the candles alone.
Pull in Tracer's data#
Your script can read Tracer's aggregated feeds, not just the chart's candles. Declare each
one at the top level with source(kind, options) or a tracer.* accessor. They must be
declared at the top, unconditionally, with literal options. A source() call inside
onBar throws, and Tracer matches fetched data back to your declarations by their order,
so a declaration hidden behind an if desyncs the data.
const oi = source("oi");
function onBar(i, ctx) {
if (!oi.values.oiUsd) { return; }
const v = oi.values.oiUsd[i];
if (Number.isFinite(v)) { plot("oi", v); }
}A few things to expect:
- Missing or failed data comes back as an empty-but-shaped payload, never an error inside
onBar. Check that a column exists before you read it. - Per-candle feeds like open interest and funding are lined up one-to-one with your
candles, with
NaNwhere there is no data, so NaN-check before doing math. Liquidation buckets are zero-filled, where0means none. - One unit gotcha: the
fromandtooptions you pass tosource()are unix milliseconds, while every timestamp you read back (candletime,values.time) is unix seconds.
Draw more than lines#
Beyond lines, you can draw entities and tables. Entities are markers, lines, boxes, and
labels placed at a time and price. Tables are small on-chart readouts. Entities are keyed,
so re-emitting the same key updates that object in place, and handle.delete() removes one.
Give each logical object a stable key, and delete signals that flip off so a stale marker
does not linger. The full entity and table API, with every field, is in the
Scripting Reference.
Limits and rules#
A handful of rules keep scripts safe and fast. You will run into these.
- Plot
null, neverNaNorInfinity, for a gap. The validator rejects non-finite values. Wrap risky division inNumber.isFinitebefore plotting. - Brace every loop body. A bare
fororwhilewithout{ }is rejected. - The candle and data arrays are read-only. Map them into your own arrays to transform.
- Only the built-in helpers exist:
candles,plot,entity,source,tracer,ta,math, andcolor, plus standardMath,Array,Number, andJSON. There is nofetchor network access, so all market data comes throughsource(). - Index arrays with numbers.
candles[i]andcloses[i - 1]are fine. Only string keys likeobj["name"]are blocked, which a normal indicator never needs.
Keep onBar cheap. Every ta.* call re-walks the series from the start, so calling
several on a long chart adds up. When your output only depends on the final state, such as a
level line or a table, gate that work behind ctx.isLast and skip the warmup bars.
Hard limits
- Plots: 20 lines, 10 fills, 100,000 values per line.
- Entities: 10,000 per script.
- Tables: 5 tables, up to 50 rows by 12 columns, 300 cells each and 1,000 cells total. Cell text is truncated to 64 characters.
- Compute budget: 5 seconds, 1,000,000 operations, 1,000,000 loop iterations, and a call depth of 64 per run. A script that blows the budget is stopped and restarts cold.
- Data feeds clamp silently: open interest to 500 rows, funding to 1,000, liquidations to 50,000 events.
Exceeding any cap fails the whole output, not just the excess.
Learn more#
- Scripting Reference lists every function, helper, and data source you can call.
- TracerAI Overview covers letting the AI write or edit an indicator for you.