Using an indicator in a backtest¶
Inside a strategy, self.indicators.get(key, asset, **settings) returns the
indicator’s values for the current bar as a dict:
rsi = self.indicators.get("rsi", asset, length=14) # {"rsi": 48.3}
dcm = self.indicators.get("donchian_mid", asset, length=20) # {"upper": ..., "mid": ..., "lower": ...}
Only bars at or before the strategy’s current time are used. A backtest cannot see the future through an indicator.
The indicator keeps its state between calls, so each new bar costs one update, not a full recompute.
A value is
Nonewhile the indicator is warming up.Noneoverall means there are no bars yet.timestep="minute"(or"day", the default) picks the bar size.version=1pins the math so a later change cannot move your results.
A complete strategy¶
This strategy uses two custom indicators from Indicator examples. It is a teaching example, not a recommendation.
"""One strategy that uses a custom indicator, in a backtest and live.
The strategy buys when the Donchian midline turns up and the up/down ratio is
above 50, and sells when the ratio drops below 50. The same class runs in a
backtest and against a live broker; only how you start it changes. This is a
teaching example, not a recommendation.
Backtest with free Yahoo data::
python -m lumibot.example_indicators.indicator_strategy
"""
from __future__ import annotations
from lumibot.entities import Asset
from lumibot.strategies import Strategy
# Importing the modules registers their indicators.
from lumibot.example_indicators import donchian_midline, up_down_ratio # noqa: F401
class CustomIndicatorStrategy(Strategy):
parameters = {"symbol": "SPY", "length": 20}
def initialize(self):
self.sleeptime = "1D"
self.vars.previous_mid = None
self.vars.log = []
def on_trading_iteration(self):
asset = Asset(self.parameters["symbol"])
length = self.parameters["length"]
mid = self.indicators.get("donchian_mid", asset, length=length)
ratio = self.indicators.get("up_down_ratio", asset, length=14)
if not mid or not ratio or mid["mid"] is None or ratio["ratio"] is None:
return # still warming up
rising = self.vars.previous_mid is not None and mid["mid"] > self.vars.previous_mid
self.vars.previous_mid = mid["mid"]
self.vars.log.append((self.get_datetime(), mid["mid"], ratio["ratio"]))
position = self.get_position(asset)
if rising and ratio["ratio"] > 50 and position is None:
quantity = int(self.get_cash() // self.get_last_price(asset))
if quantity > 0:
self.submit_order(self.create_order(asset, quantity, "buy"))
elif ratio["ratio"] < 50 and position is not None and position.quantity > 0:
self.submit_order(self.create_order(asset, position.quantity, "sell"))
if __name__ == "__main__": # pragma: no cover - needs network for Yahoo data
from datetime import datetime
from lumibot.backtesting import YahooDataBacktesting
CustomIndicatorStrategy.run_backtest(YahooDataBacktesting, datetime(2024, 1, 1), datetime(2024, 12, 31))
Run the backtest with free Yahoo data:
python -m lumibot.example_indicators.indicator_strategy
LumiBot’s test suite runs this exact class in a backtest on recorded-style data and checks every indicator value against a direct calculation.
Tips¶
Register your indicator before the backtest starts: import its module at the top of the strategy file.
Outside data inside an indicator (an API) must respect the bar’s time in a backtest. See Using outside data in an indicator.
An indicator that calls an AI model may “remember” what happened after the bar from its training data. Backtests of those can look better than live results. Paper trade them before trusting a backtest.