Saltar al contenido

Prueba tus estrategias de trading con Backtrader

    Portada backtrader

    Una vez que tenemos realizado nuestro robot de trading es importarte probarlo antes de ponerlo en producción. Hay gente que directamente lo hace con una cuenta demo y en datos reales, pero otros prefieren asegurarse de forma más rápida que su estrategia funciona. Para este cometido nos ayuda mucho la librería Backtrader.

    Backtrader te facilita mucho para probar todas aquellas estrategias que se te van ocurriendo que pueden funcionar en la bolsa de una forma muy rápida y con muy pocas líneas de código, lo cual se agradece mucho, ya que lo que normalmente queremos es probar rápido nuestro robot, que ya nos costó construir y no queremos perder mucho más tiempo en tener que programar nuevamente un nuevo código para hacer las pruebas.

    Con Backtrader además veremos claramente donde falla nuestro robot o estrategia y podemos ir poniendo rápidamente mejoras en aquellos puntos donde vemos carencias graves que nos hacen perder muchos puntos en nuestro trading.

    Todo el código de esta entrada lo podéis encontrar en mi github para que os sea más fácil el copiar y pegar el código y empezar a trastear directamente.

    No me enrollo más vamos allá con la herramienta.

    Instalación y primeros pasos

    La instalación de esta librería es tan simple como ya estamos acostumbrados a hacerlo en cualquier librería de Python:

    pip install backtrader

    No hay mucha complicación con esto, ¿no? Ahora vayamos a la estructura general de como se utiliza backtrader.

    La estructura más básica para utilizar Backtrader es muy sencilla:

    # Importamos la librería para hacer el backtest
    import backtrader as bt
    # Creamos la clase donde voy a tener mi estrategia
    class estrategia(bt.Strategy):
       def next(self):
          # Metodo para definir como se va a comportar el robot
    # Instanciamos la clase Cerebro
    cerebro = bt.Cerebro()
    # Añadimos nuestra estrategia a la clase anterior
    cerebro.addstrategy(estrategia)
    # Ejecutamos la clase
    cerebro.run()
    # Mostramos un gráfico donde se ve como ha funcionado nuestra estategia
    cerebro.plot()

    Como vemos en el código no hay mucha complicación en lo que es la base de Backtrader, aunque también es verdad que se puede ver muy básico lo que hace, porque podemos ver que hay mucho trabajo todavía por hacer si queremos probar un robot. Además de definir la estrategia que va a tener ese robot podemos complicarnos mucho más. Así a primera vista parece que no tenemos nada que gestione el capital, pero eso también lo trae implementado y lo veremos más adelante.

    Ejemplo: El cruce dorado

    Vamos a hacer un ejemplo simple de como sería el cruce de medias de 50 y de 200, a estos cruces se les suele conocer como cruce dorado (cuando se cree que el precio va a empezar a subir) que es cuando la media de 50 cruza hacía arriba sobre la media de 200 y hay que comprar. También existen el contrario, llamado cruce de la muerte, que es cuando la media de 50 cruza sobre la media de 200 y hay que vender, pero este no lo vamos a implementar aquí, solo por hacer el código más corto.

    De todas formas cualquiera que entienda como funciona el backtesting de el cruce dorado no tardará más de 10 minutos en implementar el cruce de la muerte, ya que es añadir unos cuantos detalles más.

    Descarga de datos

    Lo primero que vamos a hacer es descargarnos los datos para poder introducirlos en nuestra estrategia. Backtrader ya implementa lo que ellos llaman “Data feeds” que son unos conectores con las fuentes de datos para intentar facilitarnos la vida con la carga de datos.

    Se llevan especialmente bien con los datos que ofrece yahoo, así que nosotros vamos también a utilizar esa fuente por hacer las cosas más sencillas para el tutorial, pero funciona perfectamente con otras fuentes de datos como las que ya vimos en Financial Modeling Prep o Darwinex.

    Nosotros vamos a descargar los datos de Yahoo utilizando la librería “yahoo_fin“, que podéis instalar como cualquier otra librería con el comando pip. Y luego para descargar los datos es tan sencillo como hacer lo siguiente:

    from yahoo_fin.stock_info import get_data
    starbucks= get_data("sbux", 
       start_date="01/01/2015", 
       end_date="01/01/2022", 
       index_as_date=True, 
       interval="1d")

    Y con esto ya tendremos los datos diarios de los últimos 12 años para poder hacer nuestro backtesting. Es un buen periodo así que tendremos que ver bien como se producen estos cruces varias veces. Hemos escogido la acción de Starbucks ya que parece que tiene más cruces que otras acciones y así veremos como nuestra estrategia entra y sale varias veces.

    Creando nuestra estrategia

    Nuestra estrategia va a ser muy sencilla:

    • Si la media de 50 cruza sobre de la media de 200: Comprar
    • Si la media de 50 cruza debajo de la media de 200: Vender
    • Solo una operación a la vez (como es lógico con esta estratégia)

    Aunque la clase que podíamos desarrollar podría ser sencilla vamos a incluir algo más para que se puedan ver las primeras pinceladas de como realizar códigos más complejos. Veamos como lo implementamos:

    class cruce50_200(bt.Strategy):
      def __init__(self):
        # Inicializamos la media de 50 y la de 200
        self.sma50 = bt.indicators.SimpleMovingAverage(self.data, period=50, plotname="50 SMA")
        self.sma200 = bt.indicators.SimpleMovingAverage(self.data, period=200, plotname="200 SMA")
        self.cruce = bt.ind.CrossOver(self.sma50,self.sma200)
        # Inicializamos la variable order, que usaremos para el control de las ordenes
        # Para mantener el seguimiento de las órdenes pendientes
        self.order = None
        self.buyprice = None
        self.sellprice = None
        # guardamos el último dato de cierre
        self.dataclose = self.datas[0].close
      def next(self):
        # Comprobamos si estamos en el mercado
        # si no lo estamos seguimos para adelante
        if not self.position:
          # Si la media de 50 es mayor que la media de 200 compramos
          # print("{}: {} - {}".format(self.datas[0].datetime.date(0), self.sma50[0], self.sma200[0]))
          if self.cruce > 0:
            self.order = self.buy()
            self.log('Orden de compra lanzada: %.2f' % self.dataclose[0])
        # Si estamos en el mercado
        else:
          if self.cruce < 0:
              # Si es así vendemos (con todos los parametros por defecto posibles)
              self.log('Orden de venta lanzada: %.2f' % self.dataclose[0])
              
              # Mantenemos un seguimiento de la orden para evitar abrir una segunda orden
              self.order = self.sell()
      # Creamos este método para el control de las ordenes
      def notify_order(self, order):
        # Orden de compra o de venta aceptada por el brocker - Nada que hacer
        if order.status in [order.Submitted, order.Accepted]:
          return
        # Orden de compra completada
        # Comprobamos si es de compra o de venta y mostramos los resultados
        if order.status in [order.Completed]:
          if order.isbuy():
            self.buyprice = order.executed.price
            self.log('COMPRA EJECUTADA [Precio: %.2f, Comisión: %.2f]' % (order.executed.price, order.executed.comm))
          elif order.issell():
            self.sellprice = order.executed.price
            self.log('VENTA EJECUTADA [Precio: %.2f, Comisión: %.2f]' % (order.executed.price, order.executed.comm))
        # Orden de compra cancelada
        elif order.status == order.Canceled:
          self.log('Orden Cancelada')
        # Orden de compra cancelada por el margen de tu dinero
        elif order.status == order.Margin:
          self.log('Orden de Margen')
        # Orden de compra rechazada
        elif order.status == order.Rejected:
          self.log('Orden Rechazada')
      # Metodo para mostrar de manera más fácil los logs por pantalla
      # incluyendo la fecha en la que se producen los eventos
      def log(self, txt, dt=None):
            # Vamos mostrando los datos
            dt = dt or self.datas[0].datetime.date(0)
            print('%s, %s' % (dt.isoformat(), txt))

    Dentro de la clase que hemos llamado "cruce50_200" vemos que hemos implementado 4 métodos distintos:

    • __init__: Donde se produce la inicialización de los elementos, como en cualquier clase normal.
    • next: Esta es la clase que se llamará por cada paso que da nuestro algoritmo. En el caso que estamos examinando tenemos datos diarios, así que cada día que vaya examinando entrará en este método y evaluará lo que haya dentro.
    • notify_order: Aquí controlaremos como está la orden que hemos ejecutado por parte del broker y controlaremos todos los estados que pueden darse
    • log: Esta clase es simplemente de apoyo para mostrar los datos por pantalla con la fecha de forma más bonita.

    Como he dicho anteriormente hay muchas cosas que se podrían quitar y dejar esto aun más simple, pero creo que era necesario ponerlas por dos puntos. Uno para que podáis ver como se controlarían las cosas si se complica la estrategia (cosa que si os metéis en esto seguramente haréis) y dos, para como se podrían ejecutar otras estrategias muy distintas a la que estamos implementando.

    Examinando la estrategia en profundidad

    Vamos a analizar un poco más el método “next” para dar claridad sobre algún punto que pudiera quedar poco claro solo viendo el código.

    Al principio en la inicialización de la clase (en el "__init__") vemos como inicializamos los valores de la media de 50, 200 e incluso su cruce. Esto es algo que nos da ya hecho Backtrader y que nos facilita mucho la vida. Existen muchísimos indicadores que ya vienen preprogramados y que podemos ver en su web.

    Los indicadores que hemos utilizado aquí son dos:

    Con estas dos inicializaciones hacemos que backtrader nos calcule la media y luego que nos mire el cruce entre ambas medias. Como veis es muy fácil cambiar y poner cualquier otro indicador y ver cuando cruza uno sobre otro y así poder crear la estrategia con facilidad.

    Otra línea que puede parece curiosa es:

    self.dataclose = self.datas[0].close

    Que luego nos referimos a ella como:

    self.dataclose[0]

    Aquí lo que hacemos es coger el último dato de cierre de la vela actual que se está mirando dentro del método “next”. Si quisiéramos el valor de cierre de la anterior vela sería “Self.dataclose[1]" y sucesivamente. Es una forma fácil de tener los valores anteriores para también poder hacer cálculos para nuestras estrategias, y Backtrader también nos lo pone bastante fácil.

    Vemos que la forma de hacer una compra y una venta es muy sencilla, mediante self.buy() o self.sell(), pero también se pueden poner poner ordenes de Stop y de Limit, decir el tamaño de acciones o lotes que se quieren comprar y mucho más. Podéis ver todas estas opciones aquí.

    El resto de la clase de puede entender fácilmente.

    Datos económicos

    Una de las partes que debemos tener en cuenta son los datos económicos de las transacciones que vamos a hacer, dinero inicial, etc. Esto se puede controlar muy fácilmente desde la parte del código donde hacemos correr nuestra estrategia de esta manera:

    cerebro = bt.Cerebro(stdstats=False)
    
    # Añadimos los datos que hemso descargado previamente
    data1 = bt.feeds.PandasData(dataname=starbucks)
    cerebro.adddata(data1)
    
    # Añadimos la cantidad inicial de dinero con la que vamos a realizar el trading
    cerebro.broker.setcash(10000.0)
    # Añadimos la comisión - 0.1%
    cerebro.broker.setcommission(commission=0.001)
    # Tamaño de los lotes que queremos comprar
    cerebro.addsizer(bt.sizers.FixedSize, stake=1)
    cerebro.addstrategy(cruce50_200)
    # Mostramos los valores tanto inicial como final durante la ejecución del
    # proceso de backtesting
    print('Valor inicial del portfolio: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('Valor final del porfolio: %.2f' % cerebro.broker.getvalue())

    Como se puede ver añadir la cantidad de dinero, las comisiones e incluso el tamaño por defecto que queremos comprar se hace muy fácil. Lógicamente en estrategias más complejas esto puede ir variando y se puede ir modificando sobre la marcha durante la ejecución de backtrader.

    También hemos puesto un par de líneas junto antes y después de la ejecución de Backtrader para ver el saldo inicial y final de la estrategia y así tener mucho más claro si estamos haciendo bien o mal nuestro trabajo.

    Si ejecutamos ahora mismo este código lo que nos mostraría la pantalla sería esto:

    Valor inicial del portfolio: 10000.00
    2017-01-06, Orden de compra lanzada: 57.13
    2017-01-09, COMPRA EJECUTADA [Precio: 57.26, Comisión: 0.06]
    2017-08-17, Orden de venta lanzada: 53.04
    2017-08-18, VENTA EJECUTADA [Precio: 52.92, Comisión: 0.05]
    2018-01-09, Orden de compra lanzada: 59.18
    2018-01-10, COMPRA EJECUTADA [Precio: 60.00, Comisión: 0.06]
    2018-06-25, Orden de venta lanzada: 50.66
    2018-06-26, VENTA EJECUTADA [Precio: 50.51, Comisión: 0.05]
    2018-10-30, Orden de compra lanzada: 58.59
    2018-10-31, COMPRA EJECUTADA [Precio: 58.98, Comisión: 0.06]
    2020-03-05, Orden de venta lanzada: 76.19
    2020-03-06, VENTA EJECUTADA [Precio: 73.46, Comisión: 0.07]
    2020-09-16, Orden de compra lanzada: 88.38
    2020-09-17, COMPRA EJECUTADA [Precio: 87.05, Comisión: 0.09]
    2021-11-18, Orden de venta lanzada: 112.90
    2021-11-19, VENTA EJECUTADA [Precio: 112.73, Comisión: 0.11]
    Valor final del porfolio: 10025.78

    Donde vemos exactamente todas las operaciones realizadas y como ha sido la comisión.

    Lógicamente estos datos se puede poner mucho más bonitos y dar mucha más información pero eso ya os lo dejo a vosotros ya que simplemente es trabajo de picar código.

    Mostrando la gráfica

    La gráfica con el resultado se puede mostrar simplemente con el comando “cerebro.plot()”. Pero yo por hacerlo un poco más bonito incluyo algo más:

    cerebro.plot(style='candle', 
    	iplot=False, 
    	volume = True, 
    	barupfill = False, 
    	bardownfill = False, 
    	barup='green', 
    	bardown='red')

    Y en la salida que obtenemos con Backtrader es está:

    Backtrader usado en las acciones de starbucks

    Las opciones que se pueden poner con "plot()" son muchas, y recomiendo echar un ojo a su documentación para ponerlo a gusto de cada uno.

    En esta gráfica muestro como se han realizado las operaciones, la cantidad de beneficio que hemos obtenido, si las operaciones han sido buenas o malas e incluso cuando el cruce de líneas me daba un valor u otro, lo cual es muy interesante para ver si se han hecho las cosas bien.

    Conclusiones

    Como podéis ver Backtrader tiene mucho potencial, y aquí solo hemos rascado un poco la superficie de todo lo que se puede llegar a conseguir. Creo que es importante ver la parte de documentación en su web ya que tiene mucho contenido y muy bien explicado, aunque lamentablemente en inglés solamente.

    El ejemplo que hemos visto aquí era muy tonto, pero empezando con esto ya se pueden empezar a ver como se puede mejorar esta estrategia. Podríamos incorporar otros indicadores para tener más confianza en nuestras entradas o quizá implementar nuestro propio medidor en código para controlar algo nuevo que hemos visto y mirar la tendencia, ya que esta estrategia falla mucho si no hay una tendencia clara.

    Como veis las posibilidades son muy grandes, así que recomiendo jugar un poco con esta librería y sacarle el máximo provecho.

    Como siempre cualquier duda o aclaración no dudéis en ponerla en los comentarios o enviarme un mensaje y os contestaré lo antes posible.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *