Kevin Meneses
← Volver a artículos

Backtrader vs vectorbt vs Backtesting.py: ejecuté la misma estrategia con 10 años de datos

Publicado originalmente en dev.to

PythonAlgorithmic TradingPython TradingBacktesting
Backtrader vs vectorbt vs Backtesting.py: ejecuté la misma estrategia con 10 años de datos

Backtrader y vectorbt coinciden al céntimo con la misma estrategia, y Backtesting.py se queda a menos de 45 $, con cada dólar de diferencia explicado.

Pero solo después de alinear unas cuantas reglas que cada librería resuelve a su manera. Si ejecutas cada una con su configuración por defecto, el mismo cruce de medias sobre los mismos datos te da tres respuestas distintas, con un 5,5 % de diferencia entre ellas.

Si estás:

  • eligiendo una librería de backtesting en Python para tu primer proyecto serio,
  • pasando una estrategia de un motor a otro,
  • o preguntándote por qué tu backtest no coincide con el de otra persona,

esto te interesa.

Pasé una misma estrategia por las tres librerías con 10 años de datos diarios de SPY y después hice un barrido de 80 combinaciones de parámetros en cada una. Esto es lo que coincidió, lo que no y por qué.

El montaje

Todo lo que sigue es reproducible:

  • Datos: barras diarias de SPY.US de EODHD, del 28 de octubre de 2016 al 28 de octubre de 2026 (2.513 barras), ajustadas por splits y dividendos
  • Estrategia: cruce de medias SMA 50/200, solo largos. Compra cuando la media de 50 días cruza por encima de la de 200 y vende cuando cruza por debajo
  • Cuenta: 10.000 $, todo el capital, acciones enteras, 0,1 % de comisión por operación
  • Librerías: backtrader 1.9.78.123, vectorbt 1.2.0, backtesting 0.6.6, con Python 3.12 y pandas 3.1
  • Ejecución: 29 de octubre de 2026

No busco una buena estrategia. Quiero dar a tres motores el mismo trabajo y ver si coinciden.

Paso 1: una única fuente de datos limpia para los tres

Los motores solo pueden coincidir si leen los mismos números. Por eso los datos salen de un solo sitio, se ajustan una vez y se guardan en un CSV que leen todos los scripts.

import os
import pandas as pd
import requests

r = requests.get(
    "https://eodhd.com/api/eod/SPY.US",
    params={"api_token": os.environ["EODHD_API_KEY"], "fmt": "json",
            "from": "2016-10-28", "to": "2026-10-28"},
    timeout=30,
)
raw = pd.DataFrame(r.json()).set_index("date")
raw.index = pd.to_datetime(raw.index)

# One adjustment factor per day, applied to every price
f = raw["adjusted_close"] / raw["close"]
df = pd.DataFrame({
    "Open": raw["open"] * f, "High": raw["high"] * f, "Low": raw["low"] * f,
    "Close": raw["adjusted_close"], "Volume": raw["volume"],
})
df.to_csv("spy.csv")

El paso del ajuste importa más de lo que parece. EODHD devuelve el close sin ajustar y un adjusted_close ajustado por splits y dividendos. Si le das precios sin ajustar a un backtest, cada split parece un desplome y cada dividendo una pequeña pérdida. Aplicar el mismo factor diario a la apertura, el máximo y el mínimo mantiene coherente toda la barra, y aquí eso importa porque los tres motores ejecutan las órdenes en la apertura.

Son diez años de datos con una sola llamada a la API.

Paso 2: el mismo cruce en tres librerías

Estas son las tres versiones con las reglas alineadas. En la siguiente sección explico cada decisión.

Backtrader

import math
import backtrader as bt
import pandas as pd

FEE = 0.001
df = pd.read_csv("spy.csv", index_col=0, parse_dates=True)


