¿Cómo Implementar el Backtesting Walk Forward en Python?

0
37
mapa calor python

Aprende los fundamentos e implementación de un backtesting realista en Python.

Los backtests frecuentemente lucen mucho mejor en papel que en los mercados reales. La razón habitual no es que la idea estuviera completamente equivocada. Es que la estrategia fue ajustada demasiado estrechamente al pasado y luego evaluada con los mismos datos con los que fue entrenada.

Ese es exactamente el problema que abordaremos en esta guía.

Vamos a construir un programa completo de backtesting walk-forward en Python utilizando datos de AAPL desde 2010 en adelante. El artículo cubrirá la obtención de datos mediante las APIs de FMP, una estrategia simple de cruce de medias móviles (SMA), la optimización de parámetros y un framework walk-forward completo que reentrena repetidamente la estrategia en ventanas históricas móviles y la prueba con datos no vistos anteriormente.

El objetivo aquí no es simplemente ejecutar un backtest. Es entender cómo funciona la optimización walk-forward, por qué la utilizan los traders y cómo construir un programa que evalúe una estrategia con datos sobre los que aún no ha sido ajustada.

¿Qué es exactamente la Optimización Walk-Forward?

Por qué los backtests convencionales pueden ser engañosos

Un flujo de trabajo de optimización convencional suele verse así: tomar un largo período de datos históricos, probar muchas combinaciones de parámetros, elegir la mejor, y luego evaluar la estrategia según qué tan bien funcionó en ese mismo período.

El problema es obvio una vez que te detienes a pensarlo. Si a la estrategia se le permite explorar el pasado hasta encontrar la combinación que mejor funcionó en él, entonces el resultado final ya no es una prueba limpia. Es en parte un reflejo de qué tan bien la estrategia se adaptó a esa muestra específica.

Eso no significa necesariamente que la estrategia sea inútil. Pero sí significa que el resultado puede verse mucho mejor de lo que obtendrías en una operativa real.

¿Qué cambios trae la optimización Walk Forward?

La Optimización Walk-Forward cambia la pregunta.

En lugar de preguntar, “¿Cuál es el mejor conjunto de parámetros para todo este conjunto de datos?”, pregunta: “Si estuviera operando realmente a lo largo del tiempo, y reajustando periódicamente la estrategia a medida que llegaban nuevos datos, ¿Cómo se habría comportado?”

Esa es una prueba mucho más exigente.

En lugar de encontrar un conjunto fijo de parámetros y aplicarlo en todo momento, la optimización Walk Forward sigue avanzando a lo largo de la línea de tiempo. En cada etapa, utiliza la información disponible hasta ese punto, selecciona los mejores parámetros en ese contexto, y luego verifica cómo se comportan esos parámetros en el siguiente segmento no visto de datos de mercado.

Este proceso repetido hace que el backtest se asemeje mucho más a cómo se gestionaría una estrategia en el mundo real.

Por qué esto es más realista

Los mercados no permanecen quietos. Una combinación de parámetros que funciona bien en un período puede dejar de funcionar cuando cambia la volatilidad, se debilita la fuerza de la tendencia, o el mercado entra en un régimen diferente.

Un backtest estático oculta ese problema porque comprime todo en un gran ejercicio histórico. La WFO obliga a la estrategia a seguir demostrando su valor a lo largo del tiempo.
Por eso la WFO suele producir resultados más débiles que un backtest completamente optimizado. Pero más débil no significa peor. En muchos casos, simplemente significa más honesto.

Una estrategia que ofrece resultados modestos a lo largo de muchos períodos no vistos suele ser más creíble que una que parece increíble en una única ejecución histórica completamente optimizada.

Qué intenta mostrar este artículo

El objetivo de esta guía no es presentar la WFO como una solución mágica. No convertirá de repente una estrategia débil en una robusta.

Lo que sí ofrece es un marco de evaluación más realista. Nos ayuda a separar las estrategias que solo parecían buenas en retrospectiva de aquellas que pueden mantenerse mejor cuando el mercado avanza y las condiciones cambian.

