Optimización Algorítmica

Si ha seguido esta serie de artículos sobre backtesting financiero y probado sus propios algoritmos, sin duda en algún punto le habrán asaltado las dudas respecto a qué parámetros utilizar en sus funciones de trading. ¿Por qué usar un periodo de 50 sesiones en la media móvil breve? ¿Sería mejor usar stops de tipo fijo o de tipo trailing? ¿Y en ambos casos, cuán cerca del precio?

Hasta fecha relativamente reciente la respuesta que hubiese recibido sería “porque sí”, “porque es la costumbre” o, si es afortunado, “porque la experiencia me ha demostrado que es lo que funciona”, ahora cállese y haga auto de fé. Argumentos de tradición o autoridad son malas bases sobre las que construir un esfuerzo científico, y por suerte hoy en día disponemos de una alternativa mejor: experimentar nosotros mismos.

En verdad realizar un backtest ya es un paso en el camino de la verificación rigurosa en lugar de lanzarse al vacío a ciegas o creer religiosamente en lo que el gurú de turno le cuenta. El siguiente paso consiste en llevar a cabo diferentes backtests sobre un mismo escenario base cambiando ligeramente los parámetros en cada versión. Gracias a los avances en computación y al abaratamiento de la capacidad de proceso está al alcance de cualquier bolsillo la posibilidad de realizar miles, millones de experimentos en serie para quedarnos con la versión que nos ofrezca mejores resultados. Eso es lo que se conoce como Optimización Algorítmica.

Tipos de Optimización

Para evitar que nos acusen de reduccionismo permítanme mencionar que la optimización algorítmica es una disciplina muy amplia y con varios niveles de creciente complejidad, dependiendo de lo que queramos optimizar y las técnicas que utilicemos para ello.

En el nivel más básico tenemos la Optimización de Parámetros: encontrar las variables más oportunas para las reglas de nuestro algoritmo. Los ejemplos siguientes se centrarán en esta clase de optimización, que es también la más popular. Piense que aquí la cantidad de parámetros puede ir de unos pocos hasta cientos, e involucrar entradas, salidas, stops, activación selectiva de funciones… pero en todos los casos las reglas se consideran como algo constante y fijo.

Cuando cuestionamos dicha inmutabilidad entramos en el nivel de la Búsqueda de Reglas, en el que los escenarios difieren no sólo en los valores de los parámetros sino también en la forma conceptual de las propias reglas. Por ejemplo; la media móvil es simplemente un estilo de reglas de cruce, que forman parte de la categoría de reglas de tendencia. Otras categorías ofrecen reglas de divergencia, de difusión, y un largo etcétera. Combinando diversos estilos entre múltiples categorías mientras mantenemos la complejidad constante (por ejemplo, limitándonos a tres reglas simples) obtenemos escenarios con algoritmos muy diferentes.

Finalmente, el nivel superior de optimización algorítmica es la Inducción de Reglas, en el que permitimos reglas de complejidad creciente (por ejemplo, mediante agregación de funciones) utilizando técnicas de inteligencia artificial para inducción autónoma como algoritmos genéticos, redes neurales y máquinas de vectores de soporte (SVMs) con el fin de encontrar el grado de complejidad que produce los mejores resultados. Estas técnicas permiten la optimización en el universo más amplio posible pero nuestras necesidades computacionales también se disparan, con lo que las veremos más adelante.

Optimizando con Un Ordenador

Para nuestro ejemplo vamos a optimizar una de nuestras estrategias previas, MyBasicStrategy que incluye cruces de medias móviles y stops. Por favor descargue este archivo OptimizerLocal.py adaptado como se describe a continuación. También necesitará el fichero original con los datos de Gas Natural.

Cuando sólo disponemos de un computador en PyAlgoTrade usaremos la clase local dentro del módulo pyalgotrade.optimizer:

from pyalgotrade.optimizer import local

local.run(MyBasicStrategy, feed, parameters_generator())

Para ejecutar la clase local debemos pasarle la plantilla de las estrategias (la clase MyBasicStrategy), el feed con los datos, y los parámetros con que experimentar, en este caso generados con nuestra propia función para tener un código más ordenado:

import numpy as np
import itertools

def parameters_generator():
  stopPer = np.arange(0.05, 0.95, 0.05)
  stopTrailing = [True, False]
  smaShort = range(10, 100, 10)
  smaLong = range(50, 500, 10)
  return itertools.product(stopPer, stopTrailing, smaShort, smaLong)

Como puede verse usamos las funciones del módulo itertools de Python para devolver todas las iteraciones posibles del producto de los parámetros en los rangos especificados mediante range(valorInicial, valorFinal, incremento). Esta función opera sólo con enteros, así que en el caso del parámetro stopPer (el punto a partir del cual saltará nuestro stop) cuyo rango oscila entre 0 y 1 utilizamos en su lugar la función arange del paquete numpy en incrementos de 0.05. Por supuesto el orden en que se definan debe coincidir con lo que nuestra estrategia espera para que los valores se asignen de forma adecuada.

