Lesson 2.4.4 · 50 min

backtesting.py and backtrader: two teaching engines and their status

backtesting.py and backtrader are the two Python backtest engines that the most people have learned with. This lesson shows their real code, checks on PyPI and GitHub how they stand (versions, licenses, maintenance) and reproduces with an adapter of its own the rule that changes the most results without anyone noticing: when an order sent from next() gets executed.

Fig. — What each engine offers according to its documentation and metadata (October 2026): the event-driven engine of lesson 2.4.2, backtesting.py, backtrader and vectorbt
04/08
Module
2.4 · Backtesting: engines and paradigms
Duration
50 min
Updated
Level 2 lessons
  1. 2.1 · Statistics and econometrics for trading

    1. 2.1.1 Hypothesis tests, p-values, bootstrap and regression with robust standard errors
    2. 2.1.2 Stationarity: ADF, KPSS, differencing and fractional differencing
    3. 2.1.3 ARMA and ARIMA models: autocorrelation (ACF) and partial autocorrelation (PACF)
    4. 2.1.4 Volatility models: ARCH, GARCH, EGARCH and GJR with the arch library
    5. 2.1.5 Cointegration: Engle-Granger, Johansen and the half-life of mean reversion
    6. 2.1.6 Kalman filter for dynamic hedge ratios
    7. 2.1.7 Regime detection: hidden Markov models and structural breaks
    8. 2.1.8 The multiple testing problem: Bonferroni, FDR and the “factor zoo”
  2. 2.2 · Anatomy of a trading system

    1. 2.2.1 Edge hypotheses: risk, behavior, structure and information
    2. 2.2.2 Universe, signal, filter, entry, exit, sizing and risk management
    3. 2.2.3 Frequencies: intraday, swing and position trading, and the capacity of a strategy
    4. 2.2.4 Research journal and experiment tracking against data snooping
  3. 2.3 · Strategy families

    1. 2.3.1 Trend following and momentum
    2. 2.3.2 Mean reversion: RSI and Bollinger, intraday reversal and gaps
    3. 2.3.3 Pairs trading and statistical arbitrage: distance, cointegration and Kalman
    4. 2.3.4 Carry in currencies, futures and bonds; seasonality and calendar effects
    5. 2.3.5 Factors (value, quality, low volatility, size) and event strategies
    6. 2.3.6 Introduction to market making: inventory, spread and adverse selection (Avellaneda-Stoikov)
    7. 2.3.7 Crypto-specific strategies: momentum, funding arbitrage and basis
  4. 2.4 · Backtesting: engines and paradigms

    1. 2.4.1 Vectorized backtest from scratch: matrices, lag, costs and limits
    2. 2.4.2 Event-driven backtest from scratch: loop, portfolio, orders and fills
    3. 2.4.3 vectorbt: massive parameter sweeps and portfolio simulation
    4. 2.4.4 backtesting.py and backtrader: two teaching engines and their status
    5. 2.4.5 NautilusTrader: architecture, data, strategies and the same code in backtest and live
    6. 2.4.6 LEAN/QuantConnect and Zipline Reloaded; bt for asset allocation
    7. 2.4.7 Cost modeling: commissions, bid-ask spread, impact, financing and securities borrowing
    8. 2.4.8 Fill modeling: limit orders, queue, partial fills, gaps and stops
  5. 2.5 · Biases and validation

    1. 2.5.1 Backtest biases: look-ahead, survivorship, selection and narrative
    2. 2.5.2 In-sample and out-of-sample: anchored and rolling walk-forward
    3. 2.5.3 Combinatorial purged cross-validation for strategies
    4. 2.5.4 Deflated Sharpe Ratio and Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: trade resampling, block bootstrap and signal permutations
    6. 2.5.6 Parameter sensitivity: plateaus versus peaks, robustness across assets and periods
    7. 2.5.7 Synthetic data for stress testing: GBM, GARCH and bootstrapping
    8. 2.5.8 “Honest backtest” checklist before moving to paper trading
  6. 2.6 · Performance metrics

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown and Ulcer Index
    2. 2.6.2 Win rate, payoff, expectancy, alpha, beta, information ratio, turnover and capacity
    3. 2.6.3 Reports with QuantStats or pyfolio-reloaded and custom tearsheets

