Estacionalidad en Python II - Backtesting

Estacionalidad en Python II - Backtesting

Table of contents

En la primera parte de esta serie, definimos que estamos buscando, que es la estacionalidad intradiaria, sus causas y sus consecuencias. En este nuevo articulo, vamos a construir la base de todo nuestro estudio, el backtester que verificara las hipotesis sobre las estrategias, generando las operaciones de compra y venta necesarias para poder validad si realmente existe una ventaja o no.

Este articulo pretende ser una introduccion al backtesting estacional, el modelo puede ser mejorado ampliamente, pero su funcionalidad basica es lo suficiente robusta, y facil de entender para poder ampliarlo a posterior con mayor funcionalidad.

Otras entregas de la serie

Estacionalidad en Python. Buscando Patrones Intradia
Tras un largo periodo totalmente desconectados, volvemos a escribir algunos apuntes para vosotros. En esta ocasion, vamos a programar desde cero, todo un proceso de research, y en lugar de ir a por un activo concreto, vamos a crear un metodo completo, que se reutilizable en el futuro, para poder
Estacionalidad en Python III - Estadisticas sobre la operativa
En las ultimas entregas, explicamos la estacionalidad, y preprocesamos los datos, ademas empezamos a programar un backtest estacional en python de una forma sencilla. Dejando todo listo para esta nueva entrega. En esta nueva entrega, una vez tenemos los trades de la operativa que nos devuelve el backtester en un

Plan de accion

En el primer articulo, preprocesamos los datos, para su posterior uso. El plan para este articulo es con esos datos preprocesados, poder backtestear de forma sencilla modelos estacionales. Sin stops, sin take profits, sin comisiones, unicamente una entrada repetida y sostenida a lo largo del historico cumpliendo requisitos fijos y posteriormente analizar sus resultados.

Para ello vamos a crear una clase, donde iremos expandiendo todos los metodos necesarios,vamos a programar el backtester utilizando mascaras de pandas, ya que no tiene sentido reinventar la rueda. Podria hacerse con librerias como backtrader o backtesting.py, pero realmente, a mi me es mas facil construirla de cero. Asi que vamos a ello

Creando la clase para backtesting de modelos estacionales intradiarios en python.

Dentro de la carpeta ModelBase, vamos a crear la clase dentro del archivo SModel.py. Ademas fuera de la carepta, vamos a crear un notebook llamado ppt.ipynb donde ejecutaremos el algoritmo, dejando una estructura tal que asi, ignorando el resto de archivos, de momento.

Clase IntradaySeasonalBacktester

Lo primero que haremos sera importar las librerias que utilizaremos. En este caso pandas y numpy. De momento no necesitamos mas librerias.

La forma que tenemos de definir la clase es mediante

class IntradaySeasonalBacktester:

El siguiente paso, es asignarle unos argumentos, que seran los parametetros de nuestra estrategia, para ello lo haremos dentro de la funcion __ init __ que seran:

  • df: mediante este parametro, le incluiremos el dataframe preprocesado anteriormente
  • entry_weekday : mediante este parametro le vamos a decir el dia de la semana que va a tener que buscar, siendo 0 el lunes y 6 el domingo.
  • entry_hour= mediante este parametro le vamos a decir a que hora va a entrar
  • entry_minute= mediante este parametro le vamos a decir a que minuto exacto va a entrar
  • bars_to_exit = mediante este parametro le vamos a decir cuantas velas va a estar dentro de la operacion, como hemos preprocesado en velas de 15 minutos, 4 velas equivale a una hora.

Quedando el __ init __ de la funcion de la siguiente forma

class IntradaySeasonalBacktester:
    
    def __init__(self, df, entry_weekday=None, entry_hour=None,
    entry_minute=None,bars_to_exit=1):
        self.df = df.copy()
        self.entry_weekday = entry_weekday
        self.entry_hour = entry_hour
        self.entry_minute = entry_minute
        self.bars_to_exit = bars_to_exit
        self.trades = []

En el código de IntradaySeasonalBacktester, self es simplemente una referencia a la instancia específica del objeto que estamos creando. Es un concepto fundamental en la programación orientada a objetos en Python.

self permite que cada instancia de nuestro backtester:

  • Mantenga sus propios datos (como el dataframe de precios)
  • Almacene sus propios parámetros (como horarios de entrada)
  • Registre sus propios resultados (en la lista self.trades)

Cuando escribimos self.df = df.copy() o self.entry_hour = entry_hour, estamos creando atributos únicos para esa instancia particular del backtester.

