Saltar al contenido

Optimiza tu estrategia de trading con permutación de parámetros

    permutacion portada

    No todo es machine learning en las pruebas que podemos hacer para mejorar nuestro robot de trading. La permutación de parámetros es otro sistema que puede ayudarnos a encontrar los mejores parámetros para nuestro algoritmo de trading.

    Muchas veces nos fiamos de nuestras pruebas manuales o de estrategias que leemos por internet y las aplicamos sin pensar si hay otros parámetros que podrían ir mejor.

    ¿Por qué el cruce dorado se realiza con la media de 50 y de 200? ¿Por que el RSI se mira con 30 y 70? ¿Por qué las bandas de Bollinger tienen que ser con la media de 20 periodos?

    Puede que nunca os hayáis hecho estas preguntas preguntas, pero en este artículo vamos a ver si mediante código podemos encontrar la forma de mejorar estos patrones que parece que tenemos grabados a fuego y que nunca tocamos. Para ello veremos como hacerlo con la estrategia del cruce dorado.

    ¿Qué es la permutación de parámetros?

    La permutación de parámetros o System Parameter Permutation (SPP) fue una idea creada por Dave Walton en 2014 para mejorar las estrategias de trading (si te gusta ir a las fuentes aquí puedes ver el artículo original).

    La idea es conocer si una estrategia de trading con unos valores fijos es la mejor o todavía se puede optimizar mucho más. Para probarlo lo que se hace es probar todas las combinaciones posibles de parámetros de la estrategia y con todos los datos encima de la mesa saber si una estrategia es optimizable y hacernos con una idea realista de cuál será el rendimiento de la estrategia.

    Para saber si una estrategia es buena o no utiliza los valores medios para las estadísticas de rendimiento de cada estrategia: Beneficio, drawdrown, ratio de Sharpe, etc. Lógicamente cuantos más valores utilicemos para saber si nuestra estrategia es buena o no, más fiable será que no tengamos sorpresas en la operativa de nuestro robot.

    A su vez, en vez de probar todas las posibilidades, crearemos números aleatorios suficientes para probar que la estrategia cumple con la mayor parte de las posibilidades. Según Walton con estos valores ya nos podemos hacer una idea realistas de como será el rendimiento de una estrategia de trading.

    random number

    Probamos el cruce dorado

    Para hacer la prueba más simple y no liarnos con nuevas estrategia vamos a reutilizar el código que vimos para probar backtrader, donde probamos el trading del cruce dorado y vimos cual era su resultado en esa herramienta.

    También provecharemos que backtrading también nos puede dar los valores que necesitamos para ver si nuestra estrategia es rentable como el retorno, el drawdown y el ratio de Sharpe para ver como escoger los valores adecuados para el cruce dorado

    El código cambia muy poco respecto al que vimos en el anterior ejemplo:

    class cruce50_200(bt.Strategy):
        params = (
            ('slowPeriod', 50),
            ('fastPeriod', 200),
        )
        
        def __init__(self):
            # Inicializamos la media de 50 y la de 200
            self.sma50 = bt.indicators.SimpleMovingAverage(self.data, period=self.params.slowPeriod, plotname="slow SMA")
            self.sma200 = bt.indicators.SimpleMovingAverage(self.data, period=self.params.fastPeriod, plotname="fast 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()
            # Si estamos en el mercado
            else:
                if self.cruce < 0:
                    # Si es así vendemos (con todos los parametros por defecto posibles)
                    # 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
                elif order.issell():
                    self.sellprice = order.executed.price

    Lo más significativo es que ahora le pasamos los parámetros de la estrategia como argumentos y no como estaban anteriormente, que eran fijos. Esto, lógicamente, es imprescindible para poder hacer la permutación de los parámetros y probar todas las combinaciones posibles de esta técnica.

    También hemos quitado la función log y todas las líneas que mostraban la información por pantalla ya que en este caso no nos aportan información o incluso pueden obstaculizar para ver unos resultados claros.

    El cerebro

    Ahora vayamos a la parte central. Aquí respecto a la anterior hemos añadido la parte donde introducimos los analizadores para que nos muestre tanto el retorno, el drawdown y el ratio de sharpe.

    También los hemos metido en una función a la cual le vamos a pasar los parámetros que van a ir variando para hacer correcta la técnica de la permutación de parámetros.

    def run_test(parametro1, parametro2):
        cerebro = bt.Cerebro()
    
        # 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)
        # añadimos analizadores para ver los valores que necesitamos
        cerebro.addanalyzer(bt.analyzers.DrawDown, _name = "drawdown")
        cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name = "sharpe")
        cerebro.addanalyzer(bt.analyzers.Returns, _name = "returns")
    
        cerebro.addstrategy(cruce50_200, slowPeriod=parametro1, fastPeriod=parametro2)
    
        analisis = cerebro.run()
        
        return analisis

    Como se ve nada complicado, simplemente cogemos la salida que devuelve cerebro en la ejecución y lo devolvemos para que pueda ser tratado en la función que se le llama.

    La permutación de parámetros

    Aquí es donde creamos el código que verdaderamente hace la permutación de parámetros.

    def sweep_parameters(pruebas):
        slow = np.random.randint(5,200,pruebas)
        fast = np.random.randint(200,500,pruebas)
        resultados = []
        parametros = {}
        for i in range(pruebas):
            if slow[i] == fast[i]:
                continue
            resultado = run_test(slow[i], fast[i])
            analisis = [[x.analyzers.returns.get_analysis()['rnorm100'], 
                         x.analyzers.drawdown.get_analysis()['max']['drawdown'],
                         x.analyzers.drawdown.get_analysis()['max']['moneydown'],
                         x.analyzers.drawdown.get_analysis()['max']['len'],
                         x.analyzers.sharpe.get_analysis()['sharperatio'],
                         x.params.slowPeriod, 
                         x.params.fastPeriod             
                ] for x in resultado]
            resultados.append(analisis[0])
        return pd.DataFrame(resultados, columns=['returns', 'Maximo drawdown', 'Maximo Moneydown', 'Maximo tamaño drawdown', 'sharpe', 'slowPeriod', 'fastPeriod'])

    Como puedes ver en la primera parte creamos números aleatorios entre los valores que queremos que vayan nuestras dos variables. En este caso queremos que la media lenta sea de unos valores entre 5 y 200 y la media rápida de unos valores entre 200 y 500.

    Luego comprobamos que no coincidan (solo en el caso de 200 y 200) y sino lanzamos la prueba con los valores creados. De lo que nos devuelve el backtrader sacamos los valores del retorno varios con el drawdown y el Sharpe ratio.

    Finalmente, y por motivos estéticos devolvemos una Dataframe en vez de una lista como se había creado dentro del bucle.

    La función solo recibe un parámetro que es el número de veces que queremos que se lance la prueba. Aquí podemos ser todo lo amplio que queramos, cuantas más valores aleatorios pruebe el algoritmo mejor, pero también más tardará en darnos los resultados.

    Resultados

    Veamos que resultados podemos obtener con la permutación de parámetros si lanzamos el código para que haga las pruebas 10 veces:

    resultados = sweep_parameters(10)

    Si luego mostramos el valor que tiene la variable resultados podemos ver algo así:

    Resultados de la permutación de parámetros

    Aquí podemos observar todas las ejecuciones que se han realizado tanto los valores que ha devuelto la estrategia como los valores que hemos utilizado tanto en el periodo lento como en el rápido.

    Aunque se puede ver a simple vista haremos una línea para que nos devuelva algunos valores máximos, ya que si lanzamos 1000 veces la prueba verlo a mano puede ser una locura.

    resultados[resultados['returns'] == resultados['returns'].max()]

    Aquí lanzamos para ver cual es la prueba que nos ha devuelto más beneficios, y el resultado es:

    Máximo beneficio

    Parece que la prueba numero 1 es la mejor candidata, pero veamos cual ha tenido un drawdown más bajo con el siguiente comando:

    resultados[resultados['Maximo drawdown'] == resultados['Maximo drawdown'].min()]

    Y la salida es:

    máximo drawdown

    Parece que otra vez la prueba numero 1 es la mejor, ahora veamos que nos dice el valor del ratio de Sharpe:

    resultados[resultados['sharpe'] == resultados['sharpe'].max()]
    máximo ratio de Sharpe

    Aquí parece que la prueba 9 es la que se lleva la palma, pero como vemos el ratio de sharpe es muy negativo, mostrando que esta forma de hacer trading tiene un riesgo muy elevado.

    Mejoras al algoritmo propuesto

    Lógicamente se pueden hacer muchas mejoras en este algoritmo de permutación de parámetros como hacer que los valores de las parámetros no se repitan o poner más valores para probar la técnica.

    La forma de hacer la estrategia también es bastante mejorable pero por aprovechar el ejemplo anterior de backtrader y no cambiar mucho lo que se explicó en esa entrada he decidido mantenerlo así. La posibilidad de hacer otras técnicas siempre está ahí.

    Una de las mejoras más importantes que podría tener este algoritmo sería hacer el procesamiento utilizando GPUs y que cada prueba se realice en un núcleo de GPU diferente. Con ello reduciríamos mucho el tiempo de pruebas haciendo que pudiéramos realizar muchas pruebas en un tiempo relativamente corto.

    Conclusiones

    Como se puede ver la técnica de la permutación de parámetros puede ayudarnos mucho a hacer nuestro trading mucho más efectivo. Combinado con la técnica de la simulación de Montecarlo que hemos visto en la anterior entrada puede llevarnos a conseguir unos robots de trading muy estables y que nos den muy poco sustos.

    Ahora lo que nos queda es ir probando estrategias, adaptarlas a backtrader y lanzar las simulaciones para conseguir saber cual puede ser el rendimiento real si lo ponemos a trabajar.

    Como siempre, si tenéis cualquier duda o mejora del artículo no dudéis en poneros en contacto conmigo y os contestaré lo antes posible.

    Deja una respuesta

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