Para cuando terminemos la implementación, esa diferencia será mucho más fácil de apreciar en la práctica.

El flujo de trabajo que construiremos

  • Obtener datos históricos de precios de AAPL usando la API Histórica EOD de FMP
  • Construir una estrategia simple de cruce de medias móviles (SMA) con ventanas cortas y largas parametrizadas
  • Calcular métricas clave de rendimiento como el retorno total, el ratio de Sharpe y el drawdown máximo
  • Ejecutar una optimización estática sobre múltiples combinaciones de SMA
  • Probar esos parámetros “óptimos” en un período posterior no visto, para demostrar por qué la optimización estática puede fallar
  • Implementar un bucle de optimización walk-forward que reevalúe continuamente la estrategia a lo largo del tiempo
  • Combinar los resultados fuera de muestra en una única serie de rendimiento
  • Comparar diferentes configuraciones walk-forward variando la profundidad de entrenamiento y la frecuencia de re-optimización

Extracción de Datos

Primero, debemos importar los módulos necesarios y obtener los precios. Usaremos la API de Gráfico Histórico Completo de Índices de FMP para obtener los precios de las acciones de Apple desde 2010. Esto nos dará suficiente margen para explicar el backtesting walk-forward.

import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import requests
from itertools import product
import matplotlib.pyplot as plt

token = 'TU TOKEN FMP'

def get_prices(symbol: str, from_date: str) -> pd.DataFrame:
    url = f"https://financialmodelingprep.com/stable/historical-price-eod/full"
    params = {"apikey":token, "symbol":symbol, "from":from_date}
    resp = requests.get(url, params=params)
    df = pd.DataFrame(resp.json())
    df['date'] = pd.to_datetime(df['date'])
    df.sort_values(by='date', inplace=True)
    df.set_index('date', inplace=True)
    df = df[['open', 'low','high','close']]
    
    return df

prices = get_prices("AAPL", "2010-01-01")
prices.to_csv("AAPL_prices.csv")
prices = pd.read_csv("AAPL_prices.csv", index_col='date')
prices

Nota: Reemplaza con tu clave de API de FMP real. Si no tienes una, puedes obtenerla abriendo una cuenta de desarrollador en FMP.

Datos de precios de Apple

Construcción de la estrategia en Python

Implementaremos una estrategia sencilla utilizando una SMA corta y una SMA larga. Cuando la SMA corta está por encima de la SMA larga, indica un impulso alcista, y cuando está por debajo, sugiere una tendencia bajista. Este enfoque se basa en solo dos parámetros: los períodos para las SMA corta y larga.

Notarás que buscaremos tener todos los parámetros definidos dentro de funciones, de modo que más adelante podamos ejecutar nuestro backtesting walk-forward.

def run_strategy(df_prices: pd.DataFrame, short_window: int, long_window: int) -> pd.DataFrame:
    df = df_prices.copy()
    
    df['short_sma'] = df['close'].rolling(window=short_window).mean()
    df['long_sma'] = df['close'].rolling(window=long_window).mean()
    
    df['signal'] = np.where(df['short_sma'] > df['long_sma'], 1, -1)
    
    df['position'] = df['signal'].shift(1)
    
    df['market_returns'] = df['close'].pct_change()
    df['strategy_returns'] = df['position'] * df['market_returns']
    
    df = df.dropna()
    
    return df

prices = get_prices("AAPL", "2010-01-01")
returns_df = run_strategy(prices, short_window=50, long_window=200)

Otra función útil es la que calcula las métricas que utilizaremos para determinar los mejores parámetros en general. Estas métricas son el retorno, el ratio de Sharpe y el drawdown.

