Documentation

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

  1. While your strategy runs, you compute one or more indicator series (a list of {"time": ..., "value": ...} rows).
  2. You add each series to a ChartIndicatorSeries collection with display options (which pane, line vs. histogram, optional name/color/symbol).
  3. At the end of the backtest, the engine merges that collection into the result under chart_indicator_series.
  4. 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_indicators instance you can import — so you usually don't build your own:

from ide.backtest.runtime import chart_indicators

Anything you .add(...) on it is automatically merged into the final result. You only need to construct your own ChartIndicatorSeries() 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:

KeyTypeNotes
timenumber / datetime / ISO stringNormalized to Unix seconds (int). Use the bar's timestamp from bars[i]["t"] (or coerce_chart_time(t) if you want to convert manually).
valuenumberMust be finiteNaN / 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 bars argument of on_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(...):

ArgumentRequiredValues
paneyes"main" — overlay on the price/candle chart. "separate" — its own pane below the chart (good for RSI, MACD, etc.).
renderyes"line" or "histogram".
line_stylewhen render="line""solid" or "dotted". Omit for histograms.
idnoStable identifier for the series; auto-generated if you don't provide one. Useful for keeping the same color/order across runs.
namenoDisplay name shown on the chart legend. Defaults to id.
colornoColor hint (e.g. "#22c55e").
symbolnoUppercase 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:

  • pane controls vertical placement:
    • "main" — sits on top of the candles of its chart.
    • "separate" — gets its own pane under the candles.
  • symbol controls 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 bandspane="main", multiple series.
  • RSI / Stochastic / ADXpane="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 — the id is 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 client

merge_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:

FieldDescription
id, name, pane, render, pointsExactly what you passed to add(...).
line_style"solid" / "dotted" for lines, null for histograms.
colorPresent only if you passed color=.
symbolPresent 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 chronological bars list aligned to the execution window.
  • Use bar timestamps for time. Pulling time from bars[i]["t"] keeps your series perfectly aligned with the chart candles.
  • Drop NaNs early. Series with NaN / inf values 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 same id across 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. Set line_style only for render="line".