Intenté usar velas OHLC directamente en un modelo de ML y el resultado fue terrible

 

En esta tercera parte de la serie  que busca desentrañar y escudriñar la IA de supergana.com que con autorización previa, nos han dejado profundizar en ella. En este caso me interno en uno de los problemas más comunes al aplicar machine learning al trading: usar velas OHLC directamente como datos de entrenamiento.

Las velas OHLC (Open, High, Low, Close) contienen una enorme cantidad de información sobre el comportamiento del mercado porque cada vela representa una batalla entre compradores y vendedores en un intervalo de tiempo determinado.

En teoría, deberían ser datos perfectos para entrenar modelos de machine learning pero cuando intenté alimentar directamente velas a un modelo el resultado fue sorprendentemente malo.

Trataré de explicar el experimento, por qué falla y cómo solucionarlo mediante feature engineering, todo acompañado de código en Python.

 

El experimento

La idea inicial parecía razonable: Si cada vela contiene apertura, cierre, máximo y mínimo, entonces podríamos usar esos cuatro valores como features para entrenar un modelo que prediga la siguiente dirección del precio.

Formalmente:

features = [open, high, low, close]
target = dirección de la siguiente vela
Puedes copiar este código libremente

Vamos a implementarlo.

 

Preparación del dataset

Primero generamos un dataset OHLC simulado.

import numpy as np
import pandas as pd

np.random.seed(42)

n = 5000

price = np.cumsum(np.random.normal(0, 1, n)) + 100

open_price = price
close_price = price + np.random.normal(0, 0.5, n)

high_price = np.maximum(open_price, close_price) + np.abs(np.random.normal(0, 0.5, n))
low_price = np.minimum(open_price, close_price) – np.abs(np.random.normal(0, 0.5, n))

df = pd.DataFrame({
“open”: open_price,
“high”: high_price,
“low”: low_price,
“close”: close_price
})

df.head()

Puedes copiar este código libremente

 

Ejemplo de salida:

open high low close
100.4 101.2 99.9 100.7
100.7 101.1 100.2 100.3
100.3 101.0 99.8 100.8

 

Crear el objetivo del modelo

Vamos a intentar predecir si la siguiente vela cierra más arriba o más abajo.

df[“target”] = (df[“close”].shift(-1) > df[“close”]).astype(int)

df = df.dropna()

Puedes copiar este código libremente

Ahora tenemos:

features = OHLC
target = 1 si sube
target = 0 si baja

 

Entrenamiento del modelo

Utilizamos un modelo sencillo de clasificación.

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

[wpbch language='python']X = df[[“open”,”high”,”low”,”close”]]
y = df[“target”]

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False
)

model = RandomForestClassifier(n_estimators=100)

model.fit(X_train, y_train)

pred = model.predict(X_test)

accuracy = accuracy_score(y_test, pred)

print(“Accuracy:”, accuracy)

Puedes copiar este código libremente

 

Resultado típico:

Accuracy: 0.50

O incluso peor.

 

¿Por qué el modelo falla?

A primera vista parece absurdo pero las velas contienen muchísima información y la pregunta del millón es ¿por qué el modelo no aprende nada?

Hay varios problemas fundamentales.

 

Escala absoluta

Los modelos no entienden precios.

Para el modelo ejemplo:

USDCAD = 1.38
EURUSD = 1.07
EURGBP = 0.87

son escalas totalmente distintas.

Sin normalización el modelo aprende muy poco.

 

Redundancia extrema

OHLC están altamente correlacionados.

high >= max(open, close)
low <= min(open, close)

Esto significa que muchas variables contienen información redundante y podemos comprobarlo:

 

df[[“open”,”high”,”low”,”close”]].corr()
Puedes copiar este código libremente

 

Resultado típico:

open 1.00
high 0.99
low 0.99
close 1.00

El modelo recibe cuatro variables que básicamente dicen lo mismo.

 

Falta de estructura