class SmaCross(bt.Strategy):
    params = dict(fast=50, slow=200)

    def __init__(self):
        self.cross = bt.ind.CrossOver(bt.ind.SMA(period=self.p.fast),
                                      bt.ind.SMA(period=self.p.slow))

    def next_open(self):
        # Runs before today's bar is processed: cross[0] is yesterday's signal
        if not self.position and self.cross[0] > 0:
            size = math.floor(self.broker.getcash() / (self.data.open[0] * (1 + FEE)))
            self.buy(size=size)
        elif self.position and self.cross[0] < 0:
            self.close()


cerebro = bt.Cerebro(cheat_on_open=True)
cerebro.adddata(bt.feeds.PandasData(dataname=df))
cerebro.addstrategy(SmaCross)
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=FEE)
cerebro.run()
print(f"Backtrader final value: {cerebro.broker.getvalue():,.2f}")

vectorbt

import numpy as np
import pandas as pd
import vectorbt as vbt

df = pd.read_csv("spy.csv", index_col=0, parse_dates=True)

fast = vbt.MA.run(df["Close"], 50)
slow = vbt.MA.run(df["Close"], 200)
entries = fast.ma_crossed_above(slow).shift(1, fill_value=False)  # act on the next bar
exits = fast.ma_crossed_below(slow).shift(1, fill_value=False)

pf = vbt.Portfolio.from_signals(
    df["Close"], entries, exits,
    price=df["Open"],      # fill at the next open, not at the signal close
    size=np.inf, size_granularity=1,  # all in, whole shares
    init_cash=10_000, fees=0.001, freq="1D",
)
print(f"vectorbt final value: {pf.final_value():,.2f}")

Backtesting.py

import pandas as pd
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA

df = pd.read_csv("spy.csv", index_col=0, parse_dates=True)


class SmaCross(Strategy):
    def init(self):
        self.fast = self.I(SMA, self.data.Close, 50)
        self.slow = self.I(SMA, self.data.Close, 200)

    def next(self):
        if crossover(self.fast, self.slow):
            self.buy()
        elif crossover(self.slow, self.fast):
            self.position.close()


stats = Backtest(df, SmaCross, cash=10_000, commission=0.001, finalize_trades=True).run()
print(f"Backtesting.py final value: {stats['Equity Final [$]']:,.2f}")

Los tres scripts imprimen el mismo valor final con los mismos datos de EODHD

Backtrader y vectorbt terminan exactamente en 21.263,19 $. Backtesting.py termina en 21.218,60 $, y en la siguiente sección se ve de dónde salen esos 44,59 $.

Sin contar líneas en blanco ni comentarios, son 23 líneas en Backtrader, 15 en vectorbt y 16 en Backtesting.py.

Por qué no coinciden con la configuración por defecto

Ahora, la versión que ejecuta la mayoría: cada librería con su configuración de serie. En vectorbt es un from_signals(close, entries, exits) sin más. En Backtrader usé su sizer AllInSizerInt, que calcula el tamaño de la orden con el cierre de la barra de la señal. Backtesting.py se ejecutó con un buy() sin parámetros.

Configuración por defecto frente a reglas alineadas en los tres motores

Ejecución (sin comisión)BacktradervectorbtBacktesting.py
Por defecto21.231,58 $22.395,33 $21.325,07 $
Reglas alineadas21.348,83 $21.348,83 $21.325,07 $

Por defecto, vectorbt queda un 5,5 % por encima de Backtrader. Misma estrategia, mismos datos, las mismas cuatro operaciones. Para ver qué ajuste causa la diferencia, los cambié de uno en uno en vectorbt:

Ejecución de vectorbt (sin comisión)Valor final
Por defecto: ejecuta al cierre de la señal, fracciones de acción22.395,33 $
Solo acciones enteras22.223,85 $
Solo ejecución en la apertura siguiente21.613,44 $
Apertura siguiente y acciones enteras21.348,83 $

La última fila es exactamente el resultado alineado de Backtrader. Esto es lo que hace cada cambio.

