Skip to content
Single .md

Panes: overlay vs new pane

Each plot has a visual destination: it appears over the price chart (overlay) or in a separate pane below (new pane). The choice depends on the indicator's scale.

This page is focused on where a plot lands. For the full canonical shape — params and colors first, the dispatcher, and the math — follow Anatomy of a custom indicator. Every declaration below mirrors that keystone.

Rule of thumb

Indicator typePaneReason
Moving averages (SMA, EMA, VWAP)"overlay"Scale matches the price
Bollinger Bands"overlay"Same scale as price
Donchian channels, pivots"overlay"Price levels
RSI, Stochastic"new"0-100 scale, incompatible with price
MACD, Histogram"new"Scale oscillates around zero
Volume"new"Completely different scale
ATR"new"Absolute volatility scale

Accepted values

pane has exactly four valid values — anything else is rejected by the validator:

python
"pane": "overlay"   # over the price chart (default)
"pane": "price"     # synonym of "overlay"
"pane": "new"       # dedicated new pane below
"pane": "same"      # uses the active pane (useful in composed scripts)

There is no per-plot pane: pane lives at the root of the DECLARATION, so every plot in one script shares one pane.

Examples

Each declaration keeps params and colors at the top, the way the anatomy keystone prescribes. Remember the sharp edge a type:"color" input does not auto-recolor a plot — you must read params.get("<name>") and inject it into the plot's color.

Overlay: moving averages over price

python
# Indicator is pre-injected (same kernels the chart renders with) — NO import.

COLOR_SMA = "#22D3EE"   # cyan
DEFAULT_PERIOD = 20

DECLARATION = {
    "type": "indicator",
    # PARAMS + COLORS FIRST — the whole tuning surface, before any math.
    "inputs": [
        {"name": "period", "type": "int", "default": DEFAULT_PERIOD,
         "min": 1, "max": 200, "step": 1},
        {"name": "line_color", "type": "color", "default": COLOR_SMA},
    ],
    "plots": [
        {"name": "sma", "source": "sma", "type": "line",
         "color": COLOR_SMA, "width": 2},
    ],
    "pane": "overlay",   # drawn together with the candles
    "scale": "none",     # inherit the price scale
}


def _declaration(params):
    # A color parameter is NOT auto-applied — read it and wire it into the plot.
    color = (params or {}).get("line_color", COLOR_SMA)
    plots = [dict(p) for p in DECLARATION["plots"]]
    plots[0]["color"] = color
    return {**DECLARATION, "plots": plots}


def main(df=None, sdk=None, params={}):
    params = params or {}
    if df is not None:
        period = int(params.get("period", DEFAULT_PERIOD))
        closes = df["close"].tolist()
        return {**_declaration(params), "series": {"sma": Indicator.sma(closes, period)}}
    return _declaration(params)

Result: a moving average over the price chart, in the user's chosen color because line_color is wired into the plot.

New pane: RSI in a dedicated pane

An oscillator on a 0-100 scale must use "pane": "new". On a high-priced asset (Bitcoin at 50000) an RSI left on the price pane collapses onto y=0 — you get a legend chip but no visible line.

python
# Indicator is pre-injected — NO import.

COLOR_RSI = "#A78BFA"   # violet
DEFAULT_PERIOD = 14

DECLARATION = {
    "type": "indicator",
    # PARAMS + COLORS FIRST.
    "inputs": [
        {"name": "period", "type": "int", "default": DEFAULT_PERIOD,
         "min": 2, "max": 100, "step": 1},
        {"name": "line_color", "type": "color", "default": COLOR_RSI},
    ],
    "plots": [
        {"name": "rsi", "source": "rsi", "type": "line",
         "color": COLOR_RSI, "width": 2},
    ],
    "pane": "new",       # separate pane — required for a 0-100 oscillator
    "scale": "right",    # most oscillators read better on the right
    "levels": [
        {"name": "Overbought", "value": 70, "color": "#EF4444", "style": "dashed"},
        {"name": "Midline",    "value": 50, "color": "#64748B", "style": "dotted"},
        {"name": "Oversold",   "value": 30, "color": "#22C55E", "style": "dashed"},
    ],
}


def _declaration(params):
    # Wire the color parameter into the plot — it does not apply on its own.
    color = (params or {}).get("line_color", COLOR_RSI)
    plots = [dict(p) for p in DECLARATION["plots"]]
    plots[0]["color"] = color
    return {**DECLARATION, "plots": plots}


def main(df=None, sdk=None, params={}):
    params = params or {}
    if df is not None:
        period = int(params.get("period", DEFAULT_PERIOD))
        closes = df["close"].tolist()
        return {**_declaration(params), "series": {"rsi": Indicator.rsi(closes, period)}}
    return _declaration(params)

Result: a pane below the price chart, with the RSI line in the chosen color and three fixed levels (70, 50, 30).

New pane: MACD with three plots

MACD oscillates around zero, so it also lives in "pane": "new". The periods and colors are declared first; the full per-plot color wiring follows the same pattern as above (see anatomy for the complete multi-color _declaration that injects each picked color).

python
# Indicator is pre-injected — NO import.

COLOR_MACD   = "#22D3EE"   # cyan
COLOR_SIGNAL = "#F59E0B"   # amber
COLOR_HIST   = "#94A3B8"   # slate