def calculate_metrics(df_returns: pd.DataFrame, returns_col: str = 'strategy_returns') -> dict:
    returns = df_returns[returns_col].dropna()
    
    if len(returns) == 0:
        return {'total_return': 0.0, 'sharpe_ratio': 0.0, 'max_drawdown': 0.0}
    
    total_return = (1 + returns).prod() - 1
    
    mean_return = returns.mean()
    std_return = returns.std()
    sharpe_ratio = mean_return / std_return * np.sqrt(252) if std_return > 0 else 0.0
    
    cumulative = (1 + returns).cumprod()
    running_max = cumulative.expanding().max()
    drawdown = (cumulative - running_max) / running_max
    max_drawdown = drawdown.min()
    
    return {
        'total_return': total_return,
        'sharpe_ratio': sharpe_ratio,
        'max_drawdown': max_drawdown
    }

metrics = calculate_metrics(returns_df)
print("Strategy Metrics:")
for key, value in metrics.items():
    print(f"  {key}: {value:.3f}" if isinstance(value, float) else f"  {key}: {value}")

bh_returns = returns_df['close'].pct_change().dropna()
bh_metrics = calculate_metrics(pd.DataFrame({'strategy_returns': bh_returns}))
print("\nBuy & Hold Metrics:")
for key, value in bh_metrics.items():
    print(f"  {key}: {value:.3f}")

Notarás que, solo por esta vez, presentamos la estrategia de Buy and Hold. Esto ilustra que no es comparable con la estrategia BnH. La utilizamos únicamente para demostrar la optimización Walk Forward.

Ahora, esto generará retornos de aproximadamente un 240%, ¡mientras que el Buy and Hold es de un 2.150%!

Strategy Metrics:
  total_return: 2.347
  sharpe_ratio: 0.421
  max_drawdown: -0.709

Buy & Hold Metrics:
  total_return: 21.664
  sharpe_ratio: 0.863
  max_drawdown: -0.444

Optimización Estática

Ahora veamos cómo optimizamos una estrategia. Desarrollaremos una función llamada optimize_strategy que reutilizaremos más adelante durante nuestro walk-forward.

def optimize_strategy(df_prices: pd.DataFrame, param_grid: dict, weights: dict) -> tuple:
    best_score = -np.inf
    best_params = None
    
    param_names = list(param_grid.keys())
    param_ranges = list(param_grid.values())
    param_combinations = list(product(*param_ranges))
    
    for params in param_combinations:
        param_dict = dict(zip(param_names, params))
        
        if param_dict['short_window'] >= param_dict['long_window']:
            continue
            
        df_returns = run_strategy(df_prices, **param_dict)
        metrics = calculate_metrics(df_returns)
        
        score = sum(weights[metric] * value for metric, value in metrics.items())
        
        if score > best_score:
            best_score = score
            best_params = param_dict.copy()
    
    return best_params, best_score

param_grid = {
    'short_window': range(10, 51, 5),
    'long_window': range(60, 201, 10)
}
weights = {
    'total_return': 0.5,
    'sharpe_ratio': 0.25,
    'max_drawdown': -0.25  
}

best_params, best_score = optimize_strategy(prices, param_grid, weights)
print(f"Best params: {best_params}")
print(f"Best score: {best_score:.4f}")

best_returns = run_strategy(prices, **best_params)
best_metrics = calculate_metrics(best_returns)
print("Best strategy metrics:", best_metrics)

Analicemos y comprendamos el código anterior:

La función ejecutará todos los escenarios posibles y devolverá los mejores parámetros que produzcan los resultados óptimos
Calcularemos el “mejor resultado” basándonos en el diccionario de pesos. El retorno tendrá un peso del 50% de importancia, el Ratio de Sharpe un 25%, y el drawdown un 25%.
Además, el rango de parámetros que ejecutaremos está definido en el param_grid. La ventana corta irá de 10 a 50 con un paso de 5, y la larga de 60 a 200 con un paso de 10.

Cuando ejecutemos el código, obtendremos los resultados.

