Indicator Series
When a backtest finishes, the Test results view in the IDE renders charts of your run. Indicator series let your strategy attach extra time series — moving averages, RSI, MACD histograms, custom signals — to those charts so your indicators show up next to the OHLCV candles, in their own panes, or per-symbol on multi-symbol runs. Populate series from on_finish or earlier callbacks as described in Strategy lifecycle.
The output appears in the result payload under chart_indicator_series, and the IDE turns each entry into a line or histogram overlay on the corresponding chart pane.
How it works
- While your strategy runs, you compute one or more indicator series (a list of
{"time": ..., "value": ...}rows). - You add each series to a
ChartIndicatorSeriescollection with display options (which pane, line vs. histogram, optional name/color/symbol). - At the end of the backtest, the engine merges that collection into the result under
chart_indicator_series. - The IDE chart in Test Results renders each series with the options you chose.
Inside a backtest, the engine already provides a ready-to-use
chart_indicatorsinstance you can import — so you usually don't build your own:from ide.backtest.runtime import chart_indicatorsAnything you
.add(...)on it is automatically merged into the final result. You only need to construct your ownChartIndicatorSeries()outside a backtest run.
Quick start
The most common pattern: compute series from the chronological bars list you receive in on_finish, then add them to chart_indicators:
from ide.backtest.runtime import chart_indicators
def on_finish(bars, symbol=None, bars_by_symbol=None):
if not bars:
return
times = [int(b["t"]) if isinstance(b["t"], (int, float)) else b["t"] for b in bars]
closes = [float(b["close"]) for b in bars]
sma_points = []
window = 20
for i in range(window - 1, len(closes)):
avg = sum(closes[i - window + 1 : i + 1]) / window
sma_points.append({"time": times[i], "value": avg})
chart_indicators.add(
sma_points,
pane="main",
render="line",
line_style="dotted",
id="sma_20",
name="SMA(20)",
)The result chart then shows your SMA(20) as a dotted overlay on the candle chart.
Building series — the points list
Every series is a list of dicts. Each row needs:
| Key | Type | Notes |
|---|---|---|
time | number / datetime / ISO string | Normalized to Unix seconds (int). Use the bar's timestamp from bars[i]["t"] (or coerce_chart_time(t) if you want to convert manually). |
value | number | Must be finite — NaN / inf rows are dropped. |
Other keys on the dict are ignored, and rows with bad time or non‑finite value are silently dropped.
Tip: prefer building series from the
barsargument ofon_finish(bars, ...)(chronological OHLC, oldest first) rather than from intra-loop state. It keeps the indicator and chart aligned to the same execution window.
Series options (per add call)
You set these once per series, when you call chart_indicators.add(...):
| Argument | Required | Values |
|---|---|---|
pane | yes | "main" — overlay on the price/candle chart. "separate" — its own pane below the chart (good for RSI, MACD, etc.). |
render | yes | "line" or "histogram". |
line_style | when render="line" | "solid" or "dotted". Omit for histograms. |
id | no | Stable identifier for the series; auto-generated if you don't provide one. Useful for keeping the same color/order across runs. |
name | no | Display name shown on the chart legend. Defaults to id. |
color | no | Color hint (e.g. "#22c55e"). |
symbol | no | Uppercase ticker (e.g. "MSFT"). For multi-symbol backtests, pins the series to that symbol's chart. Omit for the clock / primary symbol. |
Where each series shows up
Two settings together decide where the series renders:
panecontrols vertical placement:"main"— sits on top of the candles of its chart."separate"— gets its own pane under the candles.
symbolcontrols which chart in a multi-symbol run:- omitted — attaches to the primary / clock symbol's chart (this matches the default for single-symbol runs).
"AAPL"(etc.) — attaches only to that ticker's chart.
For single-symbol backtests you can ignore symbol entirely; everything goes on the one chart.
Lines vs. histograms
Line overlays (render="line")
Use for continuous series that align with closes:
- Moving averages (SMA, EMA) — usually
pane="main". - Bollinger / Keltner bands —
pane="main", multiple series. - RSI / Stochastic / ADX —
pane="separate"so the price scale isn't squashed.
Pick line_style:
"solid"for primary indicators."dotted"for secondary / reference series (a slow MA next to a fast one, an upper band).
chart_indicators.add(rsi_points, pane="separate", render="line", line_style="solid",
id="rsi_14", name="RSI(14)")Histograms (render="histogram")
Use for bar/column style indicators (positive and negative values rendered as bars):
- MACD histogram, volume‑style scores, signal strength indicators.
chart_indicators.add(
[{"time": t, "value": v} for t, v in zip(hist_times, hist_vals)],
pane="separate",
render="histogram",
id="macd_hist",
name="MACD Hist",
)line_style is not used for histograms — leave it out.
Multi-symbol backtests
In multi-symbol runs, every loaded ticker has its own chart in Test Results. Use symbol="..." to send a series to a specific chart, and call add once per ticker:
def on_finish(bars, symbol=None, bars_by_symbol=None):
if not bars_by_symbol:
return
for ticker, ticker_bars in bars_by_symbol.items():
sma_points = compute_sma(ticker_bars, window=20)
chart_indicators.add(
sma_points,
pane="main",
render="line",
line_style="solid",
id=f"sma_20_{ticker.lower()}",
name="SMA(20)",
symbol=ticker,
)Series without a symbol go on the clock / primary chart. Series with a symbol go only on that ticker's chart.
Tip: keep the same
name(e.g."SMA(20)") across symbols — theidis what needs to be unique.
Multiple series at once
You can add as many series as you want — call add(...) once per series:
chart_indicators.add(sma20_points, pane="main", render="line", line_style="solid", id="sma_20", name="SMA(20)")
chart_indicators.add(sma50_points, pane="main", render="line", line_style="dotted", id="sma_50", name="SMA(50)")
chart_indicators.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)")
chart_indicators.add(macd_hist, pane="separate", render="histogram", id="macd", name="MACD Hist")Building series outside a backtest
If you're not in a backtest (for example you're shaping a result dict in your own script and want to attach an indicator overlay), construct a ChartIndicatorSeries yourself and merge it into the result:
from ide.indicator_series_module import ChartIndicatorSeries, coerce_chart_time
points = [
{"time": 1_700_000_000, "value": 150.0},
{"time": 1_700_008_640, "value": 151.2},
]
series = ChartIndicatorSeries()
series.add(
points,
pane="main",
render="line",
line_style="dotted",
id="sma_20",
name="SMA(20)",
)
test_result = {"success": True, "stdout": "..."}
test_result = series.merge_into(test_result)
# test_result["chart_indicator_series"] -> ready for the chart clientmerge_into(result) shallow-copies the result and sets the chart_indicator_series key for you. Inside a normal backtest you don't need to do this — the engine already merges chart_indicators for you.
Result payload shape (for reference)
Each entry in chart_indicator_series ends up as:
| Field | Description |
|---|---|
id, name, pane, render, points | Exactly what you passed to add(...). |
line_style | "solid" / "dotted" for lines, null for histograms. |
color | Present only if you passed color=. |
symbol | Present only if you passed symbol= — uppercase ticker so the IDE can place it on the right chart. |
Each points row is normalized to just {"time": int, "value": float}.
Tips and gotchas
- Compute in
on_finish. It's the natural place for whole‑run series and gives you the chronologicalbarslist aligned to the execution window. - Use bar timestamps for
time. Pullingtimefrombars[i]["t"]keeps your series perfectly aligned with the chart candles. - Drop NaNs early. Series with
NaN/infvalues get those rows dropped silently — it's better to skip them in your code so the visible series stays continuous. - Stable
ids help styling. Reusing the sameidacross runs lets the chart hold colors and ordering steady. - Pick the right pane. Bounded indicators (RSI 0–100) belong in
"separate"— putting them on"main"will warp the price scale. - Histogram = no
line_style. Setline_styleonly forrender="line".