Two engines that many people learned with

Before NautilusTrader or vectorbt, anyone who wanted to run a backtest in Python with something more than pandas almost always ended up with one of these two libraries. backtrader is a complete event-driven engine, with multiple assets, many order types, futures commissions and broker connections. backtesting.py is smaller and more direct: one strategy, one instrument, one DataFrame, one report and one interactive chart. Both are often called “teaching” engines because they read well and show the structure of an engine; lesson 2.4.2 built that same structure from scratch, so here they can be read with a critical eye.

In this course they are not executed (the course owner's decision): their code appears in non-executed blocks, as the libraries themselves document it, and the executable part reproduces with our own code the design decision that most changes their results. Their status, read on PyPI and GitHub on October 21, 20261Note 1 · PyPI metadata of backtesting, backtrader and IbPy2 (versions, dates, license and classifiers) and the kernc/backtesting.py and mementum/backtrader repositories on GitHub (license, last commit, archive status), consulted on October 21, 2026.:

backtesting.pybacktrader
PyPI packagebacktestingbacktrader
Latest version0.6.6, July 22, 20261.9.78.123, April 19, 2023
First version on PyPIJanuary 17, 2019May 30, 2019 (1.9.74.123)
Versions published on PyPI275
Declared Python≥ 3.9Classifiers up to 3.7; the README says ≥ 3.2
LicenseAGPL-3.0GPL-3.0 (GPLv3+ on PyPI)
Last commit on the main branchAugust 5, 2026April 19, 2023
Archived repositoryNoNo
Fig. — Releases published on PyPI by backtrader, backtesting.py and vectorbt, from 2019 to October 2026

The figure shows the release history. backtesting.py had a pause between December 2021 and January 2025 and went back to releasing regularly; backtrader published five versions on PyPI and none since April 2023. A library with no new versions may keep working with a modern Python, but it is not guaranteed: it receives no fixes or adaptations to changes in pandas, NumPy or Matplotlib, and its live connection dependencies (IbPy for Interactive Brokers, according to its README) do not move either: the IbPy2 package on PyPI published its last version, 0.8.0, in October 2016.

Licenses matter if you are going to distribute something. Both are copyleft. backtrader's GPL-3.0 requires that, if a program that includes it or derives from it is distributed, it is distributed with its source code and under the same license. backtesting.py's AGPL-3.0 adds network use: whoever offers remote users a modified version as a service has to give them access to that code. For a trader who imports the library in their own scripts and trades their own money there is nothing to publish. For anyone who wants to build a platform or a product, it is wise to read the license text with a lawyer before writing the first line; it is the same warning as vectorbt's Commons Clause (lesson 2.4.3), with other conditions.

backtesting.py: a strategy in a class

The strategy inherits from Strategy and defines two methods. In init() the indicators are declared with self.I(), which computes them once over the whole series and then reveals them bar by bar: inside next(), self.ma1 only contains the values up to the current bar. In next() the decision is made. The example from its README, with the sample data that ships with the library:

IN [01]python
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG


class SmaCross(Strategy):
    def init(self):
        price = self.data.Close
        self.ma1 = self.I(SMA, price, 10)
        self.ma2 = self.I(SMA, price, 20)

    def next(self):
        if crossover(self.ma1, self.ma2):
            self.buy()
        elif crossover(self.ma2, self.ma1):
            self.sell()


bt = Backtest(GOOG, SmaCross, commission=.002,
              exclusive_orders=True)
stats = bt.run()
bt.plot()

Backtest takes a DataFrame with Open, High, Low, Close columns (and optional Volume) and the strategy class. Its parameters, according to the API documentation: cash (10,000 by default), spread, commission (as a fraction of the order value, applied on entry and on exit since version 0.4.0; it also accepts a fixed minimum or a function), margin (1 = no leverage), trade_on_close, hedging, exclusive_orders (each new order closes the previous position) and finalize_trades. The important part is in one sentence of the buy() documentation: unless trade_on_close=True, market orders execute at the open of the next bar; limit and stop orders, when their condition is met.

Optimization comes included:

IN [02]python
stats, heatmap = bt.optimize(
    n1=range(5, 60, 5), n2=range(20, 300, 10),
    constraint=lambda p: p.n1 < p.n2,
    maximize="Sharpe Ratio", method="grid",
    return_heatmap=True)

maximize is a key of the results series from run() (by default, Van Tharp's SQN) or a function; method can be "grid" (all combinations, or a sample if limited with max_tries) or "sambo" (model-based optimization). For n1 and n2 to be parameters, the strategy declares them as class attributes and uses them in init(). The warning from lesson 2.4.3 applies just the same: optimize returns the maximum of a sweep, and the maximum of a sweep is largely noise.

Its limits, from its own documentation: one Backtest is one instrument. MultiBacktest runs the same strategy on several separately, for comparison, not as a portfolio with shared cash. For fractional units (crypto) there is FractionalBacktest. backtesting.lib brings useful pieces, such as crossover, resample_apply for multiple timeframes and TrailingStrategy, a trailing stop expressed in multiples of the ATR (and, with set_trailing_pct, a percentage that the documentation warns is inexact because it is converted to ATR).

backtrader: Cerebro, lines and next()

backtrader is an event-driven engine with more pieces. Cerebro is the coordinator: it receives the data, the strategies, the broker and the add-ons, and runs the loop.

CerebroData feedsStrategyBrokerAnalyzersObserversSizer
Mermaid source
flowchart TB
  C[Cerebro] --> D[Data feeds]
  C --> S[Strategy]
  C --> B[Broker]
  C --> A[Analyzers]
  C --> O[Observers]
  D --> S
  Z[Sizer] --> S
  S --> B
  B --> S
  • Data feeds: CSV, pandas (bt.feeds.PandasData), online or live vendors; several at once, with resampling and replaying of bars.
  • Strategy: __init__ declares indicators (bt.ind.SMA, bt.ind.CrossOver…) and next() decides on each bar; notify_order and notify_trade receive the state changes of orders and trades.
  • Broker: the simulated one (BackBroker) with commissions (setcommission), slippage (set_slippage_perc) and the set_coc (execute at the close) and set_coo (at the open) modes; or a real one.
  • Sizer: decides the size when the strategy does not give it. Analyzers (SharpeRatio, DrawDown, TradeAnalyzer…) and observers compute metrics and series for the chart.

Series are lines: self.data.close[0] is the close of the current bar and self.data.close[-1] that of the previous one, without the strategy knowing which bar it is on. The same strategy as in backtesting.py, in the style of backtrader's documentation:

IN [03]python
import backtrader as bt
import pandas as pd


class SmaCross(bt.Strategy):
    params = (("rapida", 10), ("lenta", 30))

    def __init__(self):
        sma1 = bt.ind.SMA(period=self.p.rapida)
        sma2 = bt.ind.SMA(period=self.p.lenta)
        self.cruce = bt.ind.CrossOver(sma1, sma2)

    def next(self):
        if not self.position and self.cruce[0] > 0:
            self.buy()
        elif self.position and self.cruce[0] < 0:
            self.close()

    def notify_order(self, order):
        if order.status == order.Completed:
            print(self.data.datetime.date(0), order.executed.price)


df = pd.read_csv("kapa_diario.csv", index_col=0, parse_dates=True)
cerebro = bt.Cerebro()
cerebro.adddata(bt.feeds.PandasData(dataname=df))
cerebro.addstrategy(SmaCross, rapida=10, lenta=30)
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001)     # 0.1%
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe")
resultado = cerebro.run()
print(cerebro.broker.getvalue())

For a future, the commission is given per contract together with the margin and the multiplier; the example in its documentation is the Euro Stoxx 50 at Interactive Brokers:

IN [04]python
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)

