Getting Started
Custom indicators in BacktestingMax let you write any technical indicator in Python and use it on your charts and in backtests. You write a small Python class, we run it sandboxed, and your output appears on the chart as lines, fills, markers, or boxes. Here's a working example, then we'll walk through it.
You don't have to write the Python yourself. Describe the indicator you want in AI Chat, or press the AI button inside the editor, and the code is written for you. These docs are then how you read, check, and tweak what it produced — or how you write one from scratch if you prefer.
The default template
When you open the indicator editor, the source pane is seeded with this:
from indicator_api import Indicator
class MyIndicator(Indicator):
name = "My Indicator"
params = {
"period": {"type": "int", "default": 14, "min": 2, "max": 200},
}
def init(self, ctx):
self.sma = ctx.rolling_mean(source=ctx.close, period=ctx.params.period)
def on_bar(self, ctx):
ctx.plot.series(
"sma", value=self.sma.value,
color="#2196f3", line_width=1.5,
label=f"SMA({ctx.params.period})",
)
Reading line by line:
from indicator_api import Indicator— the only import you need to start.Indicatoris the base class your indicator must subclass.class MyIndicator(Indicator):— every indicator is a subclass ofIndicator. The class name is for you; the runtime finds your indicator by locating theIndicatorsubclass in your file.name = "My Indicator"— optional metadata shown in the chart legend and your library. See Scripting basics.params = {...}— declares what the user can tweak in the side panel. See Parameters for every type and option.def init(self, ctx):— runs once, before the first bar. Create the calculation helpers you need here and store them onself. This one creates a rolling-mean (simple moving average) helper over the close.def on_bar(self, ctx):— runs once per bar, in time order. Read the helper's current value (self.sma.value) and draw it withctx.plot.series(...). It returns nothing — you draw by callingctx.plot.*, not by returning a value.
The two-method model (read this once)
This is the single most important concept, so it gets its own section.
An indicator works one bar at a time:
| Method | Runs | What it's for |
|---|---|---|
init(self, ctx) | Once, before any bars | Create helpers (ctx.rolling_mean, ctx.ema, …) and initialise any state you'll carry from bar to bar. Store everything on self. |
on_bar(self, ctx) | Once per bar, in time order | Read helper values and the current bar, do per-bar logic, and draw via ctx.plot.*. |
State lives on self between bars. To remember the previous bar's value, stash it on self at the end of on_bar and read it back next time. This is expected and required — keeping state on self is how the bar-by-bar model works.
Because on_bar only ever sees the current bar and bars before it, your indicator cannot read the future — so a backtest built on it is honest by design. (More in Errors.)
Coming from Pine Script?
If you've written Pine before, here's the rough translation. The big shift: you create stateful helpers in init, then read them and draw each bar in on_bar — you don't return anything.
Pine Ours
─────────────────────────────────────────────────────────────────────
ta.sma(close, 20) ctx.rolling_mean(source=ctx.close, period=20)
# create in init(), read .value in on_bar()
ta.ema(close, 20) ctx.ema(source=ctx.close, period=20)
ta.rsi(close, 14) ctx.rsi(source=ctx.close, period=14)
close ctx.close (float(ctx.close) for a number)
close[1] ctx.close[1] (1 bar back; None if N/A)
high - low ctx.high - ctx.low
plot(myval) ctx.plot.series("myval", value=myval)
plotshape(cond, ...) if cond: ctx.plot.shape("sig", price=..., shape="arrow_up")
ta.crossover(a, b) a > b and self._prev_a <= self._prev_b
# remember prev values on self in on_bar()
ta.crossover has no one-liner because crossover is inherently stateful here: store the previous bar's values on self at the end of on_bar, compare against them next bar. The tutorial does exactly this.
Anatomy of an indicator
| Part | What it is |
|---|---|
| Class | A subclass of Indicator. Holds metadata (name, description, optional id) and your params. |
| Params | A dict declaring the user-facing inputs. Each entry has a type, a default, and optional min / max / choices. |
init | Create helpers and state. Runs once. |
on_bar | Per-bar logic + drawing. Runs once per bar. |
ctx.plot | How you draw. There is no return value. |
The Validate → Save → Apply loop
The editor has three actions:
| Action | Hotkey | What it does |
|---|---|---|
| Validate | Ctrl/Cmd + Enter | Dry-runs your indicator against a fixed EURUSD 15m sample. Catches errors before you touch a live chart. Renders nothing. |
| Save | Ctrl/Cmd + S | Persists to your library as a new immutable version (1, 2, 3…). |
| Apply | Ctrl/Cmd + Shift + Enter | Adds the indicator to the chart you're looking at. Auto-saves first if the source has changed. |
What blocks each action:
- Validate is blocked by parse failures or a missing
Indicatorsubclass. - Save is blocked by security-check failures (forbidden imports or builtins) — see Errors.
- Apply runs the current source on the chart; it does not require a separate prior Validate.
⚠️ Gotcha: Validate runs on a fixed EURUSD 15m sample, not the chart you're looking at. If your indicator only works on a specific symbol or timeframe, Validate can pass while Apply on a different chart still surfaces a runtime error.
For a deeper tour of the editor — the library, versions, copying a built-in, publishing — see Working with the editor.
Where to go next
- Workflow: Working with the editor — the library, versions, copying built-ins, publishing.
- Reference: Scripting basics — how an indicator runs: the
Indicatorclass,init/on_bar, andctx. - Reference: Built-in Calculations — every helper (
rolling_mean,ema,rsi,macd,bollinger_bands, …). - Tutorial: Your first indicator — build a band envelope with cross markers, end to end.
- Reference: Plots and drawings — every
ctx.plot.*method with examples. - Reference: Parameters — every parameter type and the UI control it gets.