Lección 2.4.4 · 50 min

backtesting.py y backtrader: dos motores didácticos y su estado

backtesting.py y backtrader son los dos motores de backtest en Python con los que más gente ha aprendido. Esta lección enseña su código real, mira en PyPI y GitHub cómo están (versiones, licencias, mantenimiento) y reproduce con un adaptador propio la regla que más resultados cambia sin que nadie lo note: cuándo se ejecuta una orden enviada desde next().

Fig. — Qué ofrece cada motor según su documentación y sus metadatos (octubre de 2026): el motor por eventos de la lección 2.4.2, backtesting.py, backtrader y vectorbt
04/08
Módulo
2.4 · Backtesting: motores y paradigmas
Duración
50 min
Lecciones del nivel 2
  1. 2.1 · Estadística y econometría para trading

    1. 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
    2. 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
    3. 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
    4. 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
    5. 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
    6. 2.1.6 Filtro de Kalman para hedge ratios dinámicos
    7. 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
    8. 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
  2. 2.2 · Anatomía de un sistema de trading

    1. 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
    2. 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
    3. 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
    4. 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
  3. 2.3 · Familias de estrategias

    1. 2.3.1 Seguimiento de tendencia y momentum
    2. 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
    3. 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
    4. 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
    5. 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
    6. 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
    7. 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
  4. 2.4 · Backtesting: motores y paradigmas

    1. 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
    2. 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
    3. 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
    4. 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
    5. 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
    6. 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
    7. 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
    8. 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
  5. 2.5 · Sesgos y validación

    1. 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
    2. 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
    3. 2.5.3 Validación cruzada combinatoria purgada para estrategias
    4. 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
    5. 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
    6. 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
    7. 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
    8. 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
  6. 2.6 · Métricas de rendimiento

    1. 2.6.1 CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index
    2. 2.6.2 Win rate, payoff, expectativa, alpha, beta, information ratio, turnover y capacidad
    3. 2.6.3 Informes con QuantStats o pyfolio-reloaded y tearsheets propios

Dos motores con los que aprendió mucha gente

Antes de NautilusTrader o de vectorbt, quien quería hacer un backtest en Python con algo más que pandas acababa casi siempre en una de estas dos librerías. backtrader es un motor por eventos completo, con varios activos, muchos tipos de orden, comisiones de futuros y conexión con brokers. backtesting.py es más pequeño y más directo: una estrategia, un instrumento, un DataFrame, un informe y un gráfico interactivo. Las dos se llaman a menudo «didácticas» porque se leen bien y enseñan la estructura de un motor; la lección 2.4.2 construyó esa misma estructura desde cero, así que aquí se pueden leer con ojo crítico.

En este curso no se ejecutan (decisión del titular): su código aparece en bloques sin ejecutar, tal como lo documentan las propias librerías, y la parte ejecutable reproduce con código propio la decisión de diseño que más cambia sus resultados. Su estado, leído en PyPI y GitHub el 21 de octubre de 20261Nota 1 · Metadatos de PyPI de backtesting, backtrader e IbPy2 (versiones, fechas, licencia y clasificadores) y repositorios kernc/backtesting.py y mementum/backtrader en GitHub (licencia, último commit, estado de archivo), consultados el 21 de octubre de 2026.:

backtesting.pybacktrader
Paquete en PyPIbacktestingbacktrader
Última versión0.6.6, 22 de julio de 20261.9.78.123, 19 de abril de 2023
Primera versión en PyPI17 de enero de 201930 de mayo de 2019 (1.9.74.123)
Versiones publicadas en PyPI275
Python declarado≥ 3.9Clasificadores hasta 3.7; el README dice ≥ 3.2
LicenciaAGPL-3.0GPL-3.0 (GPLv3+ en PyPI)
Último commit en la rama principal5 de agosto de 202619 de abril de 2023
Repositorio archivadoNoNo
Fig. — Versiones publicadas en PyPI de backtrader, backtesting.py y vectorbt, de 2019 a octubre de 2026

