Estacionalidad en Python III - Estadisticas sobre la operativa

Estacionalidad en Python III - Estadisticas sobre la operativa

Table of contents

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 dataframe, vamos a analizarlos. De esta forma podremos ver de un vistazo las estadisticas mas relevantes de la estrategia, ademas, pensando en la siguiente entrega donde programaremos un modulo de busqueda de amplio espectro de estacionalidad intradiaria, el cual el objetivo es que nos devuelva un dataframe con los parametros seleccionados, y las estadisticas mas relevantes.

Otros Articulos 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 II - Backtesting
Descubre cómo identificar patrones estacionales intradía en futuros y validar estrategias de trading mediante backtesting. Aprende a utilizar Python para analizar datos históricos, optimizar parámetros y evaluar el rendimiento de tus algoritmos antes de aplicarlos en el mercado real

Como lo vamos a estructurar

En la siguiente imagen, vemos como acabara nuestra clase. Tenemos ya programadas las funciones que estan dentro del rectangulo rojo, que son las funcionalidades principales para la busqueda de patrones estacionales intradiarios.

El objetivo ahora sera

  • Programar los estadisticos mas relevantes como:
    • SQN
    • VaR & CVaR
    • Alpha y Beta
    • MDD
    • Sharpe Ratio
  • Realizar un resumen (summary) donde nos muestra la informacion mas relevante, y que posteriormente utilizaremos para el dataframe resultante de la busqueda de amplio espectro de estacionalidad intradiaria.

Una vez defenidos los objetivos, y explicado que es lo que ya hemos hecho, vamos a ello!

SQN - System Quality Number en Python

Vamos a programar una nueva funcion dentro de nuestra clase, que nos calcule el SQN sobre la curva de nuestra estrategia.

El System Quality Number (SQN), desarrollado por el Dr. Van Tharp, es una métrica estadística diseñada para evaluar la calidad y rendimiento ajustado al riesgo de un sistema de trading.

Fórmula del SQN

Donde:

  • Esperanza Matemática (E): Es el promedio del retorno esperado por operación en términos de múltiplos de R (relación beneficio/riesgo).
  • Número de Operaciones (N): Total de operaciones realizadas.
  • Desviación Estándar (DT): Mide la dispersión o variabilidad de los múltiplos de R.

Interpretacion del Ratio SQN

Interpretación del SQN

El valor del SQN indica la calidad del sistema:

  • < 1.5: Difícil de operar.
  • 1.51 - 2.0: Sistema promedio.
  • 2.01 - 3.0: Buen sistema.
  • 3.01 - 5.0: Excelente sistema.
  • 5.01 - 7.0: Sistema sobresaliente.
  • > 7.0: "Santo Grial" (extremadamente raro)

Consideraciones Importantes

  • Tamaño muestral: Para que el SQN sea estadísticamente significativo, se recomienda un mínimo de 30 operaciones
  • Impacto de la desviación estándar: Sistemas con alta variabilidad en los resultados (como estrategias tendenciales) tienden a tener un SQN más bajo debido a su mayor desviación estándar, incluso si son rentables a largo plazo
  • Limitaciones: El SQN puede penalizar estrategias con grandes ganancias ocasionales o volatilidad positiva, lo que lo hace más favorable para sistemas con distribuciones consistentes y menores desviaciones

Vamos a crear una nueva funcion dentro de nuestra clase con el siguiente codigo :

    def calculate_sqn(self,trades=None):
        import numpy as np
      
        if len(trades) == 0:
            return 0
      
        avg_trade = trades['Points'].mean()
        std_dev = trades['Points'].std()
        num_trades = len(trades)
        
        if std_dev == 0:
            return 0
        
        sqn = (avg_trade / std_dev) * np.sqrt(num_trades)
        
        return sqn 

En el codigo lo que hemos realizado es

  • Verificar que hay Trades
  • Posteriormente calcular los componentes de la formula
  • Y calcular el ratio SQN en funcion de los componentes

VaR y CVaR

El VaR y el CVaR son dos estimadores de riesgo,

VaR (Value At Risk)

El VaR (Value at Risk) mide la pérdida máxima que una cartera podría experimentar en un período específico, bajo condiciones normales de mercado, con un nivel de confianza determinado. En términos simples, el VaR responde a la pregunta: ¿cuánto puedo perder como máximo en un horizonte temporal dado con una probabilidad específica?

