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.
- Module
- 2.4 · Backtesting: engines and paradigms
- Duration
- 50 min
- Prerequisites
- 2.4.2 · Event-driven backtest from scratch: loop, portfolio, orders and fills, 1.1.4 · Order types
- Author
- Jesús Cuesta
- Updated
Level 2 lessons
2.1 · Statistics and econometrics for trading
- 2.1.1 Hypothesis tests, p-values, bootstrap and regression with robust standard errors
- 2.1.2 Stationarity: ADF, KPSS, differencing and fractional differencing
- 2.1.3 ARMA and ARIMA models: autocorrelation (ACF) and partial autocorrelation (PACF)
- 2.1.4 Volatility models: ARCH, GARCH, EGARCH and GJR with the arch library
- 2.1.5 Cointegration: Engle-Granger, Johansen and the half-life of mean reversion
- 2.1.6 Kalman filter for dynamic hedge ratios
- 2.1.7 Regime detection: hidden Markov models and structural breaks
- 2.1.8 The multiple testing problem: Bonferroni, FDR and the “factor zoo”
2.2 · Anatomy of a trading system
2.3 · Strategy families
- 2.3.1 Trend following and momentum
- 2.3.2 Mean reversion: RSI and Bollinger, intraday reversal and gaps
- 2.3.3 Pairs trading and statistical arbitrage: distance, cointegration and Kalman
- 2.3.4 Carry in currencies, futures and bonds; seasonality and calendar effects
- 2.3.5 Factors (value, quality, low volatility, size) and event strategies
- 2.3.6 Introduction to market making: inventory, spread and adverse selection (Avellaneda-Stoikov)
- 2.3.7 Crypto-specific strategies: momentum, funding arbitrage and basis
2.4 · Backtesting: engines and paradigms
- 2.4.1 Vectorized backtest from scratch: matrices, lag, costs and limits
- 2.4.2 Event-driven backtest from scratch: loop, portfolio, orders and fills
- 2.4.3 vectorbt: massive parameter sweeps and portfolio simulation
- 2.4.4 backtesting.py and backtrader: two teaching engines and their status
- 2.4.5 NautilusTrader: architecture, data, strategies and the same code in backtest and live
- 2.4.6 LEAN/QuantConnect and Zipline Reloaded; bt for asset allocation
- 2.4.7 Cost modeling: commissions, bid-ask spread, impact, financing and securities borrowing
- 2.4.8 Fill modeling: limit orders, queue, partial fills, gaps and stops
2.5 · Biases and validation
- 2.5.1 Backtest biases: look-ahead, survivorship, selection and narrative
- 2.5.2 In-sample and out-of-sample: anchored and rolling walk-forward
- 2.5.3 Combinatorial purged cross-validation for strategies
- 2.5.4 Deflated Sharpe Ratio and Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: trade resampling, block bootstrap and signal permutations
- 2.5.6 Parameter sensitivity: plateaus versus peaks, robustness across assets and periods
- 2.5.7 Synthetic data for stress testing: GBM, GARCH and bootstrapping
- 2.5.8 “Honest backtest” checklist before moving to paper trading
2.6 · Performance metrics
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.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 |
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:
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:
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 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…) andnext()decides on each bar;notify_orderandnotify_tradereceive the state changes of orders and trades. - Broker: the simulated one (
BackBroker) with commissions (setcommission), slippage (set_slippage_perc) and theset_coc(execute at the close) andset_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:
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:
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 source
flowchart LR N[next on bar t] --> O[Market order] O --> A[Open of t+1] O --> C[Close of t: coc]
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} %")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.
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)11.0 10.0 nan
IndexError: [1] is the futureNext, 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.
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.
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)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.74The 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.
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}")at close (coc) vectorized 1.7151 maximum difference 2.3e-15
next open vectorized 1.6462 maximum difference 2.8e-15Both 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.
Exercises
- 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
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}")coc=True capital 1.6601
coc=False capital 1.5934With 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.
- 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
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)}")capital 1.4990, orders 47The 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.
- 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
-
PyPI metadata of
backtesting,backtraderandIbPy2(versions, dates, license and classifiers) and thekernc/backtesting.pyandmementum/backtraderrepositories on GitHub (license, last commit, archive status), consulted on October 21, 2026. ↩
Each answer is folded under its question
Self-assessment
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 requiresset_coc(True)in backtrader, which its own documentation calls cheating.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.
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.
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.
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(MultiBacktestruns the same strategy on several separately). backtrader supports several data feeds in one strategy,setcommissionwithmarginandmultfor 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 withself.I) andnext();Backtestruns it on an OHLC DataFrame of a single instrument andoptimizesweeps parameters. - backtrader:
Cerebrocoordinates 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
Documentation
- backtesting.py: API documentation (Backtest, Strategy, optimize)
- backtesting on PyPI (0.6.6, July 22, 2026; AGPL-3.0)
- backtrader: Cerebro
- backtrader: orders, creation and execution
- backtrader on PyPI (1.9.78.123, April 19, 2023; GPLv3+)
- backtesting.py: text of its AGPL-3.0 license (section 13, remote network interaction)