La figura enseña la historia de publicaciones. backtesting.py tuvo un parón entre diciembre de 2021 y enero de 2025 y volvió a publicar con regularidad; backtrader publicó en PyPI cinco versiones y ninguna desde abril de 2023. Que una librería sin versiones nuevas siga funcionando con un Python moderno es posible, pero no está garantizado: no recibe correcciones ni adaptaciones a los cambios de pandas, NumPy o Matplotlib, y sus dependencias de conexión en real (IbPy para Interactive Brokers, según su README) tampoco se mueven: el paquete IbPy2 de PyPI publicó su última versión, la 0.8.0, en octubre de 2016.

Las licencias importan si vas a distribuir algo. Las dos son copyleft. La GPL-3.0 de backtrader exige que, si se distribuye un programa que incluye o deriva de ella, se distribuya con su código fuente y con la misma licencia. La AGPL-3.0 de backtesting.py añade el uso en red: quien ofrece a usuarios remotos una versión modificada como servicio tiene que darles acceso a ese código. Para un trader que importa la librería en sus propios scripts y opera con su dinero no hay nada que publicar. Para quien quiera montar una plataforma o un producto, conviene leer el texto de la licencia con un abogado antes de escribir la primera línea; es la misma advertencia que la Commons Clause de vectorbt (lección 2.4.3), con otras condiciones.

backtesting.py: una estrategia en una clase

La estrategia hereda de Strategy y define dos métodos. En init() se declaran los indicadores con self.I(), que los calcula de una vez sobre toda la serie y luego los revela barra a barra: dentro de next(), self.ma1 solo contiene los valores hasta la barra actual. En next() se decide. El ejemplo de su README, con los datos de ejemplo que trae la librería:

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 recibe un DataFrame con columnas Open, High, Low, Close (y Volume opcional) y la clase de la estrategia. Sus parámetros, según la documentación de la API: cash (10.000 por defecto), spread, commission (en fracción del valor de la orden, aplicada a la entrada y a la salida desde la versión 0.4.0; admite también un mínimo fijo o una función), margin (1 = sin apalancamiento), trade_on_close, hedging, exclusive_orders (cada orden nueva cierra la posición anterior) y finalize_trades. Lo importante está en una frase de la documentación de buy(): salvo con trade_on_close=True, las órdenes de mercado se ejecutan a la apertura de la barra siguiente; las limitadas y las stop, cuando se cumple su condición.

La optimización viene incluida:

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 es una clave de la serie de resultados de run() (por defecto, el SQN de Van Tharp) o una función; method puede ser "grid" (todas las combinaciones, o una muestra si se limita con max_tries) o "sambo" (optimización basada en modelos). Para que n1 y n2 sean parámetros, la estrategia los declara como atributos de clase y los usa en init(). La advertencia de la lección 2.4.3 vale igual: optimize devuelve el máximo de un barrido, y el máximo de un barrido es en buena parte ruido.

Sus límites, de la propia documentación: un Backtest es un instrumento. MultiBacktest ejecuta la misma estrategia sobre varios por separado, para comparar, no una cartera con caja compartida. Para fracciones de unidad (cripto) existe FractionalBacktest. backtesting.lib trae piezas útiles, como crossover, resample_apply para varios marcos temporales y TrailingStrategy, un trailing stop expresado en múltiplos del ATR (y, con set_trailing_pct, un porcentaje que la documentación advierte que es inexacto porque se convierte a ATR).

backtrader: Cerebro, líneas y next()

backtrader es un motor por eventos con más piezas. Cerebro es el coordinador: recibe los datos, las estrategias, el broker y los complementos, y ejecuta el bucle.

CerebroData feedsStrategyBrokerAnalyzersObserversSizer
Fuente 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), proveedores en línea o en real; varios a la vez, con remuestreo y repetición de barras.
  • Strategy: __init__ declara indicadores (bt.ind.SMA, bt.ind.CrossOver…) y next() decide en cada barra; notify_order y notify_trade reciben los cambios de estado de órdenes y operaciones.
  • Broker: el simulado (BackBroker) con comisiones (setcommission), deslizamiento (set_slippage_perc) y los modos set_coc (ejecutar al cierre) y set_coo (a la apertura); o uno real.
  • Sizer: decide el tamaño cuando la estrategia no lo da. Analyzers (SharpeRatio, DrawDown, TradeAnalyzer…) y observers calculan métricas y series para el gráfico.

Las series son líneas: self.data.close[0] es el cierre de la barra actual y self.data.close[-1] el de la anterior, sin que la estrategia sepa en qué barra está. La misma estrategia que en backtesting.py, al estilo de la documentación de backtrader:

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())

