Jugando con Muñecas

Dudé bastante si debía añadir “Rusas” en el título de esta entrada, debatiéndome entre resultar de ayuda para esclarecer el tema de hoy o tal vez colocarme en el punto de mira de esos servicios pseudo-legales que pululan por la entrepierna de internet ofreciendo esposas eslavas de afecto negociable.

De Muñecas Rusas se trata en definitiva, pero de las tradicionales: las matryoshkas, esas figuras lindamente decoradas en cuyo interior se encuentra otra figura similar lindamente decorada, en cuyo interior se encuentra… exacto. La programación bien ejecutada se asemeja mucho a estas muñecas auto-referentes: bloques de código que, si están correctamente construidos, se ensamblan para posibilitar la creación de módulos más ambiciosos con nuevas funciones, pero que frecuentemente expresan las propiedades de sus elementos constituyentes con notable recursividad.

La programación mal ejecutada se parece más a un plato de spaghetti con salsa boloñesa.

Índices Dinámicos

Pero dejemos esa pasta para la cena y vayamos a por la que importa, la de los mercados financieros. En el juego de matryoshkas que es nuestro sofisticado entorno de backtesting ya contamos con los materiales para ensamblar una de las muñecas más gordas: la capacidad de trabajar con índices dinámicos desde nuestro almacén de datos.

Recapitulemos un poco: la manera incorrecta de hacer backtesting es seleccionar una serie de activos en los mercados (usando por ejemplo los populares  “filtros” de las plataformas de trading, no importa con qué complejidad) y proyectar hacia atrás. Si hace eso se expone a que el Sesgo de la Supervivencia le muerda el culo y la cartera, lea el artículo si no se lo cree. La manera de curarse en salud es usar índices para obtener su universo de inversión. En nuestras pruebas usaremos el IBEX35, pero obviamente busque un índice que se ajuste a sus necesidades si los más populares son demasiado restrictivos para el perfil de su estrategia.

7011__2

El problema, claro, es que la composición de los índices cambia con el tiempo: unas compañías salen, nuevas oportunidades entran, y los entornos de backtesting no suelen estar preparados para todo este ajetreo. En ese artículo expusimos un método para PyAlgoTrade que sólo implementamos parcialmente y que ahora vamos a terminar en el contexto de nuestra base de datos. Si usted instaló ibex35.sql.zip desde nuestra entrada anterior no debe preocuparse por nada pues todas las mejoras que proponemos ya están contenidas en ese archivo. Relájese y disfrute con los ejemplos.

De Reformas en el Almacén

Primero crearemos una tabla llamada grupo con los siguientes campos:

  • ID: un identificador secuencial para facilitar las búsquedas, único para cada dato.
  • Indice: a qué índice pertenece esta entrada. En nuestro caso sólo usaremos “IBEX35” pero esta estructura nos permite guardar diversos índices en el mismo almacén… y sin vernos forzados a duplicar los datos diarios de cada activo.
  • Activo: el identificador de la empresa.
  • Fecha: la fecha en la que se denota la presencia de este activo en este índice.

También nos interesa crear dos índices a fin de mantener la integridad de la base de datos:

  • Unico: este índice asegura que cada combinación de activo, índice y fecha es única, es decir, nos ayuda a evitar duplicados.
  • Sesion: ya que la mayoría de consultas que realicemos serán sobre series temporales, esté indice sobre las fechas nos permitirá trabajar con mucha más rapidez.

Expresado en la jerga de MySQL:

CREATE TABLE IF NOT EXISTS grupo ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, indice CHAR(15) NOT NULL, activo CHAR(15) NOT NULL, fecha DATE NOT NULL, PRIMARY KEY (id), UNIQUE INDEX unico (indice, activo, fecha), INDEX sesion (fecha) ) ENGINE = InnoDB DEFAULT CHARSET=utf8;

Esta no es la única manera en la que podemos hacer las cosas. Otra opción viable consistiría en tener un campo adicional que reflejara si se trata de una entrada o una salida, y así no estaríamos a contabilizar también las permanencias en cada fecha de cambios. Hacerlo de esta forma sin duda ahorraría espacio en el almacén, pero también nos obligaría a recorrer toda la serie desde el inicio para revelar la composición del índice en una fecha concreta, mientras que ahora si queremos conocer la composición del IBEX35 el día 3 de Octubre del 2015 sólo tenemos que escribir:

SELECT fecha, activo FROM grupo WHERE indice = 'IBEX35' AND fecha = (SELECT fecha FROM grupo WHERE indice = 'IBEX35' AND fecha <= '2015-10-03' ORDER BY fecha DESC LIMIT 1);

