---
title: "2.4.4 backtesting.py y backtrader: uso y mantenimiento"
description: "Dos motores didácticos de Python: API de backtesting.py y de backtrader, licencias AGPL y GPL, estado de mantenimiento y la semántica de next()."
url: "https://pythonparatrading.com/curso-python-trading/nivel-2/backtesting-py-y-backtrader/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-2/backtesting-py-y-backtrader.png"
---

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

- Curso: [Nivel 2: Estrategias sistemáticas y backtesting riguroso](https://pythonparatrading.com/curso-python-trading/nivel-2/)
- Módulo: 2.4 · Backtesting: motores y paradigmas
- Duración: 50 min
- Requisitos: [2.4.2 · Backtest por eventos desde cero: bucle, cartera, órdenes y fills](https://pythonparatrading.com/curso-python-trading/nivel-2/backtest-por-eventos-desde-cero/), [1.1.4 · Tipos de órdenes](https://pythonparatrading.com/curso-python-trading/nivel-1/tipos-de-ordenes/)

## Al terminar esta lección

- Leer y escribir una estrategia en backtesting.py (`init`, `next`, `Backtest`, `optimize`) y en backtrader (`Cerebro`, `Strategy`, líneas, `notify_order`).
- Conocer el estado de cada librería con fuentes primarias: última versión, licencia (AGPL-3.0 y GPL-3.0) y lo que implica cada una.
- Programar un adaptador con la semántica de `next()` de backtrader (líneas con `[0]` y `[-1]`, órdenes a la apertura siguiente o al cierre) y reconciliarlo con un cálculo vectorizado.
- Comparar estos motores con el propio de la lección 2.4.2 y con vectorbt, y elegir según el problema.

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

| | backtesting.py | backtrader |
|---|---|---|
| Paquete en PyPI | `backtesting` | `backtrader` |
| Última versión | 0.6.6, 22 de julio de 2026 | 1.9.78.123, 19 de abril de 2023 |
| Primera versión en PyPI | 17 de enero de 2019 | 30 de mayo de 2019 (1.9.74.123) |
| Versiones publicadas en PyPI | 27 | 5 |
| Python declarado | ≥ 3.9 | Clasificadores hasta 3.7; el README dice ≥ 3.2 |
| Licencia | AGPL-3.0 | GPL-3.0 (GPLv3+ en PyPI) |
| Último commit en la rama principal | 5 de agosto de 2026 | 19 de abril de 2023 |
| Repositorio archivado | No | No |

*Fig. — Versiones publicadas en PyPI de backtrader, backtesting.py y vectorbt, de 2019 a octubre de 2026.* Tres filas de marcas, una por versión publicada. backtrader: cinco versiones entre mayo de 2019 y el 19 de abril de 2023 (la última, destacada) y ninguna después. backtesting.py: 27 versiones, muchas en 2020, un hueco de diciembre de 2021 a enero de 2025 y la última en julio de 2026. vectorbt: 66 versiones desde diciembre de 2020, la última el 19 de 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:

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

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

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

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

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:

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

```mermaid
flowchart LR
  N[next en barra t] --> O[Orden de mercado]
  O --> A[Apertura de t+1]
  O --> C[Cierre de 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), "sesiones; hueco nocturno medio absoluto:",
      f"{100 * hueco.abs().mean():.2f} %")
```
```output
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.

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

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

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

```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}")
```
```output
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.* Arriba, dos curvas de capital semanales casi superpuestas: ejecutando al cierre de la señal termina en 1,72 y a la apertura siguiente (destacada) en 1,65, con las mismas 33 órdenes. Abajo, la diferencia en puntos de capital, cerca de cero en 2022 y creciendo a escalones hasta 6,9 puntos al final.

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:

| Concepto | Motor propio (2.4.2) | backtrader | backtesting.py |
|---|---|---|---|
| Decidir en cada barra | `al_recibir_barra(ev)` | `next()` | `next()` |
| Datos de la barra | `ev.barras[i].cierre` | `self.data.close[0]` | `self.data.Close[-1]` |
| Indicadores | Estado propio de la estrategia | `bt.ind.*` en `__init__` | `self.I()` en `init()` |
| Pedir una posición | `SignalEvent(peso)` | `order_target_percent` | `buy(size=fracción)` |
| Aviso de ejecución | `al_recibir_fill(f)` | `notify_order(order)` | (operaciones en `self.trades`) |
| Ejecución por defecto | La que fije la orden (`cierre`, `mercado`) | Apertura siguiente | Apertura siguiente |
| Varios activos | Sí | 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

| Necesidad | Recomendación |
|---|---|
| Aprender la estructura de un motor | Escribir uno (lección 2.4.2) y leer backtrader |
| Probar rápido una idea en un solo activo con gráfico | backtesting.py |
| Carteras, futuros con multiplicador y margen, varios marcos temporales | backtrader, sabiendo que no se mantiene, o NautilusTrader (2.4.5) |
| Barridos masivos de parámetros | vectorbt o NumPy propio (lección 2.4.3) |
| El mismo código en backtest y en real, mantenido | NautilusTrader o LEAN (lecciones 2.4.5 y 2.4.6) |
| Un producto que se vaya a distribuir o vender | Revisar 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.

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

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

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

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.

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

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

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

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

   - A. Al cierre de la barra t
   - B. A la apertura de la barra t+1
   - C. Al cierre de la barra t+1
   - D. Al precio medio de la barra t

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

   - A. Que no se puede usar para operar con dinero real
   - 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
   - C. Que es de dominio público
   - D. Que obliga a publicar las estrategias

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

   - A. Una de 2026, con soporte de Python 3.14
   - 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
   - C. No está en PyPI
   - D. Se archivó en 2020

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

   - A. Por las comisiones
   - 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
   - C. Porque el adaptador tiene un error
   - D. Porque cambia el número de operaciones

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

   - A. Optimización de parámetros
   - B. Órdenes stop
   - C. Varios activos en una misma cartera con caja compartida, comisiones con multiplicador y margen de futuros, y conexiones a brokers en real
   - D. Gráficos

   *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

### Research

- [Tu primer sistema en Python: cruce de medias móviles sobre SPY](https://pythonparatrading.com/primer-sistema-cruce-de-medias/) · Research-0018 · 7 min

### Documentación

- [backtesting.py: documentación de la API (Backtest, Strategy, optimize)](https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html) · kernc.github.io
- [backtesting en PyPI (0.6.6, 22 de julio de 2026; AGPL-3.0)](https://pypi.org/project/backtesting/) · pypi.org
- [backtrader: Cerebro](https://www.backtrader.com/docu/cerebro/) · backtrader.com
- [backtrader: órdenes, creación y ejecución](https://www.backtrader.com/docu/order-creation-execution/order-creation-execution/) · backtrader.com
- [backtrader en PyPI (1.9.78.123, 19 de abril de 2023; GPLv3+)](https://pypi.org/project/backtrader/) · pypi.org
- [backtesting.py: texto de su licencia AGPL-3.0 (sección 13, interacción remota por red)](https://github.com/kernc/backtesting.py/blob/master/LICENSE.md) · github.com

Lección anterior: [2.4.3 · vectorbt: barridos masivos de parámetros y simulación de carteras](https://pythonparatrading.com/curso-python-trading/nivel-2/vectorbt-barridos-de-parametros/)

Lección siguiente: [2.4.5 · NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real](https://pythonparatrading.com/curso-python-trading/nivel-2/nautilustrader-arquitectura-y-estrategias/)
