Common patterns

Short, copy-pasteable recipes for the things you'll do most. Each one is a complete, working indicator — paste it into the editor, hit Validate, and tweak from there.

If you've read Getting Started and Scripting basics, you have the prerequisites. Every recipe follows the same shape: create helpers and state in init, read and draw in on_bar.

Reference the previous bar

ctx.close[1] is the previous bar's close — one bar back, never the future. It returns None when there is no prior bar, so guard it.

from indicator_api import Indicator
class PrevClose(Indicator):
    def on_bar(self, ctx):
        ctx.plot.series("prev", value=ctx.close[1],
                        color="#9e9e9e", label="Previous close")

Plots the previous bar's close (a gap on bar 0, where there is none).

Highest high / lowest low over N bars

ctx.rolling_max / ctx.rolling_min give you a Donchian-style channel in two helpers.

from indicator_api import Indicator
class Donchian(Indicator):
    params = {"period": {"type": "int", "default": 20, "min": 2, "max": 200}}
    def init(self, ctx):
        self.hi = ctx.rolling_max(source=ctx.high, period=ctx.params.period)
        self.lo = ctx.rolling_min(source=ctx.low,  period=ctx.params.period)
    def on_bar(self, ctx):
        ctx.plot.series("upper", value=self.hi.value, color="#22c55e",
                        label=f"High({ctx.params.period})")
        ctx.plot.series("lower", value=self.lo.value, color="#ef4444",
                        label=f"Low({ctx.params.period})")

Plots the rolling highest high and lowest low — a basic Donchian channel.

💡 Tip: Windowed helpers return None until their window is full — pass that straight to ctx.plot.series and it renders as a gap at the left edge. That's expected warm-up, not a bug. Most recipes below have it.

Crossover detection

Crossover is stateful here: remember the previous bar's values on self, compare against them next bar.

from indicator_api import Indicator
class EmaCross(Indicator):
    def init(self, ctx):
        self.fast = ctx.ema(source=ctx.close, period=20)
        self.slow = ctx.ema(source=ctx.close, period=50)
        self._pf = None
        self._ps = None
    def on_bar(self, ctx):
        f, s = self.fast.value, self.slow.value
        if f is None or s is None:
            ctx.plot.series("fast", value=None)
            ctx.plot.series("slow", value=None)
            return
        ctx.plot.series("fast", value=f, color="#2196f3", label="Fast")
        ctx.plot.series("slow", value=s, color="#9e9e9e", label="Slow")
        if self._pf is not None:
            if self._pf <= self._ps and f > s:
                ctx.plot.shape("x", price=float(ctx.low),
                               shape="arrow_up", color="#22c55e",
                               position="below_bar")
            elif self._pf >= self._ps and f < s:
                ctx.plot.shape("x", price=float(ctx.high),
                               shape="arrow_down", color="#ef4444",
                               position="above_bar")
        self._pf, self._ps = f, s

Plots two EMAs and drops a green arrow on cross-up, red on cross-down.

Color a histogram by sign

Pass color= on every series call; when it changes from bar to bar, the line is drawn multi-coloured.

from indicator_api import Indicator
class MacdHistogram(Indicator):
    def init(self, ctx):
        self.macd = ctx.macd(source=ctx.close, fast=12, slow=26, signal=9)
    def on_bar(self, ctx):
        h = self.macd.histogram
        if h is None:
            ctx.plot.series("hist", value=None, pane="sub")
            return
        ctx.plot.series("hist", value=h, pane="sub", style="histogram",
                        color="#22c55e" if h > 0 else "#ef4444",
                        label="MACD hist")

Plots the MACD histogram in a sub-pane, green above zero, red below.

Horizontal line at yesterday's close

Track the close at the end of every bar; when a new daily session starts, the last bar's close is yesterday's close. Plot it as a flat series.

from indicator_api import Indicator
class YdayClose(Indicator):
    def init(self, ctx):
        self.sess = ctx.session_window(anchor="daily")
        self._yday = None
        self._last_close = None
    def on_bar(self, ctx):
        if self.sess.just_started and self._last_close is not None:
            self._yday = self._last_close
        ctx.plot.series("yday", value=self._yday, color="#e58b39",
                        line_width=1.0, label="Yesterday's close")
        self._last_close = float(ctx.close)

Draws a horizontal line at yesterday's daily close, stepping each new day.

Mark every new 20-bar high

Compare the current high to the rolling max ending one bar back.

from indicator_api import Indicator
class NewHighs(Indicator):
    params = {"period": {"type": "int", "default": 20, "min": 5, "max": 200}}
    def init(self, ctx):
        self.prior_max = ctx.rolling_max(source=ctx.high,
                                         period=ctx.params.period)
        self._prev_max = None
    def on_bar(self, ctx):
        if self._prev_max is not None and ctx.high > self._prev_max:
            ctx.plot.shape("new_high", price=float(ctx.high),
                           shape="arrow_up", color="#22c55e",
                           position="above_bar")
        self._prev_max = self.prior_max.value

Drops a green arrow above every bar that prints a new N-bar high. _prev_max holds the rolling max as of the previous bar, so the current bar is compared against history only.

Use the built-in RSI

The helper library covers the classics — reach for ctx.rsi / ctx.macd / ctx.atr before any hand-rolled equivalent.

from indicator_api import Indicator
class Rsi(Indicator):
    params = {"period": {"type": "int", "default": 14, "min": 2, "max": 100}}
    def init(self, ctx):
        self.rsi = ctx.rsi(source=ctx.close, period=ctx.params.period)
    def on_bar(self, ctx):
        ctx.plot.series("rsi", value=self.rsi.value, pane="sub",
                        color="#9c27b0", label=f"RSI({ctx.params.period})")
        ctx.plot.rectangle("band", price_top=70, price_bottom=30,
                           fill_color="#9c27b0", fill_opacity=0.06, pane="sub")

Plots RSI in a sub-pane with a shaded 30–70 band.

Show a value as text on the chart

ctx.plot.text pins a string to a (time, price). Called every bar with the same name it's last-write-wins, so it ends up on the latest bar with the latest value.

from indicator_api import Indicator
class AtrLabel(Indicator):
    params = {"period": {"type": "int", "default": 14, "min": 2, "max": 100}}
    def init(self, ctx):
        self.atr = ctx.atr(period=ctx.params.period)
    def on_bar(self, ctx):
        a = self.atr.value
        if a is None:
            return
        ctx.plot.text("atr", time=ctx.bar_time, price=float(ctx.high),
                       text=f"ATR({ctx.params.period}) = {a:.5f}",
                       anchor="bottom-left", color="#ffffff",
                       background="#0d1117", size=12)

Pins the current ATR value as a small label above the most recent bar.

Where to go next

Documentation