Best params: {'short_window': 40, 'long_window': 190}
Best score: 4.7696
Best strategy metrics: 
{
  'total_return': np.float64(8.9546466471214), 
  'sharpe_ratio': np.float64(0.6712458027118039), 
  'max_drawdown': np.float64(-0.4980050717114106)
}

Los resultados indican que la mejor estrategia implica un período corto de 40 (frente a 50 en la primera ejecución) y 190 (frente a 200). Es interesante destacar que los parámetros son muy similares, aunque la diferencia en los resultados es sustancial. Logra un retorno del 895% frente al 240%, a pesar de tener un ratio de Sharpe y un drawdown ligeramente peores.

Por qué falla la Optimización Estática

Ahora, veamos qué habría ocurrido si retrocediéramos en el tiempo. Suponiendo que estamos a principios de 2025, optimizaremos la estrategia utilizando toda la información disponible hasta finales de 2024, y ejecutaremos los resultados para el resto del período.

prices_copy = prices.copy()
prices_copy.index = pd.to_datetime(prices_copy.index)

train_prices = prices_copy.loc[:'2024-12-31'].copy()
test_prices = prices_copy.loc['2025-01-01':].copy()

best_params_2024, best_score_2024 = optimize_strategy(train_prices, param_grid, weights)

warmup_needed = best_params_2024['long_window']
test_start_idx = prices_copy.index.get_loc(test_prices.index[0])
run_start_idx = max(0, test_start_idx - warmup_needed - 1)

test_with_warmup = prices_copy.iloc[run_start_idx:].copy()
post_2024_results = run_strategy(test_with_warmup, **best_params_2024)
post_2024_results = post_2024_results.loc[test_prices.index[0]:].copy()

post_2024_results['short_window'] = best_params_2024['short_window']
post_2024_results['long_window'] = best_params_2024['long_window']
post_2024_results['strategy_equity_curve'] = (1 + post_2024_results['strategy_returns']).cumprod()
post_2024_results['buy_hold_equity_curve'] = (1 + post_2024_results['market_returns'].fillna(0.0)).cumprod()

train_best_returns = run_strategy(train_prices, **best_params_2024)
train_best_metrics = calculate_metrics(train_best_returns)
post_2024_metrics = calculate_metrics(post_2024_results)
post_2024_bh_metrics = calculate_metrics(post_2024_results, 'market_returns')

print(f"Best params through 2024: {best_params_2024}")
print(f"Best score through 2024: {best_score_2024:.4f}")
print("Train metrics through 2024:", train_best_metrics)
print("Post-2024 strategy metrics:", post_2024_metrics)
print("Post-2024 buy & hold metrics:", post_2024_bh_metrics)

Con esto 0btenemos los siguientes resultados:

Best params through 2024: {'short_window': 40, 'long_window': 190}
Best score through 2024: 5.7777
Train metrics through 2024: {'total_return': np.float64(10.924030420948082), 'sharpe_ratio': np.float64(0.7648261444219087), 'max_drawdown': np.float64(-0.4980050717114106)}
Post-2024 strategy metrics: {'total_return': np.float64(-0.16516091491739893), 'sharpe_ratio': np.float64(-0.32810384163758666), 'max_drawdown': np.float64(-0.3552200321799349)}
Post-2024 buy & hold metrics: {'total_return': np.float64(0.028112770545483334), 'sharpe_ratio': np.float64(0.2310467501453109), 'max_drawdown': np.float64(-0.30222581950627286)}

Esto significa que los mejores parámetros son 40 y 90 (como antes), pero ¿adivina qué? Si hubiéramos utilizado esos parámetros para el resto del período (desde 2025 hasta hoy), ¡habríamos perdido el 16,5% del capital invertido!

Implementando la Optimización Walk-Forward

Ahora expliquemos la optimización walk-forward. Lo importante en este punto son los dos parámetros básicos: el período de re-optimización y el período de entrenamiento. ¿Qué significa esto?