Una vela no es solo números y tiene estructura geométrica: cuerpo, mecha superior, mecha inferior y rango, pero el modelo no ve eso.

Solo “ve”:

[102.1, 103.2, 101.9, 102.8]

que en síntesis no significa nada.

 

La solución posible: ingeniería de características

Debemos transformar las velas en variables que representen su estructura real.

Vamos a crear el cuerpo de la vela, rango, mecha superior, mecha inferior y dirección.

 

Extraer información real de las velas

df[“body”] = df[“close”] – df[“open”]

df[“range”] = df[“high”] – df[“low”]

df[“upper_wick”] = df[“high”] – df[[“open”,”close”]].max(axis=1)

df[“lower_wick”] = df[[“open”,”close”]].min(axis=1) – df[“low”]

df[“direction”] = (df[“close”] > df[“open”]).astype(int)

Puedes copiar este código libremente

Ahora cada vela tiene interpretación física.

 

Normalizar las variables

Las velas deben expresarse relativas al rango.

 

df[“body_ratio”] = df[“body”] / df[“range”]

df[“upper_ratio”] = df[“upper_wick”] / df[“range”]

df[“lower_ratio”] = df[“lower_wick”] / df[“range”]

Puedes copiar este código libremente

 

Esto hace que las features sean invariantes al precio.

 

Nuevo conjunto de features

Ahora usamos variables más informativas.

 

features = [
“body_ratio”,
“upper_ratio”,
“lower_ratio”,
“range”
]

X = df[features]
y = df[“target”]
10. Reentrenar el modelo
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False
)

model = RandomForestClassifier(n_estimators=200)

model.fit(X_train, y_train)

pred = model.predict(X_test)

accuracy = accuracy_score(y_test, pred)

print(“Accuracy:”, accuracy)

Puedes copiar este código libremente

 

Resultados típicos: Accuracy: 0.54

 

No es espectacular, pero ya supera el azar.

 

Añadiendo contexto temporal

 

Una vela aislada tampoco dice mucho porque el mercado tiene memoria temporal.

 

Para poder dar contexto, podemos incluir velas anteriores.

 

for lag in range(1,5):
df[f”body_ratio_lag{lag}”] = df[“body_ratio”].shift(lag)
df[f”upper_ratio_lag{lag}”] = df[“upper_ratio”].shift(lag)
df[f”lower_ratio_lag{lag}”] = df[“lower_ratio”].shift(lag)
Puedes copiar este código libremente

 

Luego:

 

df = df.dropna()
12. Modelo con contexto
feature_cols = [col for col in df.columns if “ratio” in col]

X = df[feature_cols]
y = df[“target”]

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, shuffle=False
)

model = RandomForestClassifier(
n_estimators=300,
max_depth=8
)

model.fit(X_train, y_train)

pred = model.predict(X_test)

accuracy = accuracy_score(y_test, pred)

print(“Accuracy:”, accuracy)

Puedes copiar este código libremente

 

Resultados típicos: Accuracy: 0.56 – 0.58. Nada mal con casi un 60% de predicción.

 

Visualizar importancia de variables

 

import matplotlib.pyplot as plt

importance = model.feature_importances_

feat_imp = pd.Series(importance, index=feature_cols)

feat_imp.sort_values().plot(kind=”barh”, figsize=(10,6))

plt.title(“Feature Importance”)
plt.show()

Puedes copiar este código libremente

 

Esto permite entender qué patrones de vela influyen más en el modelo.

 

Conclusión

El error inicial fue asumir que los modelos de machine learning entienden velas y no es así. Una vela OHLC cruda es solo un conjunto de números altamente correlacionados.

Para que el modelo aprenda algo útil es necesario extraer la estructura de la vela, normalizar los datos, incluir contexto temporal y diseñar features informativas.

En trading cuantitativo, el verdadero trabajo no está en el modelo sino que está en cómo representas el mercado en datos y en ese punto, las velas OHLC son solo el comienzo.

Agradecimientos para todo el equipo de supergana.com