Sin self, sería imposible ejecutar múltiples pruebas con diferentes parámetros simultáneamente, ya que no habría forma de distinguir a qué instancia pertenece cada variable.

Buscando las entradas estacionales intradiarias

Una vez finalizada la funcion init, vamos a crear una funcion, que nos permita desde los argumentos introducidos, encontrar las entradas dentro del dataframe con los datos del activo. Para ello, vamos a utilizar las mascaras en pandas, por simplicidad del codigo y eficiencia, para que la funcion pueda ser llamada posteriormente desde el backtester.

Quedando la funcion como :

    def find_entries(self):
        entries = (self.df['weekday'] == self.entry_weekday) & \
                (self.df['hour'] == self.entry_hour) & \
                (self.df['minute'] == self.entry_minute)

De esta forma asignamos que las entradas son, cuando en nuestro dataframe el campo weekday coincide con el que le hemos solicitado en la funcion init, y lo mismo para la hora de entrada y la hora de salida.

Mediante esta funcion tenemos definidas las entradas, acelerando y simplificando en proceso de backtesting a unicamente a una funcion dentro de la clase.

Hasta el momento, todo lo codificado queda de la siguiente forma:

class IntradaySeasonalBacktester:
    def __init__(self, df, entry_weekday=None, 
    entry_hour=None, entry_minute=None, 
                 bars_to_exit=1):
        self.df = df.copy()
        self.entry_weekday = entry_weekday
        self.entry_hour = entry_hour
        self.entry_minute = entry_minute
        self.bars_to_exit = bars_to_exit
        self.trades = []
        
    def find_entries(self):
        entries = (self.df['weekday'] == self.entry_weekday) & \
                (self.df['hour'] == self.entry_hour) & \
                (self.df['minute'] == self.entry_minute)
        
        return entries

El resultado que obtenemos al hacer una llamada a la funcion find_entries y verificar su funcionamiento es

Como podemos observar nos responde con True o False segun si la fila cumple con los requisitos o no. Posteriormente con el uso de pandas, podremos filtrar entre los datos, a los que sean entradas para localizarlas de una forma agil, y poder procesar el backtest.

Funcion de Backtesting

Una vez tenemos claras las condiciones de entrada, el siguiente paso es crear una funcion dentro de la clase, que ejecute el backtest de una forma sencilla y agil. Y vamos a ello

Primeros pasos del backtesting

Los primeros pasos dentro de nuestra estructura de backtesting sera :

  • Encontrar los puntos de entrada
  • Trabajar con posiciones numericas resetando los indices datetime
  • Obtener el nombre de la primera columna para trabajar despues con ella a la hora de saltar velas, ya que venimos de un dataframe sin indice para agilizar el proceso
def run_backtest(self):
        # Encontrar los puntos de entrada
        entries = self.find_entries()
        entry_timestamps = self.df[entries].index
        
        # Reset index para trabajar con posiciones numéricas
        df_reset = self.df.reset_index()
        
        # Coje el nombre de la primera columna, para trabajar con fechas
        index_column_name = df_reset.columns[0]