# and a 4% trailing stop as an order of the simulated broker
self.sell(exectype=bt.Order.StopTrail, trailpercent=0.04)

With margin, backtrader treats the instrument as a future: the commission is fixed per contract and cash is adjusted each day with the profit and loss, the accounting that the portfolio of lesson 2.4.2 simplified. StopTrail recomputes the stop with the last close and, when it is touched, executes it as a market order, according to its documentation.

An adapter with the semantics of next()

Neither library is executed here, but their central rule can be reproduced and measured: a strategy written as in backtrader, with lines indexed from the current bar and orders that execute at the open of the next bar, on a small engine of our own with the event order of lesson 2.4.2. The data is the daily bars of KAPA, the synthetic ETF from 2022 to 2025 of the previous lessons, built from its 5-minute bars.

next on bar tMarket orderOpen of t+1Close of t: coc
Mermaid source
flowchart LR
  N[next on bar t] --> O[Market order]
  O --> A[Open of t+1]
  O --> C[Close of t: coc]
IN [05]python
import numpy as np
import pandas as pd

b5 = pd.read_parquet("indice2_5min.parquet")
b5["sesion"] = (b5["ts"].dt.tz_convert("America/New_York")
                .dt.tz_localize(None).dt.normalize())
kapa = b5.groupby("sesion").agg(open=("apertura", "first"),
                                high=("maximo", "max"),
                                low=("minimo", "min"),
                                close=("cierre", "last"))
