---
title: "2.4.4 backtesting.py and backtrader: use and maintenance"
description: "Two teaching engines in Python: the backtesting.py and backtrader APIs, AGPL and GPL licenses, maintenance status and the semantics of next()."
url: "https://pythonparatrading.com/en/python-trading-course/level-2/backtesting-py-and-backtrader/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-2/backtesting-py-and-backtrader.png"
---

# 2.4.4 · 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.

- Course: [Level 2: Systematic strategies and rigorous backtesting](https://pythonparatrading.com/en/python-trading-course/level-2/)
- Module: 2.4 · Backtesting: engines and paradigms
- Duration: 50 min
- Prerequisites: [2.4.2 · Event-driven backtest from scratch: loop, portfolio, orders and fills](https://pythonparatrading.com/en/python-trading-course/level-2/event-driven-backtest-from-scratch/), [1.1.4 · Order types](https://pythonparatrading.com/en/python-trading-course/level-1/order-types-explained/)

## By the end of this lesson

- Read and write a strategy in backtesting.py (`init`, `next`, `Backtest`, `optimize`) and in backtrader (`Cerebro`, `Strategy`, lines, `notify_order`).
- Know the status of each library from primary sources: latest version, license (AGPL-3.0 and GPL-3.0) and what each one implies.
- Program an adapter with backtrader's `next()` semantics (lines with `[0]` and `[-1]`, orders at the next open or at the close) and reconcile it with a vectorized calculation.
- Compare these engines with the one built in lesson 2.4.2 and with vectorbt, and choose according to the problem.

## 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, 2026[^1]:

| | backtesting.py | backtrader |
|---|---|---|
| PyPI package | `backtesting` | `backtrader` |
| Latest version | 0.6.6, July 22, 2026 | 1.9.78.123, April 19, 2023 |
| First version on PyPI | January 17, 2019 | May 30, 2019 (1.9.74.123) |
| Versions published on PyPI | 27 | 5 |
| Declared Python | ≥ 3.9 | Classifiers up to 3.7; the README says ≥ 3.2 |
| License | AGPL-3.0 | GPL-3.0 (GPLv3+ on PyPI) |
| Last commit on the main branch | August 5, 2026 | April 19, 2023 |
| Archived repository | No | No |

*Fig. — Releases published on PyPI by backtrader, backtesting.py and vectorbt, from 2019 to October 2026.* Three rows of marks, one per published release. backtrader: five releases between May 2019 and 19 April 2023 (the last one, highlighted) and none after. backtesting.py: 27 releases, many in 2020, a gap from December 2021 to January 2025 and the latest in July 2026. vectorbt: 66 releases since December 2020, the latest on 19 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:

```python skip
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:

```python skip
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.

```mermaid
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:

```python skip
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:

```python skip
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.

```mermaid
flowchart LR
  N[next on bar t] --> O[Market order]
  O --> A[Open of t+1]
  O --> C[Close of t: coc]
```

```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} %")
```
```output
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.

```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)
```
```output
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.

```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.

```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)
```
```output
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.

```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}")
```
```output
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.* Top, two almost overlapping weekly equity curves: filling at the signal close ends at 1.72 and at the next open (highlighted) at 1.65, with the same 33 orders. Bottom, the gap in equity points, near zero in 2022 and growing in steps to 6.9 points at the end.

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:

| Concept | Our own engine (2.4.2) | backtrader | backtesting.py |
|---|---|---|---|
| Decide on each bar | `al_recibir_barra(ev)` | `next()` | `next()` |
| Bar data | `ev.barras[i].cierre` | `self.data.close[0]` | `self.data.Close[-1]` |
| Indicators | The strategy's own state | `bt.ind.*` in `__init__` | `self.I()` in `init()` |
| Request a position | `SignalEvent(peso)` | `order_target_percent` | `buy(size=fraction)` |
| Execution notice | `al_recibir_fill(f)` | `notify_order(order)` | (trades in `self.trades`) |
| Default execution | Whatever the order sets (`cierre`, `mercado`) | Next open | Next open |
| Multiple assets | Yes | Yes | No (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

| Need | Recommendation |
|---|---|
| Learn the structure of an engine | Write one (lesson 2.4.2) and read backtrader |
| Quickly test an idea on a single asset with a chart | backtesting.py |
| Portfolios, futures with multiplier and margin, multiple timeframes | backtrader, knowing it is not maintained, or NautilusTrader (2.4.5) |
| Massive parameter sweeps | vectorbt or your own NumPy (lesson 2.4.3) |
| The same code in backtest and live, maintained | NautilusTrader or LEAN (lessons 2.4.5 and 2.4.6) |
| A product to be distributed or sold | Review 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.

[^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.

## 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.**

```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}")
```
```output
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.

2. 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.**

```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)}")
```
```output
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.

3. 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.

## 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?

   - A. At the open of bar t+1
   - B. At the close of bar t
   - C. At the close of bar t+1
   - D. At the average price of bar t

   *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?

   - A. That it cannot be used to trade real money
   - B. That it is public domain
   - C. That it requires publishing your strategies
   - 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

   *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?

   - A. One from 2026, with Python 3.14 support
   - B. It is not on PyPI
   - 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
   - D. It was archived in 2020

   *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?

   - A. Because of the commissions
   - 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
   - C. Because the adapter has a bug
   - D. Because the number of trades changes

   *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?

   - A. Several assets in the same portfolio with shared cash, commissions with multiplier and futures margin, and live broker connections
   - B. Parameter optimization
   - C. Stop orders
   - D. Charts

   *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

### Research

- [Your first system in Python: moving-average crossover on SPY](https://pythonparatrading.com/en/first-system-moving-average-crossover/) · Research-0018 · 7 min

### Documentation

- [backtesting.py: API documentation (Backtest, Strategy, optimize)](https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html) · kernc.github.io
- [backtesting on PyPI (0.6.6, July 22, 2026; AGPL-3.0)](https://pypi.org/project/backtesting/) · pypi.org
- [backtrader: Cerebro](https://www.backtrader.com/docu/cerebro/) · backtrader.com
- [backtrader: orders, creation and execution](https://www.backtrader.com/docu/order-creation-execution/order-creation-execution/) · backtrader.com
- [backtrader on PyPI (1.9.78.123, April 19, 2023; GPLv3+)](https://pypi.org/project/backtrader/) · pypi.org
- [backtesting.py: text of its AGPL-3.0 license (section 13, remote network interaction)](https://github.com/kernc/backtesting.py/blob/master/LICENSE.md) · github.com

Previous lesson: [2.4.3 · vectorbt: massive parameter sweeps and portfolio simulation](https://pythonparatrading.com/en/python-trading-course/level-2/vectorbt-parameter-sweeps/)

Next lesson: [2.4.5 · NautilusTrader: architecture, data, strategies and the same code in backtest and live](https://pythonparatrading.com/en/python-trading-course/level-2/nautilustrader-architecture-and-strategies/)