Obtendremos la lista completa de los constituyentes desde la última fecha de cambios, que fue el 20 de Julio del 2015. Nuestra arquitectura es óptima para índices como el IBEX, cuyas rotaciones se producen aproximadamente cada medio año.

Otra instrucción muy útil es la siguiente:

SELECT DISTINCT(activo), MIN(fecha), DATE_ADD(MAX(fecha), INTERVAL 15 DAY) FROM grupo WHERE indice = 'IBEX35' GROUP BY activo;

Con ello obtendremos una lista que contiene, para cada activo, la (primera) fecha en la que entró en el índice, así como la (última) fecha en la que salió de este mas 15 días.

Out of Sight…

¿Por qué 15 días? Porque no podemos anticipar en nuestro backtesting cuándo un activo abandona el índice. Es una cosa que sólo podemos saber ex-post, no ex-ante, de lo contrario cometeríamos uno de los mayores pecados en el minoritario culto arcano del backtesting: usar la bola de cristal, es decir, tomar decisiones basándonos en información que no estaba disponible en ese momento.

Por tanto, una vez que nuestro sistema nos notifica que el activo ha abandonado el índice, necesitamos todavía unas cuantas sesiones de mercado para actuar. En mi caso, la decisión será siempre desembarazarme del activo, porque las salidas de los índices suelen hacerse por la puerta de servicio y frecuentemente en camilla: están asociadas con fuertes caídas de valor. Pero su estrategia puede tomar otros derroteros y, por ejemplo, explorar una operación con cortos, en cuyo caso necesitará una serie histórica con más de 15 jornada bursátiles.

img_1695

Pinceladas en el Código

A continuación debemos conectar las mejoras en el almacén con el código del sistema para que se encuentren a disposición de nuestros experimentos. Sencillamente implementaremos en el objeto dataBase un método getMembers que podremos consultar cuando sea necesario con una fecha para obtener una matriz de constituyentes.

def getMembers(self, index, dateTime):
    sql = "select activo from miembro where fecha = (select max(fecha) from miembro where fecha <= %s and indice = %s) and indice = %s"
    args = [dateTime, index, index]

    cursor = self.__connection.cursor()
    cursor.execute(sql, args)

    ret = []
    for row in cursor:
        ret.append(row[0])
    cursor.close()
    return ret

Además aprovecharemos para incluir una función getDates para obtener toda la secuencia de fechas al principio en lugar de sacarlas una a una, así como reformatear nuestra rutina getBars para sacar todos los precios del día juntos, en lugar de uno a uno. Ambas mejoras incrementarán enormemente la velocidad de ejecución de nuestros backtests.

Siempre hemos de tener presente que optimizar demasiado pronto es un error, pero no optimizar nunca es un error todavía más gordo.

Ahora viene el toque artístico: DbFeed usará getMembers para obtener el listado de activos, pero no eliminará inmediatamente a los ausentes. ¿Por qué? Porque nos podemos encontrar aún en los 15 días de moratoria de los que hablamos arriba. Sólo eliminará al activo de su cómputo cuando, además de estar ausente, deje de reportar precios. Esta solución elegante por otra parte también protege al activo en el caso de que, por carencias en nuestro almacén, haya lagunas en los precios durante su presencia al índice.

def getNextMembers(self):
    for instrument in self.__indices:
        self.__members = self.__db.getMembers(instrument, self.__dateTime)
        for member in self.__members:
            if member not in self.__instruments:
                self.__instruments.append(member)

def getNextBars(self):
    self.getNextMembers()

    ret = self.__db.getBars(self.__instruments, self.getFrequency(), self.__dateTime)
    for i in range(len(self.__instruments) - 1, -1, -1):
        instrument = self.__instruments[i]
        if instrument not in ret:
            self.__instruments.pop(i)

    self.getNextDateTime()

    return bars.Bars(ret)

Todas estas mejoras se encuentran implementadas en esta versión de dbfeed.py. Descargue también este nuevo DbStrategy.py. Al ejecutarlo obtendremos un listado similar al de nuestro artículo anterior… pero en esta ocasión los activos irán cambiando con el tiempo, según entren y salgan del índice. Todo ello con una instrucción bien sencilla que nos permite trabajar no sólo con uno, sino con varios índices a la vez:

indices = ['IBEX35']
feed = dbfeed.DbFeed(config, fields, 10, startDate, endDate)
for index in indices:
 feed.registerIndex(index)

¡Ahora sí que estamos listos para el Show!

Gracias por leerme, y si les gustan estos artículos por favor no olviden compartirlos en sus redes sociales. Hasta muy pronto.

Anuncios

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