hueco = kapa["open"] / kapa["close"].shift(1) - 1
print(len(kapa), "sessions; mean absolute overnight gap:",
      f"{100 * hueco.abs().mean():.2f} %")
OUT [05]KAPA daily bars
1003 sessions; mean absolute overnight gap: 0.38 %

First, the lines. A Linea stores the whole series but only lets you read up to the current bar: [0] is the current one, [-1] the previous one and [1], the future, is an error. An indicator is another line, computed once but revealed at the same pace, which is exactly what self.I() does in backtesting.py.

IN [06]python
class Reloj:
    def __init__(self):
        self.i = -1                       # index of the current bar

class Linea:
    """Series revealed bar by bar: [0] current, [-1] previous."""

    def __init__(self, valores, reloj):
        self.v, self.reloj = np.asarray(valores, dtype=float), reloj

    def __getitem__(self, k):
        if k > 0:
            raise IndexError(f"[{k}] is the future")
        j = self.reloj.i + k
        return self.v[j] if j >= 0 else np.nan

def sma(linea, n):
    return Linea(pd.Series(linea.v).rolling(n).mean(), linea.reloj)

def cruce(a, b):
    """+1 if a crosses above b on the bar, -1 if below, 0 if not."""
    d = np.sign(a.v - b.v)
    previo = np.concatenate([[np.nan], d[:-1]])
    return Linea(np.where((d > 0) & (previo <= 0), 1,
                 np.where((d < 0) & (previo >= 0), -1, 0)), a.reloj)

reloj = Reloj()
prueba = Linea([10.0, 11.0, 12.0], reloj)
reloj.i = 1
print(prueba[0], prueba[-1], prueba[-2])
try:
    prueba[1]
except IndexError as e:
    print("IndexError:", e)
OUT [06]Lines and indicators
11.0 10.0 nan
IndexError: [1] is the future

Next, the adapter: a minimal engine that calls next() on each bar and offers the strategy what it expects from backtrader (self.data, self.position, buy(), close(), notify_order()). The order of things on each bar is that of lesson 2.4.2: first the pending orders execute at the bar's open, then the strategy decides. With coc=True (like set_coc(True) in backtrader or trade_on_close=True in backtesting.py) the order executes at the close of the bar in which it is sent. For the comparison to be exact, each buy invests all the capital at the moment it executes, with no commissions.

