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
Vamos a implementarlo.
Preparación del dataset
Primero generamos un dataset OHLC simulado.
np.random.seed(42) n = 5000 price = np.cumsum(np.random.normal(0, 1, n)) + 100 open_price = price high_price = np.maximum(open_price, close_price) + np.abs(np.random.normal(0, 0.5, n)) df = pd.DataFrame({ df.head()import numpy as np
import pandas as pd
close_price = price + np.random.normal(0, 0.5, n)
low_price = np.minimum(open_price, close_price) – np.abs(np.random.normal(0, 0.5, n))
“open”: open_price,
“high”: high_price,
“low”: low_price,
“close”: close_price
})
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 = df.dropna()df[“target”] = (df[“close”].shift(-1) > df[“close”]).astype(int)
Ahora tenemos:
Entrenamiento del modelo Utilizamos un modelo sencillo de clasificación. from sklearn.model_selection import train_test_split [wpbch language='python']X = df[[“open”,”high”,”low”,”close”]] X_train, X_test, y_train, y_test = train_test_split( 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)features = OHLC
target = 1 si sube
target = 0 si baja
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
y = df[“target”]
X, y, test_size=0.2, shuffle=False
)
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()
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[“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)df[“body”] = df[“close”] – df[“open”]
Ahora cada vela tiene interpretación física.
Normalizar las variables
Las velas deben expresarse relativas al rango.
df[“upper_ratio”] = df[“upper_wick”] / df[“range”] df[“lower_ratio”] = df[“lower_wick”] / df[“range”]df[“body_ratio”] = df[“body”] / df[“range”]
Esto hace que las features sean invariantes al precio.
Nuevo conjunto de features
Ahora usamos variables más informativas.
X = df[features] 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)features = [
“body_ratio”,
“upper_ratio”,
“lower_ratio”,
“range”
]
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
)
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)
Luego:
X = df[feature_cols] X_train, X_test, y_train, y_test = train_test_split( model = RandomForestClassifier( model.fit(X_train, y_train) pred = model.predict(X_test) accuracy = accuracy_score(y_test, pred) print(“Accuracy:”, accuracy)df = df.dropna()
12. Modelo con contexto
feature_cols = [col for col in df.columns if “ratio” in col]
y = df[“target”]
X, y, test_size=0.2, shuffle=False
)
n_estimators=300,
max_depth=8
)
Resultados típicos: Accuracy: 0.56 – 0.58. Nada mal con casi un 60% de predicción.
Visualizar importancia de variables
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”)import matplotlib.pyplot as plt
plt.show()
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