Se podrtia interpretar como si el VaR diario al 95% de una cartera es $1,000,000, significa que hay un 5% de probabilidad de que las pérdidas superen ese monto en un día

Formula

Matemáticamente, el VaR se define como el percentil α de la distribución de pérdidas:

Metodos de Calculo del Value At Risk (VaR)

Existen tres métodos principales para calcular el VaR:

  • Simulación histórica: Utiliza datos pasados para estimar posibles pérdidas futuras
  • Método de varianza-covarianza: Asume que los retornos tienen una distribución normal
  • Simulación Monte Carlo: Genera escenarios aleatorios basados en modelos estadísticos.

Limitaciones

El VaR no considera la magnitud de las pérdidas más allá del umbral definido y puede subestimar el riesgo en eventos extremos (colas gruesas). Por consecuencia, debemos asumir, que el mayor drawdown, o perdida de rendimeinto esta por llegar.

CVaR (Conditional Value At Risk)

El CVaR (Conditional Value at Risk), también conocido como déficit esperado (Expected Shortfall), complementa al VaR al medir la pérdida promedio en los peores escenarios que exceden el umbral del VaR. Por lo tanto, proporciona una visión más completa del riesgo extremo.

Por ejemplo, si el CVaR al 95% es $1,200,000, significa que dentro del 5% de los peores escenarios, las pérdidas promedio serán $1,200,000

Formula

El CVaR se calcula como el promedio de las pérdidas que superan el VaR:

Ventajas

El CVaR tiene mejores propiedades matemáticas que el VaR:

  • Es una medida coherente del riesgo (cumple monotonicidad, subaditividad y otras propiedades)
  • Captura mejor los riesgos extremos y permite optimizar carteras considerando escenarios adversos

Programacion

Vamos a implementar el Value At Risk, y el Conditional Value at risk en una misma funcion de la siguiente forma:

    def calculate_var_cvar(self, confidence_level=0.95):

        if not self.trades:
            return 0, 0
      
        if isinstance(self.trades, list):
            trades_df = pd.DataFrame(self.trades)
        else:
            trades_df = self.trades
            
        r = trades_df['Points'].values
        
        var = np.percentile(r, (1-confidence_level)*100)
        cvar = r[r <= var].mean() if len(r[r <= var]) > 0 else var
        
        return var, cvar

Primero, verificamos que existan trades, posteriormente verificamos que los tenemos en un dataframe, y de no ser asi, lo creamos nosotros.

Asignamos a la variable r los valores de la variacion de nuestra estrategia

Y calculamos el VaR y el CVaR depositando sus resultados en las variables con el mismo nombre y devolviendolas.

El nivel de confianza por defecto es el 95, pero es comun tambien ver gente que lo calcula al nivel del 99%

Calculando las griegas (Alpha y Beta)

Alpha

El Alpha es una métrica que mide el rendimiento adicional o "exceso de retorno" que una inversión genera en comparación con un índice de referencia (benchmark). En otras palabras, refleja cuánto valor agregado (o perdido) aporta un gestor de cartera o estrategia de inversión más allá del comportamiento general del mercado.

  • Un Alpha positivo indica que la inversión superó al mercado.
  • Un Alpha negativo implica que la inversión tuvo un rendimiento inferior al mercado.

Formula

Interpretacion

  • α>0: La inversión generó rendimientos superiores al benchmark ajustados por riesgo.
  • α=0: La inversión igualó el rendimiento del benchmark.
  • α<0: La inversión tuvo un desempeño inferior al benchmark.

Beta

La Beta es una medida que cuantifica la sensibilidad o volatilidad de un activo en relación con los movimientos del mercado general. Representa el riesgo sistemático, es decir, el riesgo que no puede eliminarse mediante diversificación.

Ejemplos

  • Una Beta de 1 indica que el activo se mueve en línea con el mercado.
  • Una Beta mayor a 1 implica que el activo es más volátil que el mercado.
  • Una Beta menor a 1 sugiere que el activo es menos volátil que el mercado.

Calculo de la Beta

Interpretacion

  • β=1: El activo tiene la misma volatilidad que el mercado.
  • β>1: El activo es más volátil y arriesgado que el mercado.
  • β<1: El activo es menos volátil y, por lo tanto, más defensivo.
  • β<0: El activo se mueve en dirección opuesta al mercado.

Codigo en Python

