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
- Swap the asset: the same structure works for
add_forexandadd_crypto. - Add a second data source, such as Treasury yields or futures positioning, and trade on it.
- Before tuning the 50 and 200, read about overfitting.
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.