El período de re-optimización se refiere al intervalo de tiempo en el que actualizaremos nuestra estrategia. En el ejemplo siguiente, realizaremos una ejecución inicial con 72 días de trading (aproximadamente 3 meses).

Esto significa que cada tres meses, volveremos a ejecutar nuestra optimización y continuaremos implementando la estrategia con los nuevos parámetros.

El período de entrenamiento es el intervalo de tiempo utilizado para optimizar nuestra estrategia. En el ejemplo siguiente, usaremos 752, que equivale aproximadamente a tres años. Esto significa que si, por ejemplo, llega el momento de re-optimizar nuestra estrategia a principios de 2020, la entrenaremos utilizando datos desde 2017 hasta 2019.

def walk_forward_optimize(df_prices: pd.DataFrame, train_depth_days: int, reopt_freq_days: int,
                          param_grid: dict, weights: dict) -> dict:
    results = {
        'oos_returns': [],
        'param_history': [],
        'walk_metrics': []
    }

    train_end_idx = train_depth_days
    print(f"Train end: {df_prices.index[train_end_idx - 1]}")
    n_rows = len(df_prices)

    while train_end_idx + reopt_freq_days <= n_rows:
        train_start = max(0, train_end_idx - train_depth_days)
        train_df = df_prices.iloc[train_start:train_end_idx]

        test_start = train_end_idx
        test_end = min(n_rows, test_start + reopt_freq_days)

        best_params, _ = optimize_strategy(train_df, param_grid, weights)
        # print(f"Best params for walk {len(results['param_history']) + 1}: {best_params}")

        warmup_needed = best_params['long_window']
        oos_start_with_warmup = max(0, test_start - warmup_needed - 1)  # -1 for pct_change/shift
        oos_df_with_warmup = df_prices.iloc[oos_start_with_warmup:test_end]

        oos_returns_full = run_strategy(oos_df_with_warmup, **best_params)

        oos_dates = df_prices.index[test_start:test_end]
        oos_returns = oos_returns_full.reindex(oos_dates).dropna(subset=['strategy_returns']).copy()

        for param_name, param_value in best_params.items():
            oos_returns[param_name] = param_value

        results['oos_returns'].append(oos_returns)
        results['param_history'].append(best_params)

        oos_metrics = calculate_metrics(oos_returns)
        results['walk_metrics'].append(oos_metrics)

        print(f"Walk {len(results['param_history'])}: "
              f"Train {train_df.index[0].date()}→{train_df.index[-1].date()}, "
              f"OOS {oos_returns.index[0].date()}→{oos_returns.index[-1].date()}, "
              f"OOS Sharpe: {oos_metrics['sharpe_ratio']:.2f}")

        train_end_idx = test_end


    all_oos = pd.concat(results['oos_returns'], ignore_index=False)
    all_oos['strategy_equity_curve'] = (1 + all_oos['strategy_returns']).cumprod()
    all_oos['buy_hold_equity_curve'] = (1 + all_oos['market_returns'].fillna(0.0)).cumprod()
    results['oos_returns_df'] = all_oos  
    results['oos_metrics'] = calculate_metrics(all_oos)

    return results

wfo_results = walk_forward_optimize(
    prices,
    train_depth_days=752,
    reopt_freq_days=72,
    param_grid=param_grid,
    weights=weights
)

print("\nCombined OOS Metrics:", wfo_results['oos_metrics'])

Los resultados obtenidos son los siguientes:

Walk 1: Train 2010-01-04→2012-12-27, OOS 2012-12-28→2013-04-12, OOS Sharpe: 1.54
Walk 2: Train 2010-04-19→2013-04-12, OOS 2013-04-15→2013-07-25, OOS Sharpe: -0.39
Walk 3: Train 2010-07-30→2013-07-25, OOS 2013-07-26→2013-11-05, OOS Sharpe: -0.35
Walk 4: Train 2010-11-10→2013-11-05, OOS 2013-11-06→2014-02-20, OOS Sharpe: 0.27
Walk 5: Train 2011-02-24→2014-02-20, OOS 2014-02-21→2014-06-04, OOS Sharpe: -0.86
Walk 6: Train 2011-06-08→2014-06-04, OOS 2014-06-05→2014-09-16, OOS Sharpe: 1.92
Walk 7: Train 2011-09-20→2014-09-16, OOS 2014-09-17→2014-12-29, OOS Sharpe: 2.09
Walk 8: Train 2012-01-03→2014-12-29, OOS 2014-12-30→2015-04-14, OOS Sharpe: 1.51
Walk 9: Train 2012-04-17→2015-04-14, OOS 2015-04-15→2015-07-27, OOS Sharpe: -0.37
Walk 10: Train 2012-07-30→2015-07-27, OOS 2015-07-28→2015-11-05, OOS Sharpe: -1.36
Walk 11: Train 2012-11-12→2015-11-05, OOS 2015-11-06→2016-02-22, OOS Sharpe: 2.41
Walk 12: Train 2013-02-27→2016-02-22, OOS 2016-02-23→2016-06-03, OOS Sharpe: -0.28
Walk 13: Train 2013-06-11→2016-06-03, OOS 2016-06-06→2016-09-15, OOS Sharpe: -0.50
...
Walk 42: Train 2021-09-24→2024-09-20, OOS 2024-09-23→2025-01-03, OOS Sharpe: 1.35
Walk 43: Train 2022-01-06→2025-01-03, OOS 2025-01-06→2025-04-21, OOS Sharpe: -0.86
Walk 44: Train 2022-04-21→2025-04-21, OOS 2025-04-22→2025-08-04, OOS Sharpe: -0.87
Walk 45: Train 2022-08-04→2025-08-04, OOS 2025-08-05→2025-11-13, OOS Sharpe: 4.24
Walk 46: Train 2022-11-15→2025-11-13, OOS 2025-11-14→2026-03-02, OOS Sharpe: -2.54

Combined OOS Metrics: {'total_return': np.float64(0.5501837113397128), 'sharpe_ratio': np.float64(0.25981084890924117), 'max_drawdown': np.float64(-0.5696955775675036)}

Hemos añadido algunos mensajes de impresión para que comprendas mejor lo que hizo el código.

Por ejemplo, la primera optimización (Walk 1) entrenó la estrategia desde el 4 de enero de 2010 hasta el 27 de diciembre de 2012 (aproximadamente 3 años) y aplicó los mejores parámetros durante los siguientes tres meses, hasta el 12 de abril de 2013. Luego comenzó el segundo Walk. Al día siguiente (13 de abril de 2013), la estrategia fue entrenada desde el 19 de abril de 2010, que es aproximadamente 3 años antes.

La estrategia arrojó un retorno del 55%. Esto puede parecer decepcionante en comparación con el Buy and Hold. Sin embargo, sigue siendo significativamente mejor que el -16% que experimentamos cuando aplicamos los parámetros optimizados del período completo para 2025.

¿Qué tal optimizar las ventanas de la optimización Walk Forward?

Ahora, veamos qué ocurre cuando intentamos optimizar el período de re-optimización y el período de entrenamiento. Usando el código siguiente, ejecutaremos todos los escenarios posibles para:

  • Período de entrenamiento: 252, 504, 752, 1008 (aproximadamente 1, 2, 3 y 4 años)
  • Período de re-optimización: 21, 42, 63, 72, 126 (aproximadamente 1, 2, 3, 4 y 6 meses)
train_depth_days_list = [252, 504, 752, 1008]
reopt_freq_days_list = [21, 42, 63, 72, 126]

wfo_grid_results = []