Vamos a programar una funcion, que amplie nuestra clase y nos calcule los valores de la Beta y el Alpha para las estrategias.

    def calculate_greeks(self, benchmark_returns=None):
        import numpy as np
      
        if not self.trades:
            return 0, 0
            
        if isinstance(self.trades, list):
            trades_df = pd.DataFrame(self.trades)
        else:
            trades_df = self.trades
            
        strategy_returns = trades_df['Points'].values
        
        if benchmark_returns is None:
            if hasattr(self, 'df') and 'Close' in self.df.columns:

                benchmark_returns = self.df['Close'].diff().dropna().values
                
                if len(benchmark_returns) != len(strategy_returns):
                    benchmark_returns = benchmark_returns[:len(strategy_returns)]
                    
                    if len(benchmark_returns) != len(strategy_returns):
                        return 0, 0
        
        if benchmark_returns is None or len(benchmark_returns) != len(strategy_returns):
            return 0, 0
            
        if not isinstance(benchmark_returns, np.ndarray):
            benchmark_returns = np.array(benchmark_returns)
            
        covariance = np.cov(strategy_returns, benchmark_returns)[0, 1]
        variance = np.var(benchmark_returns)
        beta = covariance / variance if variance != 0 else 0
        
        avg_strategy = np.mean(strategy_returns)
        avg_benchmark = np.mean(benchmark_returns)
        alpha = avg_strategy - (beta * avg_benchmark)
        
        return beta, alpha

  • Si no hay operaciones registradas (self.trades está vacío), devuelve (0, 0) como Beta y Alpha.
  • Convierte las operaciones (self.trades) en un DataFrame si es una lista.
  • Extrae los retornos de la estrategia desde la columna 'Points'.
  • Si no se proporcionan retornos del benchmark (benchmark_returns), calcula los retornos basados en los precios de cierre ('Close') del activo original.
  • Alinea la longitud de los retornos del benchmark con los de la estrategia; si no coinciden, devuelve (0, 0).
  • Calcula la Beta
  • Si la varianza del benchmark es cero (sin movimientos en el mercado), se establece Beta en 0.
  • Calcula la Alpha
  • Devuelve los valores calculados de Beta y Alpha como una tupla (beta, alpha).

Max Drawdown (MDD)

El Max Drawdown corresponde a la perdida maxima historica que ha sufrido la estrategia en el pasado. Al estar trabajando con futuros, la calculamos como puntos maximos de perdida, en lugar de porcentajes, y nos sirve para tener una aproximacion de hasta donde llego en el pasado la estrategia hacia una perdida maxima.

Asi que extendemos nuestra clase con los calculos

    def calculate_mdd(self, trades=None):
        import numpy as np
        
        if trades is None:
            if not self.trades:
                return 0
            trades = pd.DataFrame(self.trades) if isinstance(self.trades, list) else self.trades
        
        if len(trades) == 0:
            return 0
            
        equity_curve = trades['Points'].cumsum()
        
        running_max = np.maximum.accumulate(equity_curve)
        
        drawdowns = (equity_curve - running_max)
        
        if np.max(running_max) == 0:
            return 0
            
        max_drawdown = np.min(drawdowns)
        
        return abs(max_drawdown)
  • Si no se proporcionan operaciones (trades), utiliza las operaciones registradas en la instancia (self.trades).
  • Convierte las operaciones en un DataFrame si son una lista.
  • Si no hay operaciones, devuelve 0 como el MDD.
  • Calcula la curva de equidad acumulando los resultados de las operaciones (Points) usando cumsum().
  • Encuentra los máximos acumulados en la curva de equidad utilizando np.maximum.accumulate.
  • Calcula los drawdowns como la diferencia entre la curva de equidad y los máximos acumulados.
  • Si el máximo acumulado es cero (por ejemplo, si no hay ganancias ni pérdidas), devuelve 0.
  • Busca el valor mínimo en los drawdowns (mayor pérdida acumulada).
  • Devuelve el valor absoluto del máximo drawdown como resultado (convención positiva).

Ratio de Sharpe

El Ratio de Sharpe, desarrollado por William F. Sharpe, es una métrica financiera que mide la rentabilidad ajustada al riesgo de una inversión. Su objetivo es determinar si los rendimientos de una inversión se deben a decisiones acertadas o simplemente a la asunción de un mayor riesgo. Es ampliamente utilizado para comparar inversiones y evaluar la calidad del rendimiento en relación con el riesgo asumido.

Formula