Para un futuro, la comisión se da por contrato junto con el margen y el multiplicador; el ejemplo de su documentación es el del Euro Stoxx 50 en Interactive Brokers:

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

# y un trailing stop del 4 % como orden del broker simulado
self.sell(exectype=bt.Order.StopTrail, trailpercent=0.04)

Con margin, backtrader trata el instrumento como un futuro: la comisión es fija por contrato y el efectivo se ajusta cada día con las pérdidas y ganancias, la contabilidad que la cartera de la lección 2.4.2 simplificaba. StopTrail recalcula el stop con el último cierre y, cuando se toca, lo ejecuta como orden de mercado, según su documentación.

Un adaptador con la semántica de next()

Ninguna de las dos librerías se ejecuta aquí, pero su regla central sí se puede reproducir y medir: una estrategia escrita como en backtrader, con líneas indexadas desde la barra actual y órdenes que se ejecutan a la apertura de la barra siguiente, sobre un pequeño motor propio con el orden de eventos de la lección 2.4.2. Los datos son las barras diarias de KAPA, el ETF sintético de 2022 a 2025 de las lecciones anteriores, construidas desde sus barras de 5 minutos.

next en barra tOrden de mercadoApertura de t+1Cierre de t: coc
Fuente Mermaid
flowchart LR
  N[next en barra t] --> O[Orden de mercado]
  O --> A[Apertura de t+1]
  O --> C[Cierre de 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), "sesiones; hueco nocturno medio absoluto:",
      f"{100 * hueco.abs().mean():.2f} %")
OUT [05]Barras diarias de KAPA
1003 sesiones; hueco nocturno medio absoluto: 0.38 %

Primero, las líneas. Una Linea guarda toda la serie pero solo deja leer hasta la barra actual: [0] es la actual, [-1] la anterior y [1], el futuro, es un error. Un indicador es otra línea, calculada de una vez pero revelada al mismo ritmo, que es exactamente lo que hace self.I() en backtesting.py.

IN [06]python
class Reloj:
    def __init__(self):
        self.i = -1                       # índice de la barra actual

