Lección 2: Backtesting y Optimización
Introducción
El backtesting y la optimización son procesos fundamentales en el desarrollo de estrategias de trading algorítmico. El backtesting permite a los traders evaluar el rendimiento de una estrategia utilizando datos históricos, mientras que la optimización ayuda a ajustar los parámetros de la estrategia para maximizar su rendimiento. En esta lección, exploraremos los conceptos y técnicas esenciales de backtesting y optimización, proporcionando ejemplos detallados y aplicaciones prácticas para ayudar a comprender y aplicar estos métodos en el desarrollo de estrategias de trading.
Backtesting: Evaluación de Estrategias con Datos Históricos
El backtesting es el proceso de evaluar una estrategia de trading utilizando datos históricos para simular cómo habría funcionado en el pasado. Este proceso es crucial para validar la efectividad de una estrategia antes de implementarla en el mercado real.
1. Conceptos Básicos de Backtesting
- Datos Históricos: El backtesting requiere datos históricos precisos y de alta calidad para simular el rendimiento de la estrategia. Estos datos pueden incluir precios de apertura, cierre, máximo, mínimo y volumen.
- Señales de Trading: Las señales generadas por la estrategia, como las señales de compra y venta, se aplican a los datos históricos para evaluar el rendimiento.
- Indicadores de Rendimiento: Los indicadores clave de rendimiento (KPIs) como el retorno total, el drawdown máximo, el ratio de Sharpe y el ratio de Sortino se utilizan para evaluar la eficacia de la estrategia.
2. Proceso de Backtesting
El proceso de backtesting generalmente incluye los siguientes pasos:
a) Recopilación de Datos
Obtener datos históricos precisos es crucial para el backtesting. Los datos deben ser representativos del mercado y del período en el que se desea evaluar la estrategia.
import yfinance as yf
# Descargar datos históricos de precios
data = yf.download('AAPL', start='2020-01-01', end='2021-01-01')
b) Implementación de la Estrategia
Desarrollar la lógica de la estrategia utilizando indicadores técnicos y reglas de trading. A continuación se presenta un ejemplo de una estrategia de cruce de medias móviles.
import pandas as pd
import numpy as np
# Calcular medias móviles
data['SMA_50'] = data['Close'].rolling(window=50).mean()
data['SMA_200'] = data['Close'].rolling(window=200).mean()
# Señales de compra y venta
data['Signal'] = 0
data['Signal'][50:] = np.where(data['SMA_50'][50:] > data['SMA_200'][50:], 1, 0)
data['Position'] = data['Signal'].diff()
c) Simulación de Trading
Aplicar las señales de trading a los datos históricos para simular las operaciones y calcular el rendimiento de la estrategia.
# Calcular retornos diarios
data['Return'] = data['Close'].pct_change()
# Calcular retornos de la estrategia
data['Strategy_Return'] = data['Return'] * data['Position'].shift(1)
# Calcular rendimiento acumulado
data['Cumulative_Return'] = (1 + data['Strategy_Return']).cumprod()
# Evaluar rendimiento de la estrategia
cumulative_return = data['Cumulative_Return'][-1]
d) Evaluación del Rendimiento
Utilizar indicadores de rendimiento para evaluar la eficacia de la estrategia.
# Calcular drawdown máximo
data['Cumulative_Max'] = data['Cumulative_Return'].cummax()
data['Drawdown'] = data['Cumulative_Max'] - data['Cumulative_Return']
max_drawdown = data['Drawdown'].max()
# Calcular ratio de Sharpe
sharpe_ratio = (data['Strategy_Return'].mean() / data['Strategy_Return'].std()) * np.sqrt(252)
print(f'Retorno Acumulado: {cumulative_return:.2f}')
print(f'Max Drawdown: {max_drawdown:.2f}')
print(f'Sharpe Ratio: {sharpe_ratio:.2f}')
3. Ejemplo Práctico de Backtesting
Supongamos que queremos evaluar una estrategia de cruce de medias móviles para la acción de Apple (AAPL) utilizando datos históricos del año 2020. La estrategia compra cuando la media móvil de 50 días cruza por encima de la media móvil de 200 días y vende cuando cruza por debajo.
Día | Precio de Cierre ($) | SMA 50 | SMA 200 | Señal | Posición | Retorno Diario | Retorno Estrategia | Retorno Acumulado |
---|---|---|---|---|---|---|---|---|
01/01 | 75.09 | 75.09 | 75.09 | 0 | 0 | 0 | 0 | 1.00 |
01/02 | 74.36 | 74.73 | 75.09 | 0 | 0 | -0.0097 | 0 | 1.00 |
01/03 | 74.41 | 74.62 | 75.09 | 0 | 0 | 0.0007 | 0 | 1.00 |
… | … | … | … | … | … | … | … | … |
12/30 | 132.69 | 121.22 | 100.34 | 1 | 1 | 0.0038 | 0.0038 | 1.67 |
12/31 | 133.72 | 121.48 | 100.46 | 1 | 1 | 0.0077 | 0.0077 | 1.68 |
En este ejemplo, la estrategia habría generado un retorno acumulado del 68% durante el año 2020, con un drawdown máximo del 15% y un ratio de Sharpe de 1.5.
Optimización de Estrategias de Trading
La optimización de estrategias de trading implica ajustar los parámetros de la estrategia para maximizar su rendimiento. Esto se puede lograr mediante la búsqueda sistemática de combinaciones de parámetros que proporcionen los mejores resultados.
1. Conceptos Básicos de Optimización
- Parámetros de la Estrategia: Los parámetros de la estrategia, como los períodos de las medias móviles, deben ser optimizados para maximizar el rendimiento.
- Espacio de Búsqueda: El espacio de búsqueda define el rango de valores posibles para cada parámetro.
- Criterio de Optimización: El criterio de optimización define la métrica que se utilizará para evaluar el rendimiento de la estrategia, como el retorno total, el ratio de Sharpe o el drawdown.
2. Métodos de Optimización
Existen varios métodos de optimización, incluyendo la búsqueda exhaustiva, la búsqueda aleatoria y la optimización bayesiana.
a) Búsqueda Exhaustiva
La búsqueda exhaustiva implica evaluar todas las combinaciones posibles de parámetros dentro del espacio de búsqueda. Este método es simple pero puede ser computacionalmente intensivo.
Ejemplo Práctico:
Supongamos que queremos optimizar los períodos de las medias móviles de una estrategia de cruce de medias móviles para encontrar la combinación que maximiza el ratio de Sharpe.
from itertools import product
# Definir espacio de búsqueda
short_window = range(10, 50, 10)
long_window = range(100, 200, 20)
# Inicializar variables para almacenar los mejores resultados
best_sharpe_ratio = -np.inf
best_params = None
# Búsqueda exhaustiva
for short, long in product(short_window, long_window):
data['SMA_Short'] = data['Close'].rolling(window=short).mean()
data['SMA_Long'] = data['Close'].rolling(window=long).mean()
data['Signal'] = 0
data['Signal'][short:] = np.where(data['SMA_Short'][short:] > data['SMA_Long'][short:], 1, 0)
data['Position'] = data['Signal'].diff()
data['Strategy_Return'] = data['Return'] * data['Position'].shift(1)
sharpe_ratio = (data['Strategy_Return'].mean() / data['Strategy_Return'].std()) * np.sqrt(252)
if sharpe_ratio > best_sharpe_ratio:
best_sharpe_ratio = sharpe_ratio
best_params = (short, long)
print(f'Mejores Parámetros: {best_params}')
print(f'Mejor Ratio de Sharpe: {best_sharpe_ratio:.2f}')
b) Búsqueda Aleatoria
La búsqueda aleatoria implica evaluar un subconjunto aleatorio de combinaciones de parámetros. Este método puede ser más eficiente que la búsqueda exhaustiva pero puede no encontrar la mejor combinación de parámetros.
Ejemplo Práctico:
Utilizamos la búsqueda aleatoria para optimizar los períodos de las medias móviles.
import random
# Definir espacio de búsqueda
short_window = range(10, 50, 10)
long_window = range(100, 200, 20)
# Inicializar variables para almacenar los mejores resultados
best_sharpe_ratio = -np.inf
best_params = None
# Búsqueda aleatoria
for _ in range(100): # Evaluar 100 combinaciones aleatorias
short = random.choice(short_window)
long = random.choice(long_window)
data['SMA_Short'] = data['Close'].rolling(window=short).mean()
data['SMA_Long'] = data['Close'].rolling(window=long).mean()
data['Signal'] = 0
data['Signal'][short:] = np.where(data['SMA_Short'][short:] > data['SMA_Long'][short:], 1, 0)
data['Position'] = data['Signal'].diff()
data['Strategy_Return'] = data['Return'] * data['Position'].shift(1)
sharpe_ratio = (data['Strategy_Return'].mean() / data['Strategy_Return'].std()) * np.sqrt(252)
if sharpe_ratio > best_sharpe_ratio:
best_sharpe_ratio = sharpe_ratio
best_params = (short, long)
print(f'Mejores Parámetros: {best_params}')
print(f'Mejor Ratio de Sharpe: {best_sharpe_ratio:.2f}')
c) Optimización Bayesiana
La optimización bayesiana utiliza modelos probabilísticos para dirigir la búsqueda hacia las combinaciones de parámetros más prometedoras. Este método es eficiente y puede encontrar buenas combinaciones de parámetros con menos evaluaciones.
Ejemplo Práctico:
Utilizamos la biblioteca hyperopt
para realizar la optimización bayesiana.
from hyperopt import fmin, tpe, hp, Trials
# Definir función objetivo
def objective(params):
short, long = params
data['SMA_Short'] = data['Close'].rolling(window=int(short)).mean()
data['SMA_Long'] = data['Close'].rolling(window=int(long)).mean()
data['Signal'] = 0
data['Signal'][int(short):] = np.where(data['SMA_Short'][int(short):] > data['SMA_Long'][int(short):], 1, 0)
data['Position'] = data['Signal'].diff()
data['Strategy_Return'] = data['Return'] * data['Position'].shift(1)
sharpe_ratio = (data['Strategy_Return'].mean() / data['Strategy_Return'].std()) * np.sqrt(252)
return -sharpe_ratio # Minimizar la función objetivo
# Definir espacio de búsqueda
space = [hp.uniform('short', 10, 50), hp.uniform('long', 100, 200)]
# Realizar optimización bayesiana
trials = Trials()
best = fmin(objective, space, algo=tpe.suggest, max_evals=100, trials=trials)
print(f'Mejores Parámetros: {best}')
Validación Cruzada y Overfitting
La validación cruzada es una técnica utilizada para evaluar el rendimiento de una estrategia y garantizar que no se esté ajustando demasiado a los datos históricos (overfitting). El overfitting ocurre cuando una estrategia está demasiado ajustada a las peculiaridades de los datos históricos y no generaliza bien en datos futuros.
1. Validación Cruzada
La validación cruzada implica dividir los datos históricos en varios conjuntos de entrenamiento y prueba, y evaluar el rendimiento de la estrategia en cada conjunto. Esto proporciona una estimación más robusta del rendimiento de la estrategia.
Ejemplo Práctico:
Utilizamos la validación cruzada para evaluar una estrategia de cruce de medias móviles.
from sklearn.model_selection import TimeSeriesSplit
# Definir validación cruzada con 5 divisiones
tscv = TimeSeriesSplit(n_splits=5)
# Inicializar variables para almacenar los resultados
results = []
# Validación cruzada
for train_index, test_index in tscv.split(data):
train, test = data.iloc[train_index], data.iloc[test_index]
train['SMA_Short'] = train['Close'].rolling(window=20).mean()
train['SMA_Long'] = train['Close'].rolling(window=50).mean()
train['Signal'] = 0
train['Signal'][20:] = np.where(train['SMA_Short'][20:] > train['SMA_Long'][20:], 1, 0)
train['Position'] = train['Signal'].diff()
train['Strategy_Return'] = train['Return'] * train['Position'].shift(1)
sharpe_ratio = (train['Strategy_Return'].mean() / train['Strategy_Return'].std()) * np.sqrt(252)
results.append(sharpe_ratio)
print(f'Ratio de Sharpe Promedio: {np.mean(results):.2f}')
2. Overfitting
El overfitting se puede mitigar mediante la simplificación de la estrategia, el uso de regularización y la validación cruzada. Una estrategia más simple y generalizada tiende a tener un mejor rendimiento en datos futuros.
Conclusión
El backtesting y la optimización son procesos esenciales en el desarrollo de estrategias de trading algorítmico. El backtesting permite evaluar el rendimiento de una estrategia utilizando datos históricos, mientras que la optimización ayuda a ajustar los parámetros de la estrategia para maximizar su rendimiento. La validación cruzada y la mitigación del overfitting son cruciales para garantizar que la estrategia funcione bien en datos futuros y no solo en los datos históricos. Con una comprensión profunda de estos conceptos y técnicas, estarás bien preparado para desarrollar, evaluar y optimizar estrategias de trading algorítmico de manera efectiva y robusta.
Lecturas Recomendadas:
- «Advances in Financial Machine Learning» por Marcos López de Prado.
- «Algorithmic Trading: Winning Strategies and Their Rationale» por Ernest P. Chan.
- «Python for Data Analysis» por Wes McKinney.
Ejercicio Práctico:
- Backtesting de una Estrategia de Trading:
- Recopilar datos históricos de precios para una acción.
- Desarrollar una estrategia de cruce de medias móviles en Python.
- Realizar backtesting de la estrategia utilizando datos históricos.
- Evaluar el rendimiento de la estrategia utilizando indicadores de rendimiento como el retorno acumulado, el drawdown máximo y el ratio de Sharpe.
- Optimización de Parámetros:
- Definir el espacio de búsqueda para los parámetros de la estrategia.
- Utilizar métodos de optimización como la búsqueda exhaustiva, la búsqueda aleatoria y la optimización bayesiana para encontrar la mejor combinación de parámetros.
- Evaluar el rendimiento de la estrategia optimizada.
- Validación Cruzada:
- Implementar la validación cruzada para evaluar la robustez de la estrategia.
- Comparar los resultados de la validación cruzada con los resultados del backtesting tradicional.
- Ajustar la estrategia para mitigar el overfitting y mejorar el rendimiento en datos futuros.
Esta lección te proporciona una comprensión integral del backtesting y la optimización en el desarrollo de estrategias de trading algorítmico.