IN [07]python
class Datos:
    def __init__(self, df, reloj):
        for c in ("open", "high", "low", "close"):
            setattr(self, c, Linea(df[c], reloj))
        self.fechas = df.index

class Orden:
    def __init__(self, objetivo, fecha):
        self.objetivo, self.creada = objetivo, fecha
        self.status = "Submitted"
        self.precio, self.ejecutada = None, None

class EstrategiaNext:
    """The minimum of the backtrader API that a strategy uses."""

    def __init__(self, motor, data, **params):
        self.motor, self.data = motor, data
        self.p = type("Params", (), params)

    @property
    def position(self):
        return self.motor.unidades

    def buy(self):
        return self.motor.enviar(1.0)     # all the capital

    def close(self):
        return self.motor.enviar(0.0)

    def notify_order(self, orden):
        pass

class MiniCerebro:
    def __init__(self, df, coc=False, cash=1.0):
        self.df, self.coc = df, coc
        self.reloj = Reloj()
        self.efectivo, self.unidades = cash, 0.0
        self.pendientes, self.ordenes = [], []

    def enviar(self, objetivo):
        o = Orden(objetivo, self.df.index[self.reloj.i])
        if self.coc:
            self._ejecutar(o, self.df["close"].iloc[self.reloj.i])
        else:
            self.pendientes.append(o)
        return o

    def _ejecutar(self, o, precio):
        valor = self.efectivo + self.unidades * precio
        self.unidades = o.objetivo * valor / precio
        self.efectivo = valor - self.unidades * precio
        o.status, o.precio = "Completed", precio
        o.ejecutada = self.df.index[self.reloj.i]
        self.ordenes.append(o)
        self.estrategia.notify_order(o)

    def run(self, clase, **params):
        datos = Datos(self.df, self.reloj)
        self.estrategia = clase(self, datos, **params)
        capital = []
        for i in range(len(self.df)):
            self.reloj.i = i
            for o in self.pendientes:     # orders from the previous bar
                self._ejecutar(o, self.df["open"].iloc[i])
            self.pendientes = []
            self.estrategia.next()
            capital.append(self.efectivo + self.unidades
                           * self.df["close"].iloc[i])
        return pd.Series(capital, self.df.index)

The strategy is written as in backtrader: indicators in __init__, the decision in next() with indices relative to the current bar.

IN [08]python
class SmaCross(EstrategiaNext):
    def __init__(self, motor, data, **params):
        super().__init__(motor, data, **params)
        rapida = sma(self.data.close, self.p.rapida)
        lenta = sma(self.data.close, self.p.lenta)
        self.cruce = cruce(rapida, lenta)

    def next(self):
        if not self.position and self.cruce[0] > 0:
            self.buy()
        elif self.position and self.cruce[0] < 0:
            self.close()

resultados = {}
for coc in (True, False):
    motor = MiniCerebro(kapa, coc=coc)
    eq = motor.run(SmaCross, rapida=10, lenta=30)
    nombre = "at close (coc)" if coc else "next open"
    resultados[nombre] = (eq, motor.ordenes)
    print(f"{nombre:19s} capital {eq.iloc[-1]:.4f}, "
          f"orders {len(motor.ordenes)}")
o = resultados["next open"][1][0]
print("first order:", o.creada.date(), "->", o.ejecutada.date(),
      "at", o.precio)
OUT [08]SmaCross, backtrader style
at close (coc)      capital 1.7151, orders 33
next open           capital 1.6462, orders 33
first order: 2022-03-10 -> 2022-03-11 at 250.74

The same signals and the same number of orders (33); a different final capital: 1.7151 executing at the signal's close and 1.6462 at the next open. The first order is created in next() on March 10, 2022 and executes at the open of the 11th, at 250.74. That the difference is not a bug in the adapter is shown just as in lesson 2.4.2: by reconciling with a vectorized calculation that applies each convention. At the close it is the usual shift(1); at the next open, the new position starts at the open and the previous night is spent with the old position.