Donde:

  • Rp: Rentabilidad promedio de la inversión o cartera.
  • Rf: Rentabilidad del activo libre de riesgo (como bonos del Tesoro).
  • σp: Desviación estándar de los rendimientos de la inversión (volatilidad).

Ventajas y Limitaciones

Ventajas
  • Permite comparar inversiones con diferentes niveles de riesgo.
  • Ajusta los rendimientos en función del riesgo, facilitando análisis más objetivos.
  • Es útil para evaluar gestores de carteras o estrategias de trading.

Limitaciones

  • Asume que los rendimientos tienen una distribución normal, lo cual no siempre es cierto.
  • No distingue entre volatilidad positiva (ganancias) y negativa (pérdidas), lo que puede penalizar estrategias con alta variabilidad positiva.
    def calculate_sharpe_ratio(self, trades=None, risk_free_rate=0, periods_per_year=252):
        if trades is None:
            if not self.trades:
                return 0
            trades = pd.DataFrame(self.trades) if isinstance(self.trades, list) else self.trades
        
        if len(trades) == 0:
            return 0
            
        returns_mean = trades['Points'].mean()
        returns_std = trades['Points'].std()
        
        if returns_std == 0:
            return 0
            
        sharpe = (returns_mean - risk_free_rate) / returns_std
        annualized_sharpe = sharpe * np.sqrt(periods_per_year)
        return annualized_sharpe

Summary - Crear estadisticas sobre el TradeHistory

Una vez hemos programado todas las funciones de las estadisticas, vamos a programar una funcion que nos haga un 'summary' sobre nuestro tradehistory. Informacion extremadamente util, para cuando lancemos nuestra optimizacion de amplio espectro, poder localizar las zonas horarias con algun tipo de edge, o incluso crear nuestros ratios derivados de los ratios calculados actualmente.

    
    def get_summary(self, benchmark_returns=None, confidence_level=0.95,):
        
        if not self.trades:
            return {"message": "No trades found"}
        
        trades_df = pd.DataFrame(self.trades)
        
        winning_trades = trades_df[trades_df['Points'] > 0]
        losing_trades = trades_df[trades_df['Points'] < 0]
        
        # Calcular SQN
        sqn_value = self.calculate_sqn(trades_df)
        
        # Calcular VaR y CVaR
        var, cvar = self.calculate_var_cvar(confidence_level)
        
        # Calcular alpha y beta si hay benchmark
        beta, alpha = self.calculate_greeks(benchmark_returns)

        # calculate MDD
        mdd = self.calculate_mdd(trades_df)
        
        # Calcular Sharpe Ratio
        sharpe_ratio = self.calculate_sharpe_ratio(trades_df)
        # Calcular el Summary
        summary = {
            'total_trades': len(trades_df),
            'mdd': mdd,
            'winning_trades': len(winning_trades),
            'losing_trades': len(losing_trades),
            'win_rate': len(winning_trades) / len(trades_df) if len(trades_df) > 0 else 0,
            'avg_profit': trades_df['Points'].mean(),
            'total_profit': trades_df['Points'].sum(),
            'max_profit': trades_df['Points'].max(),
            'max_loss': trades_df['Points'].min(),
            'variance': np.var(trades_df['Points']),
            'profit_factor': abs(winning_trades['Points'].sum() / losing_trades['Points'].sum()) 
                           if len(losing_trades) > 0 and losing_trades['Points'].sum() != 0 else float('inf'),
            'sharpe': sharpe_ratio,
            'sqn': sqn_value,
            'VaR': var,
            'CVaR': cvar,
            'beta': beta,
            'alpha': alpha
        }
        
        return summary