1. Cuándo se ejecuta la orden. Por defecto, la primera entrada de vectorbt fue el 26 de marzo de 2019 a 251,97 $, el cierre del día de la señal. Backtrader y Backtesting.py entraron el 27 de marzo en la apertura, a 251,96 $. Ejecutar al cierre de la señal significa comprar al mismo precio que usaste para decidir, algo que en la vida real no puedes hacer. Desplazar las señales una barra y pasar price=df["Open"] pone a vectorbt en la regla de la apertura siguiente.

2. Fracciones de acción. Por defecto, vectorbt compró 39,6881 acciones en esa primera entrada. Los otros dos compraron 39. Con size_granularity=1, vectorbt opera con acciones enteras.

El resultado por defecto de Backtrader también queda por debajo de su versión alineada, por un motivo parecido: AllInSizerInt calcula el tamaño con el cierre de la barra de la señal, pero la orden se ejecuta en la apertura siguiente. En la segunda entrada eso supuso 30 acciones en lugar de 31.

3. Cómo se trata la última operación abierta. Esta diferencia se mantiene tras alinear, y explica cada dólar que separa a Backtesting.py de los otros dos. Los tres motores hacen las mismas cuatro operaciones a los mismos precios. La cuarta sigue abierta al final de los datos. vectorbt y Backtrader la valoran al último cierre, 772,31 $, sin vender. Backtesting.py, con el ajuste finalize_trades=True que le pasé, la cierra de verdad en la apertura de la última barra, 771,43 $:

  • Sin comisión, son 27 acciones por 0,88 $, es decir, 23,76 $.
  • Con un 0,1 % de comisión, Backtesting.py también paga la comisión de esa venta de cierre: 27 acciones por 771,43 $ por 0,1 %, es decir, 20,83 $. Sumados a los 23,76 $, dan los 44,59 $ completos.

Así que, cuando coinciden la ejecución y el redondeo de acciones, Backtrader y vectorbt coinciden al céntimo, y Backtesting.py solo difiere en cómo liquida la última operación abierta.

Lo comprobé también con una estrategia más activa. Con un cruce 10/30, con 36 operaciones en diez años, los tres terminaron exactamente en 20.254,43 $.

Dos trampas de Backtrader que me costaron una operación

Backtrader fue la que más código necesitó y donde cometí mis dos errores.

Trampa 1: una orden de todo el capital que no puede pagar la comisión

Mi primera regla de tamaño fue floor(cash / open). Sin comisiones funciona. Con un 0,1 % de comisión, la orden cuesta un poco más que el efectivo disponible, y el broker de Backtrader la rechaza con el estado Margin.

No hay ningún error. La orden simplemente no se ejecuta.

En el cruce 70/250 eso se saltó la tercera entrada, el 23 de marzo de 2023. Backtrader terminó con 3 operaciones y un 65,8 % de rentabilidad, mientras que vectorbt y Backtesting.py hicieron 4 operaciones y ganaron un 149 %. Calcular el tamaño con floor(cash / (open * (1 + fee))) lo arregló, y los tres motores volvieron a coincidir. Si inviertes todo el capital, cuenta las órdenes rechazadas en notify_order, o no te darás cuenta.

Trampa 2: next_open lee los indicadores de ayer

Para operar en la apertura con acciones enteras calculadas sobre esa apertura, Backtrader ofrece cheat_on_open=True y un método next_open(). Dentro de next_open, los indicadores todavía no se han actualizado con la barra nueva. Así que self.cross[0] sigue siendo la señal de la barra anterior.

Primero escribí cross[-1], suponiendo que [0] sería el valor de hoy. Todas las operaciones llegaron un día tarde: la primera entrada fue el 28 de marzo de 2019 en lugar del 27, y el resultado se alejó de los otros dos motores. Imprimir ambos valores alrededor de esa fecha del cruce mostró el problema.

Velocidad: donde vectorbt juega en otra liga

Un backtest tarda una fracción de segundo en cualquiera de las tres. La diferencia aparece cuando pruebas muchos juegos de parámetros.

Ejecuté el mismo barrido en cada motor: medias rápidas de 10 a 100 y lentas de 50 a 250, 80 parejas válidas, 0,1 % de comisión.