class Linea:
    """Serie revelada barra a barra: [0] actual, [-1] anterior."""

    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}] es el futuro")
        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 si a cruza b al alza en la barra, -1 a la baja, 0 si no."""
    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]Líneas e indicadores
11.0 10.0 nan
IndexError: [1] es el futuro

Después, el adaptador: un motor mínimo que llama a next() en cada barra y ofrece a la estrategia lo que espera de backtrader (self.data, self.position, buy(), close(), notify_order()). El orden de las cosas en cada barra es el de la lección 2.4.2: primero se ejecutan las órdenes pendientes con la apertura de la barra, después decide la estrategia. Con coc=True (como set_coc(True) en backtrader o trade_on_close=True en backtesting.py) la orden se ejecuta al cierre de la barra en la que se envía. Para que la comparación sea exacta, cada compra invierte todo el capital en el momento de ejecutarse, sin comisiones.

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:
    """Lo mínimo de la API de backtrader que usa una estrategia."""

    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)     # todo el 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:     # órdenes de la barra anterior
                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)

La estrategia se escribe como en backtrader: indicadores en __init__, decisión en next() con índices relativos a la barra actual.

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 = "al cierre (coc)" if coc else "apertura siguiente"
    resultados[nombre] = (eq, motor.ordenes)
    print(f"{nombre:19s} capital {eq.iloc[-1]:.4f}, "
          f"órdenes {len(motor.ordenes)}")
o = resultados["apertura siguiente"][1][0]
print("primera orden:", o.creada.date(), "->", o.ejecutada.date(),
      "a", o.precio)
OUT [08]SmaCross, al estilo de backtrader
al cierre (coc)     capital 1.7151, órdenes 33
apertura siguiente  capital 1.6462, órdenes 33
primera orden: 2022-03-10 -> 2022-03-11 a 250.74

Las mismas señales y el mismo número de órdenes (33); distinto capital final: 1,7151 ejecutando al cierre de la señal y 1,6462 a la apertura siguiente. La primera orden se crea en next() del 10 de marzo de 2022 y se ejecuta a la apertura del 11, a 250,74. Que la diferencia no es un error del adaptador se demuestra igual que en la lección 2.4.2: reconciliando con un cálculo vectorizado que aplica cada convención. Al cierre es el shift(1) de siempre; a la apertura siguiente, la posición nueva empieza a la apertura y la noche anterior se pasa con la posición vieja.

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)
# estado tras next() de cada barra (largo o fuera), con memoria
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)       # posición desde la apertura
s2 = estado.shift(2, fill_value=0)       # posición durante la noche
r_apertura = ((1 + s2 * (o_ / c.shift(1) - 1).fillna(0))
              * (1 + s1 * (c / o_ - 1)) - 1)
for nombre, rv in [("al cierre (coc)", r_cierre),
                   ("apertura siguiente", r_apertura)]:
    eq_v = (1 + rv).cumprod()
    dif = (resultados[nombre][0] / eq_v - 1).abs().max()
    print(f"{nombre:19s} vectorizado {eq_v.iloc[-1]:.4f}  "
          f"diferencia máxima {dif:.1e}")
OUT [09]Reconciliación con el vectorizado
al cierre (coc)     vectorizado 1.7151  diferencia máxima 2.3e-15
apertura siguiente  vectorizado 1.6462  diferencia máxima 2.8e-15
Fig. — La misma estrategia de cruce de medias sobre KAPA, 2022–2025, en el adaptador con la semántica de next(): órdenes ejecutadas al cierre de la señal o a la apertura siguiente, y la diferencia de capital

Los dos se reconcilian al redondeo de la coma flotante. La diferencia entre ellos sale entera del hueco nocturno: cada compra a la apertura paga (o cobra) lo que el precio se movió desde el cierre de la señal, y cada venta igual. En KAPA, un ETF sintético con un hueco nocturno medio del 0,38 %, son casi 7 puntos de capital en cuatro años; en un activo con huecos grandes (acciones con resultados, cripto con fines de semana, futuros con sesiones partidas) puede ser todo el resultado. Y fíjate en la trampa: un backtest vectorizado con shift(1) sobre cierres equivale al modo coc, el que backtrader llama hacer trampa. Ejecutar al cierre de la señal solo es honesto si la señal se puede calcular y la orden enviar antes del cierre (lección 1.1.4).

La correspondencia entre las tres arquitecturas, la del motor de la lección 2.4.2 y las dos librerías:

ConceptoMotor propio (2.4.2)backtraderbacktesting.py
Decidir en cada barraal_recibir_barra(ev)next()next()
Datos de la barraev.barras[i].cierreself.data.close[0]self.data.Close[-1]
IndicadoresEstado propio de la estrategiabt.ind.* en __init__self.I() en init()
Pedir una posiciónSignalEvent(peso)order_target_percentbuy(size=fracción)
Aviso de ejecuciónal_recibir_fill(f)notify_order(order)(operaciones en self.trades)
Ejecución por defectoLa que fije la orden (cierre, mercado)Apertura siguienteApertura siguiente
Varios activosSíSíNo (uno por Backtest)

Ojo a una diferencia de índices: en backtesting.py, self.data.Close es un array con los datos hasta la barra actual, así que la última es [-1]; en backtrader, la actual es [0]. Traducir una estrategia de una librería a otra sin tenerlo en cuenta desplaza todo una barra.

Cuál elegir

NecesidadRecomendación
Aprender la estructura de un motorEscribir uno (lección 2.4.2) y leer backtrader
Probar rápido una idea en un solo activo con gráficobacktesting.py
Carteras, futuros con multiplicador y margen, varios marcos temporalesbacktrader, sabiendo que no se mantiene, o NautilusTrader (2.4.5)
Barridos masivos de parámetrosvectorbt o NumPy propio (lección 2.4.3)
El mismo código en backtest y en real, mantenidoNautilusTrader o LEAN (lecciones 2.4.5 y 2.4.6)
Un producto que se vaya a distribuir o venderRevisar antes las licencias: AGPL, GPL y Commons Clause tienen condiciones

La figura de la cabecera resume las capacidades de cada uno según su documentación. Lo que ninguna tabla dice es cómo se comporta cada motor en los detalles (el orden de los eventos dentro de una barra, qué precio toma un stop con hueco, cuándo se calcula el tamaño de una orden). Antes de fiarte de un resultado, reconcílialo con algo independiente, como se ha hecho en esta lección y en la 2.4.2.

Ejercicios

  1. Añade al adaptador una comisión proporcional (por ejemplo, 0,1 % del valor negociado, como setcommission(commission=0.001) de backtrader) y mide cuánto cambia el capital final en los dos modos de ejecución.
Solución
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]Comisión en el adaptador
coc=True  capital 1.6601
coc=False capital 1.5934

Con un 0,1 % por lado y 33 órdenes en cuatro años, la comisión resta unos 5 puntos de capital en los dos modos (de 1,7151 a 1,6601 al cierre y de 1,6462 a 1,5934 a la apertura). La diferencia entre ejecutar al cierre o a la apertura no la arregla ninguna comisión: es una diferencia de precio, no de coste.

  1. Escribe en el estilo del adaptador una estrategia que use self.data.close[-1]: comprar si el cierre de hoy supera el máximo de los cierres de las 20 sesiones anteriores y salir si cae por debajo del mínimo de las 10 anteriores (un canal de Donchian, lección 2.3.1). ¿Por qué no se puede escribir con [1]?
Solución
IN [11]python
class Donchian(EstrategiaNext):
    def next(self):
        cl = self.data.close
        if np.isnan(cl[-20]):
            return                         # aún no hay 20 anteriores
        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}, órdenes {len(m.ordenes)}")
OUT [11]Donchian con líneas
capital 1.4990, órdenes 47

El canal termina con un capital de 1,4990 y 47 órdenes. [1] sería el cierre de mañana: la Linea lo prohíbe con un IndexError, y así el sesgo de anticipación no se puede escribir por descuido. Los motores por eventos protegen de esa forma: la estrategia solo ve lo que ya ha llegado. En un backtest vectorizado, el mismo error es un shift(-1) que no da ningún aviso.

  1. Un compañero ha hecho un backtest con backtesting.py y otro con un vectorizado propio de la misma estrategia diaria y no le coinciden. Enumera tres causas que comprobarías antes de buscar un error, con lo visto en esta lección.
Solución

Primera, el precio de ejecución: backtesting.py ejecuta las órdenes de mercado a la apertura de la barra siguiente si no se le pasa trade_on_close=True, y el vectorizado con shift(1) sobre cierres ejecuta al cierre de la señal. Segunda, el tamaño: buy() sin size usa casi todo el efectivo disponible (size=.9999) y en unidades enteras salvo con FractionalBacktest, mientras que un vectorizado suele usar pesos fraccionarios. Tercera, los costes: la comisión de backtesting.py se aplica a la entrada y a la salida, y spread es aparte; el vectorizado puede estar cobrando solo un lado o sobre otra base. La cuarta, si hay stops, es el orden dentro de la barra. Todas se aíslan como en la reconciliación de esta lección: cambiando una hipótesis cada vez.

Footnotes

  1. Metadatos de PyPI de backtesting, backtrader e IbPy2 (versiones, fechas, licencia y clasificadores) y repositorios kernc/backtesting.py y mementum/backtrader en GitHub (licencia, último commit, estado de archivo), consultados el 21 de octubre de 2026. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. En backtrader y en backtesting.py, por defecto, ¿a qué precio se ejecuta una orden de mercado enviada en next() de la barra t?

    • AAl cierre de la barra t
    • BA la apertura de la barra t+1
    • CAl cierre de la barra t+1
    • DAl precio medio de la barra t
    Ver respuesta

    Respuesta B. A la apertura de la barra t+1

    La documentación de backtrader dice que la orden de mercado se ejecuta una barra después de la señal con el precio de apertura; la de backtesting.py, que salvo con trade_on_close=True las órdenes de mercado se ejecutan a la apertura de la barra siguiente. Ejecutar al cierre de t exige set_coc(True) en backtrader, que su propia documentación llama hacer trampa.

  2. ¿Qué significa en la práctica que backtesting.py tenga licencia AGPL-3.0?

    • AQue no se puede usar para operar con dinero real
    • BQue si se modifica y se ofrece a otros como servicio en red, hay que poner el código fuente de esa versión a disposición de sus usuarios; para uso propio no obliga a publicar nada
    • CQue es de dominio público
    • DQue obliga a publicar las estrategias
    Ver respuesta

    Respuesta B. Que si se modifica y se ofrece a otros como servicio en red, hay que poner el código fuente de esa versión a disposición de sus usuarios; para uso propio no obliga a publicar nada

    La AGPL extiende la GPL al uso en red: quien ofrece a usuarios remotos una versión modificada debe darles acceso a su código. Una estrategia propia que importa la librería y se ejecuta en tu máquina no se distribuye ni se ofrece a nadie. Montar una plataforma web de backtesting sobre ella es otra cosa.

  3. ¿Cuál es la última versión de backtrader publicada en PyPI y qué dice de su mantenimiento?

    • AUna de 2026, con soporte de Python 3.14
    • BLa 1.9.78.123, del 19 de abril de 2023; desde entonces no hay versiones nuevas, y los clasificadores de Python del paquete llegan a la 3.7
    • CNo está en PyPI
    • DSe archivó en 2020
    Ver respuesta

    Respuesta B. La 1.9.78.123, del 19 de abril de 2023; desde entonces no hay versiones nuevas, y los clasificadores de Python del paquete llegan a la 3.7

    Los metadatos de PyPI dan cinco versiones entre 2019 y 2023, la última el 19 de abril de 2023, y el repositorio no está archivado. Funcionar en un Python moderno no es lo mismo que estar mantenido: una librería sin versiones nuevas no recibe correcciones ni adaptaciones a cambios de pandas o NumPy.

  4. En el adaptador de la lección, la misma estrategia de cruce de medias sobre KAPA termina con un capital distinto ejecutando al cierre de la señal o a la apertura siguiente. ¿Por qué?

    • APor las comisiones
    • BPorque entre el cierre de la señal y la apertura siguiente hay un hueco nocturno: cada entrada y cada salida se hace a otro precio, y esa diferencia se acumula en todas las operaciones
    • CPorque el adaptador tiene un error
    • DPorque cambia el número de operaciones
    Ver respuesta

    Respuesta B. Porque entre el cierre de la señal y la apertura siguiente hay un hueco nocturno: cada entrada y cada salida se hace a otro precio, y esa diferencia se acumula en todas las operaciones

    Las señales y el número de operaciones son los mismos; solo cambia el precio de ejecución. Los dos resultados se reconcilian exactamente con un cálculo vectorizado que aplica cada convención, así que la diferencia es la del hueco, no un error.

  5. ¿Qué tiene backtrader que backtesting.py no tiene?

    • AOptimización de parámetros
    • BÓrdenes stop
    • CVarios activos en una misma cartera con caja compartida, comisiones con multiplicador y margen de futuros, y conexiones a brokers en real
    • DGráficos
    Ver respuesta

    Respuesta C. Varios activos en una misma cartera con caja compartida, comisiones con multiplicador y margen de futuros, y conexiones a brokers en real

    backtesting.py simula un solo instrumento por Backtest (MultiBacktest ejecuta la misma estrategia en varios por separado). backtrader admite varios data feeds en una estrategia, setcommission con margin y mult para futuros y, según su README, datos y órdenes en real con Interactive Brokers, Oanda y Visual Chart.

Ideas clave

  • backtesting.py: una estrategia es una clase con init() (indicadores con self.I) y next(); Backtest la ejecuta sobre un DataFrame OHLC de un solo instrumento y optimize barre parámetros.
  • backtrader: Cerebro coordina data feeds, estrategias, broker, sizers, analizadores y observadores; las series son líneas en las que [0] es la barra actual y [-1] la anterior.
  • Por defecto, los dos ejecutan las órdenes de mercado a la apertura de la barra siguiente; con un cálculo vectorizado al cierre se obtiene otro resultado, y ninguno avisa.
  • Licencias con fuentes: backtesting.py, AGPL-3.0; backtrader, GPL-3.0 (copyleft, a diferencia de la Apache 2.0 con Commons Clause de vectorbt).
  • Estado en octubre de 2026: backtesting.py publicó la 0.6.6 en julio de 2026; backtrader no publica desde abril de 2023 (1.9.78.123).
  • Sirven para aprender y para estrategias de un activo; para carteras, futuros reales o paso a real, mejor un motor propio validado o NautilusTrader (lección 2.4.5).

Recursos