Este código define un método llamado get_summary que genera un resumen estadístico detallado de las operaciones realizadas por una estrategia de trading. Combina varias métricas clave para evaluar el rendimiento, el riesgo y la calidad de la estrategia.

  • Si no hay operaciones (self.trades está vacío), devuelve un mensaje indicando que no se encontraron operaciones.
  • Convierte las operaciones (self.trades) en un DataFrame para facilitar los cálculos.
  • Separa las operaciones ganadoras (winning_trades) y perdedoras (losing_trades) basándose en la columna 'Points'.
  • SQN: Llama al método calculate_sqn para calcular el System Quality Number.
  • VaR y CVaR: Llama al método calculate_var_cvar para calcular el Value at Risk y el Conditional Value at Risk con un nivel de confianza especificado.
  • Alpha y Beta: Llama al método calculate_greeks si se proporcionan retornos del benchmark.
  • MDD: Llama al método calculate_mdd para calcular el Maximum Drawdown.
  • Sharpe Ratio: Llama al método calculate_sharpe_ratio para medir la rentabilidad ajustada al riesgo.
  • Número total de operaciones, ganadoras y perdedoras.
  • Tasa de éxito (win rate): Porcentaje de operaciones ganadoras.
  • Promedio, total, máximo beneficio y máxima pérdida.
  • Varianza de los resultados.
  • Profit Factor: Relación entre las ganancias totales y las pérdidas totales (si hay pérdidas).
  • Compila todas las métricas calculadas en un diccionario llamado summary.
  • Devuelve el diccionario con todas las estadísticas clave.

Todas las metricas incluidas en el resumen

Métrica Descripción
total_trades Número total de operaciones realizadas.
mdd Maximum Drawdown: Mayor pérdida acumulada desde un pico hasta un valle en la curva de equidad.
winning_trades Número de operaciones ganadoras.
losing_trades Número de operaciones perdedoras.
win_rate Porcentaje de operaciones ganadoras sobre el total.
avg_profit Beneficio promedio por operación.
total_profit Beneficio total acumulado.
max_profit Mayor beneficio obtenido en una operación.
max_loss Mayor pérdida sufrida en una operación.
variance Varianza de los resultados (medida de dispersión).
profit_factor Relación entre ganancias totales y pérdidas totales (indicador de eficiencia).
sharpe Ratio de Sharpe: Rentabilidad ajustada al riesgo.
sqn System Quality Number: Calidad del sistema basado en riesgo y rendimiento.
VaR Value at Risk: Pérdida máxima esperada con un nivel de confianza dado.
CVaR Conditional Value at Risk: Pérdida promedio más allá del VaR (riesgo extremo).
beta Sensibilidad del sistema frente a movimientos del mercado (benchmark).
alpha Rendimiento adicional generado por la estrategia ajustado por riesgo sistemático (benchmark).

Implementacion de lo programado hasta el momento

El primer paso es cargar las librerias, en este caso pandas para cargar el picke de los datos, y nuestro IntradaySeasonalBacktester desde el notebook

Posteriormente, cargaremos a un dataframe nuestro pickle con los datos, en este caso es el futuro del ES preprocesado como explicamos en articulo 1, pero puede ser de cualquier otro activo.

Creamos una instrancia de nuestro IntradaySeasonalBacktester llamada model, le pasamos como argumento el dataframe de nuestros datos preprocesados, y le he metido unos parametros random de dia de la semana de entrada, hora de entrada, minuto de entrada y velas dentro.

Posteriormente ejecutamos el backtest con el metodo run_backtest() para generar el TradeHistory de la estrategia.

Y ahora vamos a solicitar un summary sobre los trades de la estrategia que acabamos de hacer funcionar, mediante la llamada a la funcion get_summary(), que llamara a todos los estadisticos avanzados que hemos calculado, ademas de otros mas sencillos que calculamos directamente con pandas y numpy. Obteniendo los datos necesarios para una evaluacion de la estrategia.

Enlaces de Interes

Sharpe Ratio: Definition, Formula, and Examples
The Sharpe ratio is used to help investors understand the return of an investment compared to its risk.
Understanding Value at Risk (VaR) and How It’s Computed
Value at risk (VaR) is a statistic that quantifies the level of financial risk within a firm, portfolio, or position over a specific time frame.
Conditional Value at Risk (CVar): Definition, Uses, Formula
Conditional Value at Risk (CVaR) quantifies the potential extreme losses in the tail of a distribution of possible returns.
The SQN ratio (System Quality Number)
Introduced by Van Tharp in 2008 in his book The Definitive Guide to Position Sizing, the System Quality Number, abbreviated as SQN, is an indicator designed to evaluate or measure the performance of a…

Proximamente

En el proximo articulo, vamos a crear un analizador de amplio espectro, que investige todas las franjas horarias del activo, en busca de patrones estacionales intradiarios. Utilizando todos los metodos programados anteriormente y que nos devuelva un dataframe con los parametros utilizados y las estadisticas de dicha combinacion, para posteriormente ejecutar un analisis.

Ante cualquier duda, no dude en contactarme por email a jcx[a]quantarmy.com

Un saludo, Jesus.

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.