IN [09]python
c, o_ = kapa["close"], kapa["open"]
x = np.sign(c.rolling(10).mean() - c.rolling(30).mean())
cruce_s = pd.Series(np.where((x > 0) & (x.shift(1) <= 0), 1,
                    np.where((x < 0) & (x.shift(1) >= 0), -1, 0)),
                    kapa.index)
# state after next() on each bar (long or flat), with memory
estado = cruce_s.replace(0, np.nan).clip(lower=0).ffill().fillna(0)

r_cierre = estado.shift(1, fill_value=0) * c.pct_change().fillna(0)
s1 = estado.shift(1, fill_value=0)       # position from the open
s2 = estado.shift(2, fill_value=0)       # position overnight
r_apertura = ((1 + s2 * (o_ / c.shift(1) - 1).fillna(0))
              * (1 + s1 * (c / o_ - 1)) - 1)
for nombre, rv in [("at close (coc)", r_cierre),
                   ("next open", r_apertura)]:
    eq_v = (1 + rv).cumprod()
    dif = (resultados[nombre][0] / eq_v - 1).abs().max()
    print(f"{nombre:19s} vectorized {eq_v.iloc[-1]:.4f}  "
          f"maximum difference {dif:.1e}")
OUT [09]Reconciliation with the vectorized calculation
at close (coc)      vectorized 1.7151  maximum difference 2.3e-15
next open           vectorized 1.6462  maximum difference 2.8e-15
Fig. — The same moving-average crossover on KAPA, 2022–2025, in the adapter with next() semantics: orders filled at the signal close or at the next open, and the equity gap

Both reconcile down to floating-point rounding. The difference between them comes entirely from the overnight gap: each buy at the open pays (or earns) what the price moved since the signal's close, and so does each sell. In KAPA, a synthetic ETF with a mean overnight gap of 0.38%, that is almost 7 points of capital in four years; in an asset with large gaps (stocks with earnings, crypto with weekends, futures with split sessions) it can be the whole result. And note the trap: a vectorized backtest with shift(1) on closes is equivalent to the coc mode, the one backtrader calls cheating. Executing at the signal's close is only honest if the signal can be computed and the order sent before the close (lesson 1.1.4).

The correspondence between the three architectures, the engine of lesson 2.4.2 and the two libraries:

ConceptOur own engine (2.4.2)backtraderbacktesting.py
Decide on each baral_recibir_barra(ev)next()next()
Bar dataev.barras[i].cierreself.data.close[0]self.data.Close[-1]
IndicatorsThe strategy's own statebt.ind.* in __init__self.I() in init()
Request a positionSignalEvent(peso)order_target_percentbuy(size=fraction)
Execution noticeal_recibir_fill(f)notify_order(order)(trades in self.trades)
Default executionWhatever the order sets (cierre, mercado)Next openNext open
Multiple assetsYesYesNo (one per Backtest)

Watch out for an index difference: in backtesting.py, self.data.Close is an array with the data up to the current bar, so the last one is [-1]; in backtrader, the current one is [0]. Translating a strategy from one library to the other without taking this into account shifts everything by one bar.

Which one to choose

NeedRecommendation
Learn the structure of an engineWrite one (lesson 2.4.2) and read backtrader
Quickly test an idea on a single asset with a chartbacktesting.py
Portfolios, futures with multiplier and margin, multiple timeframesbacktrader, knowing it is not maintained, or NautilusTrader (2.4.5)
Massive parameter sweepsvectorbt or your own NumPy (lesson 2.4.3)
The same code in backtest and live, maintainedNautilusTrader or LEAN (lessons 2.4.5 and 2.4.6)
A product to be distributed or soldReview the licenses first: AGPL, GPL and Commons Clause have conditions

