Blog

How to backtest a trading strategy in Python with the LEAN engine

· 8 min read · Backtesting, Python, Tutorial

LEAN is an open-source algorithmic trading engine. You write a strategy as a Python class, and LEAN feeds it market data one moment at a time, executes its orders with a fee and fill model, and keeps the portfolio's books. The same class runs in a backtest and in live trading, which is the main reason to use an engine instead of a spreadsheet or a vectorised script.

This walkthrough builds a simple trend-following strategy on the S&P 500 ETF, SPY, and explains each part. The point isn't the strategy, which is a textbook one: it's the structure every LEAN algorithm shares.

The whole strategy

from AlgorithmImports import *


class TrendAlgorithm(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2010, 1, 1)
        self.set_end_date(2021, 1, 1)
        self.set_cash(100000)
        self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
        self.fast = self.sma(self.spy, 50, Resolution.DAILY)
        self.slow = self.sma(self.spy, 200, Resolution.DAILY)
        self.set_warm_up(200, Resolution.DAILY)

    def on_data(self, slice):
        if self.is_warming_up:
            return
        # Hold SPY while its 50-day average is above its 200-day average.
        if self.fast.current.value > self.slow.current.value:
            self.set_holdings(self.spy, 1)
        else:
            self.liquidate(self.spy)

1. initialize: the setup

initialize runs once, before any data. It sets the backtest's dates and starting cash, and subscribes to data. add_equity("SPY", Resolution.DAILY) asks for daily bars and returns a security; keep its .symbol, the identifier you'll use everywhere else.

Resolution matters. Daily bars are fast to test and fine for strategies that trade at most once a day. Minute or tick data lets you model intraday entries, at the cost of far more data and slower runs.

2. Indicators that update themselves

self.sma(self.spy, 50, Resolution.DAILY) creates a 50-day simple moving average that LEAN updates with every new daily bar. You never compute it from a price list yourself, which removes a common bug: an average that accidentally includes the bar you're about to trade on. LEAN has well over a hundred built-in indicators, including EMA, RSI, Bollinger Bands, ATR and MACD, created the same way.

3. Warm-up

A 200-day average needs 200 days of data before it means anything. set_warm_up(200, Resolution.DAILY) replays 200 days before the start date to fill the indicators, and is_warming_up is true during that time, so the strategy doesn't trade on half-built signals. Skipping warm-up is a quiet source of odd early trades.

4. on_data: the decision

on_data is called with a slice, everything that arrived at that moment. Here the rule is simple: when the fast average is above the slow one, hold SPY; otherwise, hold cash.

  • set_holdings(symbol, 1) targets 100% of the portfolio in the asset, and LEAN works out the order size.
  • liquidate(symbol) closes the position.

Calling set_holdings every day is safe: LEAN only orders the difference from your current position, and skips orders too small to matter, so positions don't stack up. Orders made in on_data on daily bars fill at the next available price, not the close you just saw, which keeps the test free of the look-ahead bias that spreadsheet backtests often have.

5. Reading the report

A backtest report gives you a lot of numbers. Start with these:

  • Compounding annual return, next to the benchmark's over the same dates. Beating cash is not the bar; beating simply holding the asset often is.
  • Maximum drawdown, the worst fall from a peak. Ask whether you'd really have kept running the strategy through it.
  • Sharpe ratio, return per unit of volatility. Useful to compare strategies, but it hides tail risk.
  • Number of trades and fees. A strategy with few trades has little evidence behind it; one with thousands needs realistic costs.

Then look at the equity curve itself. Is the return spread across the period, or does it all come from one stretch? A strategy whose whole result comes from one year is a bet on that year happening again.

Where to go from here

On MarketsCoder this code runs as it is, in the browser: every dataset comes with an example like it, and the AI assistant can write, explain or change one from a plain-English description.

Test your own idea on point-in-time data.

Write a strategy in Python or describe it to the AI assistant, and see a full backtest report in minutes.

Get started