Barrido de 80 parejas de parámetros: tiempo y resultados en cada motor

Motor80 backtests
vectorbt, primera llamada5,09 s
vectorbt, ya en caliente0,08 s
Backtesting.py3,23 s
Backtrader19,27 s

La primera llamada de vectorbt incluye compilar sus funciones con numba, así que un barrido aislado no es más rápido que con Backtesting.py. En caliente, ejecuta las 80 carteras como un único cálculo vectorizado, unas 40 veces más rápido que Backtesting.py y unas 250 veces más rápido que Backtrader.

Los tres encontraron la misma mejor pareja, 80/250. En las 80 parejas, vectorbt y Backtrader dieron resultados idénticos, y Backtesting.py nunca se alejó más de 0,545 puntos porcentuales, lo que encaja con cómo liquida la última operación abierta.

Dónde falla esto y de qué me fiaría

Los motores coinciden. La estrategia es otra historia.

SMA 50/200 y SMA 10/30 frente a buy and hold, mismos datos y comisiones

En esos mismos diez años, comprar SPY y mantenerlo convirtió 10.000 $ en 42.429,98 $, un 324,30 % de rentabilidad. Incluso comprando el 16 de agosto de 2017, el primer día en que existe la media de 200 días, dio un 260,49 %. El cruce 50/200 dio un 112,63 %. Incluso la mejor de las 80 parejas, la 80/250, dio un 158,40 %. Ninguna se acercó.

Resultados de la SMA 10/30 y del buy and hold

Los cruces hicieron una cosa mejor: la versión 10/30 redujo la caída máxima al 17,44 %, frente al 33,72 % del buy and hold. Si eso compensa renunciar a dos tercios de la rentabilidad es una pregunta para tu tolerancia al riesgo, no para un backtest.

Tres advertencias más:

  • Elegir la mejor de 80 es sobreajuste. La 80/250 ganó en esta década de SPY. Elegirla porque ganó y luego presentar su rentabilidad te habla del pasado, no de los próximos diez años. Prueba la pareja elegida con datos que no haya visto.
  • El 0,1 % de comisión no es todo el coste. En estas pruebas no hay deslizamiento ni horquilla. Con cuatro operaciones en diez años apenas importa. Con una estrategia que opera a diario, sí.
  • Que los motores coincidan no demuestra que el resultado sea correcto. Los tres pueden coincidir en un backtest que usa precios equivocados. Por eso el paso de los datos va primero. Lo explico con más detalle en Backtesting in Python: Why Most Backtests Lie.

¿Cuál deberías usar?

BacktradervectorbtBacktesting.py
EstiloPor eventos, simula un brokerArrays vectorizadosPor eventos, API compacta
Líneas para esta estrategia231516
Barrido de 80 pruebas19,27 s0,08 s en caliente, 5,09 s en frío3,23 s
Ejecución en mi prueba por defectoApertura siguienteCierre de la señalApertura siguiente
Fracciones de acción en mi prueba por defectoNoSíNo
Última versión en PyPI19 de abril de 202319 de octubre de 202622 de julio de 2026
Trampa a vigilarLa comisión puede rechazar órdenes de todo el capital; índices en next_openDesplazar señales y fijar el precio de ejecuciónCon finalize_trades=True, la última operación se cierra en la última apertura

Backtrader

Ventajas

  • Un modelo de broker completo: tipos de orden, comisiones, control de margen, varias fuentes de datos
  • El bucle de eventos se parece al código de trading real, así que pasar a un broker es más fácil

Inconvenientes

  • La más lenta de las tres en barridos de parámetros
  • Sin versiones nuevas en PyPI desde abril de 2023. Aun así, funcionó bien con Python 3.12 y pandas 3.1

Ideal para: estrategias con gestión realista de órdenes, varios instrumentos o con intención de pasar a real.

vectorbt

Ventajas

  • 80 backtests en 0,08 segundos una vez compilado
  • El código más corto de esta prueba, por una línea

