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().
- Módulo
- 2.4 · Backtesting: motores y paradigmas
- Duración
- 50 min
Lecciones del nivel 2
2.1 · Estadística y econometría para trading
- 2.1.1 Contrastes de hipótesis, p-valores, bootstrap y regresión con errores robustos
- 2.1.2 Estacionariedad: ADF, KPSS, diferenciación y diferenciación fraccional
- 2.1.3 Modelos ARMA y ARIMA: autocorrelación simple (ACF) y parcial (PACF)
- 2.1.4 Modelos de volatilidad: ARCH, GARCH, EGARCH y GJR con la librería arch
- 2.1.5 Cointegración: Engle-Granger, Johansen y vida media de la reversión
- 2.1.6 Filtro de Kalman para hedge ratios dinámicos
- 2.1.7 Detección de regímenes: modelos de Markov ocultos y cambios estructurales
- 2.1.8 El problema de las pruebas múltiples: Bonferroni, FDR y el «factor zoo»
2.2 · Anatomía de un sistema de trading
- 2.2.1 Hipótesis de ventaja: riesgo, comportamiento, estructura e información
- 2.2.2 Universo, señal, filtro, entrada, salida, sizing y gestión del riesgo
- 2.2.3 Frecuencias: intradía, swing y posicional, y la capacidad de una estrategia
- 2.2.4 Diario de investigación y registro de experimentos contra el data snooping
2.3 · Familias de estrategias
- 2.3.1 Seguimiento de tendencia y momentum
- 2.3.2 Reversión a la media: RSI y Bollinger, reversión intradía y huecos
- 2.3.3 Pairs trading y arbitraje estadístico: distancia, cointegración y Kalman
- 2.3.4 Carry en divisas, futuros y bonos; estacionalidad y efectos calendario
- 2.3.5 Factores (value, calidad, baja volatilidad, tamaño) y estrategias de eventos
- 2.3.6 Introducción al market making: inventario, spread y selección adversa (Avellaneda-Stoikov)
- 2.3.7 Estrategias específicas de cripto: momentum, arbitraje de funding y basis
2.4 · Backtesting: motores y paradigmas
- 2.4.1 Backtest vectorizado desde cero: matrices, desfase, costes y límites
- 2.4.2 Backtest por eventos desde cero: bucle, cartera, órdenes y fills
- 2.4.3 vectorbt: barridos masivos de parámetros y simulación de carteras
- 2.4.4 backtesting.py y backtrader: dos motores didácticos y su estado
- 2.4.5 NautilusTrader: arquitectura, datos, estrategias y el mismo código en backtest y en real
- 2.4.6 LEAN/QuantConnect y Zipline Reloaded; bt para asignación de activos
- 2.4.7 Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos
- 2.4.8 Modelado de fills: órdenes limitadas, cola, fills parciales, huecos y stops
2.5 · Sesgos y validación
- 2.5.1 Sesgos de backtest: anticipación, supervivencia, selección y narrativa
- 2.5.2 In-sample y out-of-sample: walk-forward anclado y móvil
- 2.5.3 Validación cruzada combinatoria purgada para estrategias
- 2.5.4 Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: remuestreo de trades, bootstrap de bloques y permutaciones de señales
- 2.5.6 Sensibilidad de parámetros: mesetas frente a picos, robustez entre activos y periodos
- 2.5.7 Datos sintéticos para pruebas de estrés: GBM, GARCH y bootstrapping
- 2.5.8 Checklist de «backtest honesto» antes de pasar a paper trading
2.6 · Métricas de rendimiento
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.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 |
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:
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:
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.
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…) ynext()decide en cada barra;notify_orderynotify_tradereciben los cambios de estado de órdenes y operaciones. - Broker: el simulado (
BackBroker) con comisiones (setcommission), deslizamiento (set_slippage_perc) y los modosset_coc(ejecutar al cierre) yset_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:
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:
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.
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]
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} %")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.
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)11.0 10.0 nan
IndexError: [1] es el futuroDespué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.
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.
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)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.74Las 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.
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}")al cierre (coc) vectorizado 1.7151 diferencia máxima 2.3e-15
apertura siguiente vectorizado 1.6462 diferencia máxima 2.8e-15Los 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.
Ejercicios
- 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
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.5934Con 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.
- 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
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)}")capital 1.4990, órdenes 47El 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.
- 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
-
Metadatos de PyPI de
backtesting,backtradereIbPy2(versiones, fechas, licencia y clasificadores) y repositorioskernc/backtesting.pyymementum/backtraderen GitHub (licencia, último commit, estado de archivo), consultados el 21 de octubre de 2026. ↩
Respuesta plegada en cada pregunta
Autoevaluación
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=Truelas órdenes de mercado se ejecutan a la apertura de la barra siguiente. Ejecutar al cierre de t exigeset_coc(True)en backtrader, que su propia documentación llama hacer trampa.¿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.
¿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.
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.
¿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(MultiBacktestejecuta la misma estrategia en varios por separado). backtrader admite varios data feeds en una estrategia,setcommissionconmarginymultpara 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 conself.I) ynext();Backtestla ejecuta sobre un DataFrame OHLC de un solo instrumento yoptimizebarre parámetros. - backtrader:
Cerebrocoordina 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
Documentación
- backtesting.py: documentación de la API (Backtest, Strategy, optimize)
- backtesting en PyPI (0.6.6, 22 de julio de 2026; AGPL-3.0)
- backtrader: Cerebro
- backtrader: órdenes, creación y ejecución
- backtrader en PyPI (1.9.78.123, 19 de abril de 2023; GPLv3+)
- backtesting.py: texto de su licencia AGPL-3.0 (sección 13, interacción remota por red)