DECLARATION = {
    "type": "indicator",
    # PARAMS + COLORS FIRST — periods and the three line colors.
    "inputs": [
        {"name": "fast",   "type": "int", "default": 12, "min": 1, "max": 200, "step": 1},
        {"name": "slow",   "type": "int", "default": 26, "min": 2, "max": 400, "step": 1},
        {"name": "signal", "type": "int", "default": 9,  "min": 1, "max": 100, "step": 1},
        {"name": "macd_color",   "type": "color", "default": COLOR_MACD},
        {"name": "signal_color", "type": "color", "default": COLOR_SIGNAL},
    ],
    "plots": [
        {"name": "macd",        "source": "macd",        "type": "line",      "color": COLOR_MACD,   "width": 2},
        {"name": "signal_line", "source": "signal_line", "type": "line",      "color": COLOR_SIGNAL, "width": 2},
        {"name": "hist",        "source": "hist",        "type": "histogram", "color": COLOR_HIST},
    ],
    "pane": "new",       # oscillates around zero — own pane
    "scale": "right",
    "levels": [
        {"name": "Zero", "value": 0, "color": "#64748B", "style": "dotted"},
    ],
}


def _declaration(params):
    p = params or {}
    plots = [dict(plot) for plot in DECLARATION["plots"]]
    plots[0]["color"] = p.get("macd_color", COLOR_MACD)      # line colors are
    plots[1]["color"] = p.get("signal_color", COLOR_SIGNAL)  # read + injected,
    return {**DECLARATION, "plots": plots}                   # never auto-applied


def main(df=None, sdk=None, params={}):
    params = params or {}
    if df is not None:
        p = params
        macd, signal, hist = Indicator.macd(
            df,
            int(p.get("fast", 12)),
            int(p.get("slow", 26)),
            int(p.get("signal", 9)),
        )
        return {**_declaration(params),
                "series": {"macd": macd, "signal_line": signal, "hist": hist}}
    return _declaration(params)

All three plots share the same new pane (because pane is at the root level of the DECLARATION, not on each plot). Indicator.macd returns three parallel lists — (macd, signal, hist) — straight from the kernels the chart renders with, with no import.

Customizing the scale

The scale key controls which side the Y axis of the pane appears on. It has exactly three valid values:

ValueEffect
"right"Scale on the right side (default on the price chart)
"left"Scale on the left side (useful when you already have something on the right)
"none"No visible Y axis

For an overlay over price, set "scale": "none" — the price scale is already rendered.

python
"pane": "overlay",
"scale": "none",   # moving averages inherit the price scale

Levels: fixed horizontal lines

levels draws fixed horizontal lines that do not change with the data. Suitable for overbought/oversold, theoretical pivots, and global take-profits.

python
"levels": [
    {"name": "TP 10%",    "value": 110.0, "color": "#22C55E", "style": "dashed"},
    {"name": "Break-Even","value": 100.0, "color": "#64748B", "style": "solid"},
    {"name": "Stop -5%",  "value": 95.0,  "color": "#EF4444", "style": "dashed"},
],

Level colors are 6-digit hex only (#RRGGBB); CSS names and 8-digit alpha are not honored on levels. In the context of an RSI, levels go in the same pane as the plot (pane: "new"). In the context of overlay over price, they are drawn on top of the candles.

Plots in different panes (advanced)

Current support is one pane per DECLARATION. For plots in separate panes (for example, moving averages on the overlay and RSI in a new pane within the same script), create two scripts: one for the overlay indicator and another for the RSI.

For combined strategies (logic in a single script), keep all plots in the same pane and use levels to mark reference points.

Combining plots: histogram and line

It is common to want a line and a histogram in the same pane. Classic example: MACD + signal line + histogram:

python
"plots": [
    {"name": "macd",        "source": "macd",        "type": "line",      "color": "#22D3EE", "width": 2},
    {"name": "signal_line", "source": "signal_line", "type": "line",      "color": "#F59E0B", "width": 2},
    {"name": "hist",        "source": "hist",        "type": "histogram", "color": "#94A3B8"},
],
"pane": "new",

The frontend renders in declaration order: first the macd line (behind), then signal line, then histogram (on top). To prevent the histogram from hiding the lines, keep the histogram last. For a translucent fill use a "type": "area" plot — the renderer applies the fill alpha for you (8-digit #RRGGBBAA colors are dropped, so do not encode transparency in the hex). Use width for line thickness; lineWidth is silently ignored.

Value-driven colors (two-color histograms)

For green-positive and red-negative histograms without two series, use colorExpression:

python
{
    "name": "hist",
    "source": "hist",
    "type": "histogram",
    "colorExpression": "value >= 0 ? '#22C55E' : '#EF4444'",
}

The expression is evaluated per value: each bar may have its own color.

Common errors

  • Plot in the right pane but not appearing: check the source (key in series) and the length of the series.
  • RSI in overlay over price: price may be at 50000 (Bitcoin) and the RSI at 70. The RSI becomes a flat line glued to zero. Use "pane": "new".
  • Color picker shows but the line never changes: a type:"color" input is not auto-applied. Read params.get("<name>") and inject it into the plot's color (see the examples above and anatomy).
  • Levels not appearing: make sure levels is at the root of the DECLARATION, not inside a plot.
  • New pane without scale: if scale is omitted, the frontend tries to pick one, with inconsistent results. Pass "scale": "left" or "right" explicitly for new panes.

Next steps