Inconvenientes

  • Valores por defecto distintos de los otros dos (ejecución al cierre de la señal, fracciones de acción)
  • La lógica que depende de la trayectoria, como un stop dinámico que depende de cada barra anterior, es más difícil de expresar que en un bucle de eventos

Ideal para: investigación, barridos de parámetros y comparar muchas señales rápido.

Backtesting.py

Ventajas

  • Una API pequeña y legible, con una tabla de estadísticas incluida
  • 80 backtests en unos 3 segundos

Inconvenientes

  • Un solo instrumento por backtest
  • Con finalize_trades=True, cierra la última operación abierta en la apertura de la última barra y cobra la comisión, lo que mueve ligeramente el resultado

Ideal para: estrategias de un solo activo en las que quieres código claro y resultados rápidos.

Cómo conseguir los datos

Los diez años de SPY de esta prueba salieron de una sola llamada a la API de fin de día de EODHD, con adjusted_close haciendo el trabajo del ajuste.

El plan gratuito cubre un año de histórico de fin de día y 20 llamadas a la API al día, suficiente para probar el código con un rango corto. Diez años de histórico necesitan el plan EOD Historical All-World o superior.

👉 Consigue tu API key de EODHD aquí. Usa el código KEVIN10 al pagar para un 10 % de descuento.

Conclusiones clave

  • Con la misma regla de ejecución y el mismo redondeo de acciones, Backtrader y vectorbt coinciden al céntimo con los mismos datos, y Backtesting.py solo difiere en cómo liquida la última operación abierta.
  • Con la configuración por defecto, el mismo cruce difiere un 5,5 %, sobre todo porque vectorbt ejecuta al cierre de la barra de la señal.
  • Ninguno de los 80 cruces de medias superó al buy and hold de SPY en los últimos diez años. El motor que elijas importa menos que la pregunta que pruebas.

Preguntas frecuentes

❓ ¿Backtrader sigue manteniéndose? ✅ Su última versión en PyPI es la 1.9.78.123, del 19 de abril de 2023, y no ha salido ninguna después. Aun así, se instaló y funcionó bien con Python 3.12 y pandas 3.1 en esta prueba, y coincidió al céntimo con vectorbt al alinear las reglas.

❓ ¿Es vectorbt más rápido que Backtrader? ✅ En barridos de parámetros, con mucha diferencia. En esta prueba, 80 backtests tardaron 0,08 segundos en vectorbt ya en caliente, frente a 19,27 segundos en Backtrader. La primera llamada de vectorbt tardó 5,09 segundos porque compila sus funciones.

❓ ¿Por qué mis resultados de backtest cambian según la librería? ✅ Normalmente por el momento de ejecución, el redondeo de acciones o cómo se trata la última operación abierta. En esta prueba, vectorbt por defecto ejecutaba al cierre de la barra de la señal con fracciones de acción, mientras que Backtrader y Backtesting.py ejecutaban en la apertura siguiente con acciones enteras.

❓ ¿Qué librería de backtesting en Python es mejor para empezar? ✅ Backtesting.py tiene la API más pequeña y un informe de estadísticas legible, y ejecutó esta estrategia en 16 líneas. vectorbt es todavía más corta, pero sus valores por defecto requieren cuidado para reflejar ejecuciones realistas.

❓ ¿Debo usar precios ajustados para hacer backtesting? ✅ Sí. Los precios sin ajustar convierten los splits en falsos desplomes y dejan fuera los dividendos. Aplica el factor de ajuste a la apertura, el máximo y el mínimo además del cierre, para que las órdenes ejecutadas en la apertura usen precios coherentes.


Los tres motores no son lo difícil. Con las reglas alineadas coinciden, y cada dólar que queda de diferencia tiene un motivo.

Lo difícil es la pregunta que les haces, y si la respuesta mejora el no hacer nada.


¿Buscas contenido técnico para tu empresa? Puedo ayudarte — LinkedIn · kevinmenesesgonzalez@gmail.com

Backtrader vs vectorbt vs Backtesting.py: ejecuté la misma estrategia con 10 años de datos