for train_depth_days in train_depth_days_list:
    for reopt_freq_days in reopt_freq_days_list:
        try:
            wfo_results = walk_forward_optimize(
                prices,
                train_depth_days=train_depth_days,
                reopt_freq_days=reopt_freq_days,
                param_grid=param_grid,
                weights=weights
            )

            metrics = wfo_results['oos_metrics']

            wfo_grid_results.append({
                'train_depth_days': train_depth_days,
                'reopt_freq_days': reopt_freq_days,
                'total_return': metrics['total_return'],
                'sharpe_ratio': metrics['sharpe_ratio'],
                'max_drawdown': metrics['max_drawdown'],
                'n_walks': len(wfo_results['walk_metrics'])
            })
        except Exception:
            wfo_grid_results.append({
                'train_depth_days': train_depth_days,
                'reopt_freq_days': reopt_freq_days,
                'total_return': np.nan,
                'sharpe_ratio': np.nan,
                'max_drawdown': np.nan,
                'n_walks': 0
            })

wfo_metrics_df = pd.DataFrame(wfo_grid_results).sort_values(
    by=['sharpe_ratio', 'total_return'],
    ascending=[False, False]
).reset_index(drop=True)

Ahora vamos a visualizar los retornos en un mapa de calor:

heatmap_data = wfo_metrics_df.pivot(
    index='reopt_freq_days',
    columns='train_depth_days',
    values='total_return'
).sort_index()

fig, ax = plt.subplots(figsize=(10, 6))

im = ax.imshow(
    heatmap_data.values,
    cmap='RdYlGn',
    aspect='auto',
    origin='lower'
)

ax.set_xticks(range(len(heatmap_data.columns)))
ax.set_xticklabels(heatmap_data.columns)
ax.set_yticks(range(len(heatmap_data.index)))
ax.set_yticklabels(heatmap_data.index)

for i in range(heatmap_data.shape[0]):
    for j in range(heatmap_data.shape[1]):
        value = heatmap_data.iloc[i, j]
        if pd.notna(value):
            ax.text(j, i, f'{value:.2f}', ha='center', va='center', color='black')

ax.set_xlabel('train_depth')
ax.set_ylabel('reopt_freq')
ax.set_title('WFO Total Return Heatmap by Train Depth and Reoptimization Frequency')

cbar = fig.colorbar(im, ax=ax)
cbar.set_label('total_return')

plt.tight_layout()
plt.show()

mapa calor python

A partir de este mapa de calor, podemos comprender varios hechos interesantes:

Solo tenemos una combinación con resultado negativo, en la que optimizábamos cada 3 meses utilizando datos de 4 años para el entrenamiento.

En general, parece que cuatro años de entrenamiento son demasiado, ya que capturan ruido y producen los peores resultados.

Los mejores parámetros son re-optimizar cada mes, utilizando datos de los últimos 2 años. Esto resultó en un retorno del 190%, lo que parece prometedor, especialmente considerando que al inicio del artículo logramos un 240% al probar nuestra estrategia con algunos parámetros básicos de corto y largo plazo (50 y 200) para el período completo.

Reflexiones Finales

La Optimización Walk-Forward transforma el backtesting de una garantía engañosa en una herramienta de evaluación práctica.

Conclusiones clave para tu trading:

  1. Nunca confíes únicamente en la optimización dentro de la muestra, ya que casi siempre está sobreajustada.
  2. Las frecuencias de la optimización walk forward importan: demasiado frecuente = parámetros ruidosos; demasiado infrecuente = adaptación desactualizada.
  3. La meta-optimización revela verdades ocultas (tu ventana de entrenamiento “óptima” no es tan obvia).

Comienza reemplazando datos aleatorios por llamadas reales a la API de FMP, añade costos de transacción y slippage para mayor realismo institucional, y luego escala a portafolios multi-activo o incluso modelos de ML. Despliégalo en producción programando re-optimizaciones mensuales con tus ventanas óptimas.

El código está listo para producción. Cópialo, adáptalo, compréndelo. El backtesting adecuado no consiste en encontrar el número más alto. Se trata de números que sobrevivan al futuro. Ya tienes el conjunto de herramientas. Ahora ve y construye estrategias que realmente funcionen.


 

TagsPython
Raul Canessa

Leave a reply