AlgoPro UniversityCourse home
Part 5 · Lesson

Using a backtesting library

Rolling your own teaches you how backtesting works; a mature library lets you move fast without reinventing fills, analyzers and plotting. A worked backtrader example, and when to use which.

You have now built both engines by hand, which means you understand exactly what a backtesting library does under the hood — and that is the whole reason we did it. For real research, though, you rarely want to maintain your own engine. A mature library gives you tested fill logic, dozens of built-in performance analyzers, multi-asset portfolios, and plotting, all for free.

We will use backtrader, the most popular pure-Python event-driven framework. It uses the same mental model as our engine — a Strategy class with a next() method called once per bar — so everything you learned transfers directly.

bashInstall
pip install backtrader yfinance

Defining a Strategy

A backtrader strategy subclasses bt.Strategy. You set up your indicators once in __init__, and put your per-bar decision logic in next() — called automatically for every bar, with look-ahead handled for you. Compare this to our hand-rolled on_bar: same idea, more batteries included.

pythonAn MA-cross strategy in backtrader
import backtrader as bt

class MACross(bt.Strategy):
    params = dict(fast=class="n">20, slow=class="n">100)

    def __init__(self):
        fast = bt.ind.SMA(period=self.p.fast)
        slow = bt.ind.SMA(period=self.p.slow)
        class="c"># CrossOver: +class="n">1 when fast crosses above slow, -class="n">1 when below
        self.crossover = bt.ind.CrossOver(fast, slow)

    def next(self):
        if not self.position:              class="c"># flat -> look to enter
            if self.crossover > class="n">0:
                self.buy()
        elif self.crossover < class="n">0:           class="c"># long -> exit on down-cross
            self.close()

Running it with realistic costs and analyzers

The Cerebro object is backtrader's engine. You feed it data, cash, a commission scheme (our costs from Lesson 3), and analyzers — pluggable objects that compute Sharpe, drawdown, trade stats and more while the backtest runs, so you never hand-code those metrics again.

pythonWiring up Cerebro and reading the stats
import backtrader as bt
import yfinance as yf

df = yf.download(class="s">"SPY", start=class="s">"class="n">2015-class="n">01-class="n">01", end=class="s">"class="n">2024-class="n">12-class="n">31", auto_adjust=True)

cerebro = bt.Cerebro()
cerebro.addstrategy(MACross, fast=class="n">20, slow=class="n">100)
cerebro.adddata(bt.feeds.PandasData(dataname=df))

cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=class="n">0.0006)   class="c"># class="n">6 bps per side, from Lesson class="n">3
cerebro.addsizer(bt.sizers.PercentSizer, percents=class="n">95)  class="c"># deploy class="n">95% per trade

class="c"># Analyzers compute the metrics we built by hand in Lesson class="n">2 — for free
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name=class="s">"sharpe",
                    timeframe=bt.TimeFrame.Days, annualize=True)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name=class="s">"dd")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name=class="s">"trades")
cerebro.addanalyzer(bt.analyzers.Returns, _name=class="s">"returns")

start_value = cerebro.broker.getvalue()
results = cerebro.run()
strat = results[class="n">0]
end_value = cerebro.broker.getvalue()

When the run finishes, the analyzers hold exactly the metrics we implemented by hand in Lesson 2 — now computed and validated for you. You just read them off.

pythonReading the analyzer output
sharpe = strat.analyzers.sharpe.get_analysis()
dd     = strat.analyzers.dd.get_analysis()
trades = strat.analyzers.trades.get_analysis()
rets   = strat.analyzers.returns.get_analysis()

print(fclass="s">"Final value:   {end_value:,.0f}  ({end_value/start_value - class="n">1:.class="n">1%})")
print(fclass="s">"CAGR:          {rets['rnorm100']:.2f}%")
print(fclass="s">"Sharpe:        {sharpe['sharperatio']:.2f}")
print(fclass="s">"Max drawdown:  {dd['max']['drawdown']:.1f}%")

won  = trades.get(class="s">'won',  {}).get(class="s">'total', class="n">0)
lost = trades.get(class="s">'lost', {}).get(class="s">'total', class="n">0)
total = won + lost
if total:
    print(fclass="s">"Win rate:      {won/total:.class="n">1%}  ({total} trades)")

class="c"># One line to see the equity curve, trades and indicators plotted
class="c"># cerebro.plot(style=class="s">'candlestick')

The alternatives

When to use a library vs build your own

What you now know

You can build a vectorised backtest in a dozen lines and know exactly why .shift(1) matters. You can measure a strategy the way professionals do — CAGR, Sharpe, Sortino, max drawdown, profit factor, exposure. You can charge realistic costs and watch them separate paper edges from real ones. You can write an event-driven engine for path-dependent logic, and you can drive a full library when you want to move fast. That is the complete toolkit for answering the only question that matters before you risk a cent: *is this edge real?*

And yet even a perfect backtest can lie to you in one last way — by fitting the past too well to predict the future. That is Part 6: robustness, overfitting, and why your beautiful backtest might be worthless.