class MyBasicStrategy(MyBenchmark):
  def __init__(self, feed, stopPer, stopTrailing, smaShort, smaLong):

En cada iteración el motor de optimización instanciará MyBasicStrategy y le pasará los valores correspondientes. Como vamos a ver muchos escenarios en serie y no queremos sobrecargarnos de información inútil, silenciaremos la función logOp:

 def logOp(self, type, order):
  pass

Y sólo imprimiremos una línea informativa por iteración:

self.info("%s %s %s %s" % (stopPer, stopTrailing, smaShort, smaLong))

Teniendo en cuenta los rangos especificados arriba para los parámetros nos manejaremos con un universo de 18 * 2 * 8 * 50 = 14400 combinatorias diferentes. No se preocupe, un humano se tiraría años para terminar el trabajo, pero si cuenta con un computador moderadamente reciente se ventilará los 14400 escenarios en poco más de una hora. Para acelerar los cálculos mantendremos la información en memoria desde el archivo CSV en lugar de cargarlos secuencialmente desde la base de datos.

Además tener una sola máquina no significa que las iteraciones se realicen meramente en serie; el motor de optimización detectará automáticamente si su ordenador cuenta con varios procesadores y correrá tantos escenarios en paralelo como le sea posible.

El resultado obtenido será:

Screen Shot 2016-08-17 at 10.16.13 AM

Es decir, la mayor rentabilidad acumulada se obtiene con la combinación de parámetros [0.1, False, 80, 230]. Esto confirma nuestra conjetura de que los stops de tipo trailing para asegurar ganancias no hacen sino dañar nuestra rentabilidad final. Si queremos conocer los detalles de dicho escenario podemos introducir esos valores en la estrategia original (le dejo aquí el archivo adaptado) y obtendremos lo siguiente:

Screen Shot 2016-08-17 at 10.18.01 AM

Lo cual es coherente con los resultados anteriores: una rentabilidad del 332% (4321049 sobre una inversión inicial de 1000000) mejorando en gran medida los resultados del escenario base.

Siguientes Pasos

Mucho nos queda que hablar sobre la optimización algorítmica: cómo optimizar para diferentes funciones objetivo (no sólo la rentabilidad acumulada), cómo utilizar capacidad de procesamiento distribuida (en lugar de una máquina), conectar con bases de datos en red… así como protegernos de los peligros de la sobreoptimización (por ejemplo, usando la metodología llamada walk-forward optimization). Tiempo al tiempo.

De momento, para practicar sus habilidades recientemente adquiridas, podría trabajar en optimizar la otra estrategia incluida en ese archivo, MyTaLibStrategy que cuenta con:

  • Un cruce de medias móviles, como arriba.
  • Un stop fijo o trailing, de porcentaje ajustable, como arriba.
  • Un MACD sobre el precio, que combina tres medias diferentes (corta, larga y señal).
  • Un MACD sobre el volumen, con otras tres media móviles.
  • Un indicador Aroon cuyo periodo, cota de entrada y cota de salida son ajustables.

Si pone rangos amplios, como por ejemplo de 5 a 50 para las medias y 5 a 95 para las cotas, las combinatorias rápidamente crecen: 18 * 2 * 8 * 50 * 45 * 45 * 45 * 45 * 45 * 45 * 45 * 90 * 90 = 43584805012500000000 posibilidades. Puede limitar el universo de exploración reduciendo los rangos y aumentando los pasos (en lugar de ir de 1 en 1), o bien incluir más ordenadores en una red distribuida, como veremos pronto.

Otro punto a tener en cuenta es que varias de las combinaciones darán lugar a escenarios absurdos, como por ejemplo una media corta de periodo más amplio que una media larga dentro de un MACD, con lo cual nuestro código se quejará. Para evitar que ello detenga la ejecución de las demás iteraciones, capturaremos la excepción lanzada y simplemente desactivaremos este escenario, eventualmente devolviendo una rentabilidad de cero.

self._active = False

Les dejo aquí un TaLibStrategy preparado. Espero sus preguntas si les puedo resultar de ayuda en este tema de gran utilidad, pero también notable complejidad. ¡Gracias por leerme, y si les ha gustado por favor no olviden compartir en sus redes sociales!.

Anuncios

3 comentarios en “Optimización Algorítmica

  1. Francisco

    Buenas tardes Isaac,

    Por favor, ¿podrías subir de nuevo los ficheros utilizados?, ya no están disponibles y el tema es muy interesante.

    Muchas gracias.
    Un saludo.

    Me gusta

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s