The header figure summarizes each one's capabilities according to its documentation. What no table tells you is how each engine behaves in the details (the order of events within a bar, what price a stop with a gap takes, when the size of an order is computed). Before trusting a result, reconcile it with something independent, as was done in this lesson and in 2.4.2.

Exercises

  1. Add a proportional commission to the adapter (for example, 0.1% of the traded value, like backtrader's setcommission(commission=0.001)) and measure how much the final capital changes in the two execution modes.
Solution
IN [10]python
class MiniCerebroComision(MiniCerebro):
    def __init__(self, df, coc=False, comision=0.001):
        super().__init__(df, coc)
        self.comision = comision

    def _ejecutar(self, o, precio):
        antes = self.unidades
        super()._ejecutar(o, precio)
        self.efectivo -= abs(self.unidades - antes) * precio \
            * self.comision

for coc in (True, False):
    m = MiniCerebroComision(kapa, coc=coc)
    eq = m.run(SmaCross, rapida=10, lenta=30)
    print(f"coc={coc!s:5s} capital {eq.iloc[-1]:.4f}")
OUT [10]Commission in the adapter
coc=True  capital 1.6601
coc=False capital 1.5934

With 0.1% per side and 33 orders in four years, the commission takes away about 5 points of capital in both modes (from 1.7151 to 1.6601 at the close and from 1.6462 to 1.5934 at the open). No commission fixes the difference between executing at the close or at the open: it is a difference in price, not in cost.

  1. Write, in the adapter's style, a strategy that uses self.data.close[-1]: buy if today's close exceeds the highest close of the previous 20 sessions and exit if it falls below the lowest of the previous 10 (a Donchian channel, lesson 2.3.1). Why can it not be written with [1]?
Solution
IN [11]python
class Donchian(EstrategiaNext):
    def next(self):
        cl = self.data.close
        if np.isnan(cl[-20]):
            return                         # not 20 previous ones yet
        previos = [cl[-k] for k in range(1, 21)]
        if not self.position and cl[0] > max(previos):
            self.buy()
        elif self.position and cl[0] < min(previos[:10]):
            self.close()

m = MiniCerebro(kapa)
eq = m.run(Donchian)
print(f"capital {eq.iloc[-1]:.4f}, orders {len(m.ordenes)}")
OUT [11]Donchian with lines
capital 1.4990, orders 47

The channel ends with a capital of 1.4990 and 47 orders. [1] would be tomorrow's close: the Linea forbids it with an IndexError, so look-ahead bias cannot be written by accident. That is how event-driven engines protect you: the strategy only sees what has already arrived. In a vectorized backtest, the same mistake is a shift(-1) that gives no warning at all.

  1. A colleague has run one backtest with backtesting.py and another with their own vectorized version of the same daily strategy, and they do not match. List three causes you would check before looking for a bug, using what you have seen in this lesson.
Solution

First, the execution price: backtesting.py executes market orders at the open of the next bar unless it is passed trade_on_close=True, and the vectorized backtest with shift(1) on closes executes at the signal's close. Second, the size: buy() without size uses almost all the available cash (size=.9999) and in whole units unless you use FractionalBacktest, whereas a vectorized backtest usually uses fractional weights. Third, the costs: backtesting.py's commission is applied on entry and on exit, and spread is separate; the vectorized one may be charging only one side or on a different base. The fourth, if there are stops, is the order within the bar. They are all isolated as in this lesson's reconciliation: changing one assumption at a time.

Footnotes

  1. PyPI metadata of backtesting, backtrader and IbPy2 (versions, dates, license and classifiers) and the kernc/backtesting.py and mementum/backtrader repositories on GitHub (license, last commit, archive status), consulted on October 21, 2026. ↩

Each answer is folded under its question

Self-assessment

  1. In backtrader and in backtesting.py, by default, at what price does a market order sent in next() of bar t execute?

    • AAt the open of bar t+1
    • BAt the close of bar t
    • CAt the close of bar t+1
    • DAt the average price of bar t
    Show answer

    Answer A. At the open of bar t+1

    backtrader's documentation says the market order executes one bar after the signal with the opening price; backtesting.py's says that, unless trade_on_close=True, market orders execute at the open of the next bar. Executing at the close of t requires set_coc(True) in backtrader, which its own documentation calls cheating.

  2. What does it mean in practice that backtesting.py has an AGPL-3.0 license?

    • AThat it cannot be used to trade real money
    • BThat it is public domain
    • CThat it requires publishing your strategies
    • DThat if it is modified and offered to others as a network service, the source code of that version must be made available to its users; for your own use it does not require publishing anything
    Show answer

    Answer D. That if it is modified and offered to others as a network service, the source code of that version must be made available to its users; for your own use it does not require publishing anything

    The AGPL extends the GPL to network use: whoever offers remote users a modified version must give them access to its code. Your own strategy that imports the library and runs on your machine is neither distributed nor offered to anyone. Building a web backtesting platform on top of it is something else.

  3. What is the latest version of backtrader published on PyPI and what does it say about its maintenance?

    • AOne from 2026, with Python 3.14 support
    • BIt is not on PyPI
    • C1.9.78.123, from April 19, 2023; there have been no new versions since then, and the package's Python classifiers go up to 3.7
    • DIt was archived in 2020
    Show answer

    Answer C. 1.9.78.123, from April 19, 2023; there have been no new versions since then, and the package's Python classifiers go up to 3.7

    The PyPI metadata gives five versions between 2019 and 2023, the last on April 19, 2023, and the repository is not archived. Working on a modern Python is not the same as being maintained: a library with no new versions receives no fixes or adaptations to changes in pandas or NumPy.

  4. In the lesson's adapter, the same moving-average crossover strategy on KAPA ends with a different capital when executing at the signal's close or at the next open. Why?

    • ABecause of the commissions
    • BBecause between the signal's close and the next open there is an overnight gap: each entry and each exit happens at another price, and that difference accumulates over all the trades
    • CBecause the adapter has a bug
    • DBecause the number of trades changes
    Show answer

    Answer B. Because between the signal's close and the next open there is an overnight gap: each entry and each exit happens at another price, and that difference accumulates over all the trades

    The signals and the number of trades are the same; only the execution price changes. Both results reconcile exactly with a vectorized calculation that applies each convention, so the difference is the gap's, not a bug.

  5. What does backtrader have that backtesting.py does not?

    • ASeveral assets in the same portfolio with shared cash, commissions with multiplier and futures margin, and live broker connections
    • BParameter optimization
    • CStop orders
    • DCharts
    Show answer

    Answer A. Several assets in the same portfolio with shared cash, commissions with multiplier and futures margin, and live broker connections

    backtesting.py simulates a single instrument per Backtest (MultiBacktest runs the same strategy on several separately). backtrader supports several data feeds in one strategy, setcommission with margin and mult for futures and, according to its README, live data and orders with Interactive Brokers, Oanda and Visual Chart.

Key points

  • backtesting.py: a strategy is a class with init() (indicators with self.I) and next(); Backtest runs it on an OHLC DataFrame of a single instrument and optimize sweeps parameters.
  • backtrader: Cerebro coordinates data feeds, strategies, broker, sizers, analyzers and observers; series are lines in which [0] is the current bar and [-1] the previous one.
  • By default, both execute market orders at the open of the next bar; a vectorized calculation at the close gives another result, and neither warns you.
  • Licenses with sources: backtesting.py, AGPL-3.0; backtrader, GPL-3.0 (copyleft, unlike vectorbt's Apache 2.0 with Commons Clause).
  • Status in October 2026: backtesting.py released 0.6.6 in July 2026; backtrader has not released since April 2023 (1.9.78.123).
  • They are good for learning and for single-asset strategies; for portfolios, real futures or going live, a validated engine of your own or NautilusTrader (lesson 2.4.5) is better.

Resources