Con este codigo, identificamos facilmente las entradas, y almacenamos los indices (que aun no han sido reseteados, quedando las fechas guardadas para su reutilizacion en el futuro, cuando se necesite encontrar un punto de entrada.

Creamos un dataframe con el indice reseteado, y obtenemos el nombre de la columna que antiguamente era el indice, por que es donde esta almacenada las cualidades temporales actuales, como el dia y la hora que se producen los registros.

Procesado de fechas

Una vez que tenemos indentificadas las fechas donde se produciran las entradas, vamos a procesarlas, realizando los siguientes pasos

  • Recorremos las entradas
  • Encontramos la posicion en el df sin indice
  • verificamos que podemos cerrar la operacion
  • guardamos los datos de la entrada
  • guardamos los datos de la salida
  • calculamos la variacion de la operacion
  • guardamos la operacion dentro del registro de operaciones
        for entry_timestamp in entry_timestamps:
            entry_position = df_reset[df_reset[index_column_name] == entry_timestamp].index[0]
            
            if entry_position + self.bars_to_exit >= len(df_reset):
                continue
                
            entry_row = df_reset.iloc[entry_position]
            entry_price = entry_row['Open'] # Entramos en Apertura
            
            exit_position = entry_position + self.bars_to_exit
            exit_row = df_reset.iloc[exit_position]
            exit_price = exit_row['Close']
            exit_timestamp = exit_row[index_column_name] 
            
            points = exit_price - entry_price
            
            self.trades.append({
                'EntryTime': entry_timestamp,
                'EntryPrice': entry_price,
                'OpenPriceEntry': entry_row['Open'],
                'ExitTime': exit_timestamp,
                'ExitPrice': exit_price,
                'ClosePriceExit': exit_row['Close'],
                'Bars': self.bars_to_exit,
                'Points': points
            })

mediante este sencillo y agil codigo, hemos escaneado todos los dias que hay entrada, verificamos si podremos cerrar la operacion mediante

            if entry_position + self.bars_to_exit >= len(df_reset):
                continue

Y empezamos a capturar los valores necesarios para poder generar las operaciones, incluso calculamos la variacion en puntos de la operacion. Al estar trabajando con futuros, utilizamos la contabilidad en puntos basicos

            entry_row = df_reset.iloc[entry_position]
            entry_price = entry_row['Open'] # Entramos en Apertura
            exit_position = entry_position + self.bars_to_exit
            exit_row = df_reset.iloc[exit_position]
            exit_price = exit_row['Close']
            exit_timestamp = exit_row[index_column_name] 
            points = exit_price - entry_price

TradeHistory

El dataframe que nos proporcionara informacion sobre el modelo que acabamos de simular, lo denominamos TradeHistory. El TradeHistory muestra todos los casos donde se ha producido una entrada y todo lo calculado, en un dataframe ordenado, que a posterior sera la materia prima del calculo de estadisticas relevantes para poder analizar la viabilidad de la estrategia.

Al ejecutar el backtest, nos devuelve directamente el TradeHistory, con los siguientes parametros

  • EntryTime : Hora y Dia a la que se efectua la entrada
  • EntryPrice : Precio Teorico de Entrada
  • OpenPriceEntry : Precio de la vela de entrada al Open (para simular slipagges en un futuro entre el OpenPriceEntry y el EntryPrice)
  • ExityTime Hora y Dia de Salida
  • ExitPrice: Precio Teorico de Salida
  • CloseExitPrice : Precio de Cierre de la vela de salida
  • Bars : Numero de velas dentro de la operacion
  • Points : Variacion en puntos entre entre exit_price y entry_price. Al ser futuros, este es el profit, para pasarlo a dolares, habria que multiplicarlo por el dolar per points de las especificaciones del contrato.

Como utilizarlo desde el notebook

El primer paso, respetando la estructura de archivos, vamos a cargar las librerias

Posteriormente vamos a cargar los datos preprocesados, en mi caso los datos del futuro ES

El siguiente paso, es lanzar una instancia de IntradaySeasonalBacktester sobre el dataframe data (que contiene datos del futuro ES) entrando el dia 1 (martes) a las 9:00 y saliendo tras dos barras, es decir a las 9:30

Una vez que lanzamos la instancia vemos que ha sido bien ejecutada

Y una vez con el dataframe cargado y los argumentos necesarios, podemos lanzar el backtest de la siguiente forma, devolviendo el TradeHistory

Conclusiones

En esta entrega, hemos creado el primer modelo de backtesting, donde ya nos deja testear hipotesis de forma sencilla de patrones intradiarios estacionales. Este modelo es la base de todo, que en las siguientes entregas iremos aumentando su funcionalidad y proposito. Pero como introduccion al backtesting estacional intradiario en python, es mas que suficiente.

Proximo Articulo

En el proximo articulo, procesaremos las operaciones para sacar las estasdisticas mas relevantes, pasaremos algun test de aletoriedad, y empezaremos a buscar pinceladas de alpha entre todos los datos. Y dejaremos todos los conceptos asentados para en una entrega posterior empezar con una optimizacion de amplio espectro, buscando en todo el campo operativo las zonas relevantes donde se producen los patrones estacionales, que pueden servir como punto de apoyo para un futuro research hacia un modelo totalmente funcional y operable.

Nos vemos en la proxima entrega, y cualquier duda jcx[a]quantarmy.com

Enlaces de Interes

https://www.cmegroup.com/education/courses/introduction-to-agriculture/grains-oilseeds/understanding-seasonality-in-grains.html

https://www.cmegroup.com/education/interactive/moore-report/pdf/AC-167_MooreCattleFinalwDemo.pdf

Jesús Cuesta

Odesa (Ucrania)
Inversor desde 2014. Research desde 2017. He trabjado en diferentes gestoras de capital, y Hedgefunds Crypto. Apasasionado en el codigo, los datos y las finanzas. Acualmente localizado en Ucrania.