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.
pip install backtrader yfinanceDefining 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.
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.
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.
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
- backtrader — mature, huge community, event-driven, great for realistic single- or multi-asset strategies. Slower, and no longer actively developed, but rock-solid.
- vectorbt — insanely fast, built on NumPy; sweeps *thousands* of parameter combinations in seconds. Ideal for research and optimisation; steeper learning curve and a more array-oriented mindset.
- zipline-reloaded — the engine behind the old Quantopian; strong for equities with a proper trading calendar and pipeline API.
- Roll your own — when your logic is genuinely unusual, or you need it to share code with your live trading system exactly. You now know how.
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.