Build your first custom indicator¶
This page builds a Donchian channel: the highest high and lowest low of the last N bars, and the line halfway between them.
1. Install LumiBot¶
pip install lumibot
2. Write the class¶
from collections import deque
from lumibot.indicators import Indicator, InputSpec, OutputSpec, register
@register
class DonchianMidline(Indicator):
key = "donchian_mid"
version = 1
title = "Donchian Channel Midline"
short_title = "DCM"
overlay = True # same pane as the price
inputs = [InputSpec("length", "int", 20, min=1, label="Length")]
outputs = [
OutputSpec("upper", "Upper", "line", "#22C55E"),
OutputSpec("mid", "Midline", "line", "#F59E0B"),
OutputSpec("lower", "Lower", "line", "#EF4444"),
]
def init_state(self):
n = self.inputs["length"]
return {"highs": deque(maxlen=n), "lows": deque(maxlen=n)}
def step(self, state, bar):
state["highs"].append(bar.high)
state["lows"].append(bar.low)
if len(state["highs"]) < self.inputs["length"]:
return {"upper": None, "mid": None, "lower": None}
upper, lower = max(state["highs"]), min(state["lows"])
return {"upper": upper, "mid": (upper + lower) / 2, "lower": lower}
What each part does:
keyandversionThe name you use to find it, and the version of its math. If you change the formula later, raise
versionso saved charts and strategies that used the old numbers keep them.inputsThe settings a person can change (here,
length).outputsWhat it draws: three lines.
init_state()The data that changes from bar to bar. Keep all of it here.
step(state, bar)Called once per bar. Update
stateand return this bar’s values. ReturnNonewhile there is not enough data yet. Never invent a number.@registerAdds it to LumiBot’s registry so every part of LumiBot can find it by
key.
3. Run it on bars, then feed it live bars¶
"""Run a custom indicator on bars, then feed it live bars.
Uses made-up prices so it runs anywhere without a network. Swap ``df`` for
any OHLCV DataFrame (a CSV, yfinance, your broker) with a time index.
python -m lumibot.example_indicators.quickstart
"""
import numpy as np
import pandas as pd
from lumibot.example_indicators.donchian_midline import DonchianMidline
def main():
# 1. Some daily bars (index = bar time; columns open/high/low/close/volume).
index = pd.date_range("2025-01-01", periods=60, freq="1D", tz="UTC")
close = 100 + np.cumsum(np.random.default_rng(0).normal(0, 1, len(index)))
df = pd.DataFrame({"open": close, "high": close + 1, "low": close - 1, "close": close, "volume": 1000.0}, index=index)
# 2. Whole series at once: lists aligned with the bars, None while warming up.
series = DonchianMidline(length=20).compute(df)
print("last 3 midline values:", series["mid"][-3:])
# 3. Streaming: history first, then live bars one at a time.
dcm = DonchianMidline(length=20)
dcm.feed(df) # every history bar is closed
next_time = index[-1] + pd.Timedelta(days=1)
tick = {"time": next_time, "open": close[-1], "high": close[-1] + 5, "low": close[-1] - 1, "close": close[-1] + 4}
print("forming:", dcm.update(tick, closed=False)) # live tick: nothing saved
print("closed: ", dcm.update(tick, closed=True)) # bar finished: saved
return series, dcm
if __name__ == "__main__":
main()
compute returns lists that line up with your bars. The first 19 values
are None because 20 bars are needed. update(bar, closed=False) gives a
live value for a forming bar without saving anything; closed=True saves
it. Run it with python -m lumibot.example_indicators.quickstart.
4. Use it in a strategy¶
from lumibot.example_indicators import donchian_midline # registers "donchian_mid"
def on_trading_iteration(self):
dcm = self.indicators.get("donchian_mid", self.asset, length=20)
if dcm and dcm["mid"] is not None and self.get_last_price(self.asset) > dcm["upper"]:
...
See Using an indicator in a backtest and Using an indicator in a live strategy.
Three rules that keep your numbers right¶
All changing data goes in
state. LumiBot copies it to try a forming bar, then throws the copy away. Data saved onselfwould leak live ticks into history. (Outside data you download is the exception; see Using outside data in an indicator.)Never look ahead.
steponly sees the current bar. A value for bar k may only use bars up to k.Do not recompute history. Keep running sums or a fixed window. Then every new bar costs the same small amount of time.
The full contract is on Building indicators.