Aprendizaje automático y Data Science (Parte 37): Uso de patrones de velas japonesas e inteligencia artificial para superar al mercado
Contenido
- Introducción
- Conceptos básicos de las velas japonesas
- Patrones de velas japonesas
- Indicador de detección de patrones de velas japonesas
- Recopilación de patrones de velas japonesas para el aprendizaje automático
- Entrenamiento de un modelo de IA para que realice predicciones basadas en patrones de velas japonesas
- Implementarlo en un robot de trading
- Conclusión
Introducción
La primera estrategia que utilicé en el trading se basaba en las velas japonesas; hoy en día no la llamaría estrategia, pero la primera vez que abrí una operación fue gracias a unos patrones de velas que aprendí leyendo un libro titulado The Candlestick Bible, de Honma Munehisa, que me recomendó un amigo.
Los operadores utilizan los patrones de velas en los gráficos financieros para determinar posibles movimientos de precios basándose en patrones históricos; estos patrones se generan a partir de las fluctuaciones alcistas y bajistas de los precios que, aunque parezcan aleatorias, los operadores utilizan para pronosticar la dirección a corto plazo del precio.
Este concepto se remonta al siglo XVIII y fue creado por un trader llamado Honma Munehisa, a quien se considera el trader más exitoso de la historia. Conocido en su época como el «Dios de los mercados», sus descubrimientos le reportaron más de 10.000 millones de dólares al cambio actual.
Munehisa descubrió que, si bien el precio del arroz viene determinado por la oferta y la demanda, los mercados también se veían influidos por las emociones humanas.
Estas emociones humanas pueden reflejarse en un gráfico de velas, donde la magnitud de los movimientos de precios se representa con diferentes colores; a menudo, una vela negra se utiliza para indicar una tendencia bajista y una vela blanca suele indicar una tendencia alcista; hoy en día, estos colores no tienen importancia, ya que pueden modificarse en cualquier plataforma de negociación.
Conceptos básicos de las velas japonesas

Las sombras o mechas de las velas indican los precios máximos y mínimos del día y cómo se comparan con los precios de apertura y cierre. La forma de la vela varía en función de la relación entre los precios OHLC (apertura, máximo, mínimo y cierre) de la vela. Hay un montón de patrones de velas que Munehisa introdujo en su día, y otros que han surgido recientemente entre los traders.
A partir de estos patrones de velas japonesas, vamos a identificarlos, recopilarlos y aplicarlos a modelos de aprendizaje automático, para observar cómo pueden aportar valor a nuestros modelos de trading basados en IA y comprobar si nos ayudan a obtener mejores resultados que los mercados financieros.
Patrones de velas japonesas
Dado que hay multitud de patrones de velas japonesas, vamos a analizar solo 10 de ellos; la razón principal por la que hemos elegido estos 10 es que son fáciles de entender y se basan únicamente en una sola vela, es decir, se pueden detectar en la vela actual.
Descartaremos todos los patrones de velas japonesas cuya detección requiera el análisis de varias velas.
El formato de código de la clase CTALib se inspira en TALib (Technical Analysis Library), una biblioteca para análisis técnico basada en C#, C++ y Python que incluye varias funciones para detectar patrones de velas japonesas.
Una vela blanca
Esta es una vela alcista en la que el cierre es superior a la apertura. Esta sencilla vela alcista indica un impulso alcista; no ofrece ninguna predicción, solo es un indicio de la dirección de la vela.
Podemos programarlo de la siguiente manera.
bool CTALib::CDLWHITECANDLE(double open, double close) { return (close > open); }
Una vela negra
A diferencia de la vela blanca, esta es una vela bajista en la que el precio de apertura es superior al de cierre. Esta sencilla vela bajista indica una tendencia a la baja.
Al igual que la vela blanca, no ofrece ninguna predicción, simplemente indica la dirección de la vela.
bool CTALib::CDLBLACKCANDLE(double open,double close) { return (open > close); }
Un patrón de vela Doji
Una vela doji es aquella en la que los precios de apertura y cierre son prácticamente iguales. Parece una cruz, a veces una cruz invertida, o un signo de suma. Este patrón de velas japonesas es poco común, ya que suele aparecer en grupos. Suele ser una señal de cambio de tendencia, aunque también puede indicar indecisión sobre los precios futuros del mercado.
Existen algunas variaciones, como se muestra en la siguiente imagen.

Programar una función para detectar una vela doji puede resultar complicado, simplemente por la propia definición de una vela doji. Para que una vela se considere un doji, los precios de apertura y cierre deben ser iguales. Es extremadamente raro que el precio de apertura y el de cierre coincidan, dado el volumen y la volatilidad que caracterizan al mercado actual; incluso si los precios se acercaran mucho, por ejemplo: apertura = 1,0000 y cierre = 1,0001, para un ordenador estos dos valores no son iguales.
Para resolver esto, podemos utilizar un valor de tolerancia; siempre que la diferencia entre el precio de apertura y el de cierre se encuentre dentro de ese valor, podemos considerar que los precios están demasiado próximos y concluir que se trata de un patrón de velas doji.
bool CTALib::CDLDOJI(double open,double close, double torrelance=3) { return (fabs(open-close)<torrelance*_Point); //A torrelance in market points }
Doji «Libélula»
El patrón de velas Doji «Libélula» indica un posible cambio de tendencia en el precio de un valor. Si aparece un patrón doji de libélula en una tendencia bajista, es una buena señal de que los precios están a punto de subir o de que la tendencia bajista podría haber terminado.

Dado que se trata simplemente de una vela doji con una sombra inferior más larga que la sombra superior, en el código podemos asegurarnos primero de que la vela sea un doji y luego debemos asegurarnos explícitamente de que la sombra inferior sea el doble de larga que la sombra superior.
bool CTALib::CDLDRAGONFLYDOJI(double open, double high, double low, double close, double body_torrelance = 3, double shadow_ratio = 2.0) { double body_size = MathAbs(open - close); double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); //--- Body is very small (like a Doji) if (CDLDOJI(open, close, body_torrelance)) { //--- Lower shadow is significantly longer than upper shadow if (lower_shadow > upper_shadow * shadow_ratio) return true; } return false; }
Doji «Lápida»
Esta es una vela doji con una larga sombra superior (mecha). Se trata de un patrón de reversión bajista que indica un cambio en la dirección del mercado.
Cuando esta vela aparece en una tendencia alcista, indica que la tendencia actual podría estar llegando a su fin, ya que está a punto de producirse una tendencia bajista.

Dado que se trata simplemente de una vela doji con una sombra superior más larga que la inferior, en el código podemos comprobar primero que la vela es un doji y, a continuación, debemos asegurarnos explícitamente de que la sombra superior es el doble de larga que la inferior.
bool CTALib::CDLGRAVESTONEDOJI(double open, double high, double low, double close, double body_torrelance = 3, double shadow_ratio = 2.0) { double body_size = MathAbs(open - close); double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); //--- Body is very small (like a Doji) if (CDLDOJI(open, close, body_torrelance)) { //--- Lower shadow is significantly longer than upper shadow if (upper_shadow > lower_shadow * shadow_ratio) return true; } return false; }
Martillo
Se trata de una vela con un cuerpo pequeño en la parte superior y una sombra larga en la parte inferior. Se trata de una señal de reversión alcista que, cuando aparece en una tendencia bajista, indica que existe un potencial movimiento alcista en el futuro.

Los patrones de velas japonesas son abstractos y confusos; a veces, un patrón puede variar dependiendo de cómo se mire y de lo que se esté buscando en cada momento. El patrón del martillo puede confundirse con el doji de la libélula, ya que lo único que diferencia a ambos es el tamaño de su cuerpo, pero comparten las mismas características.
Este es uno de los problemas con los que nos encontramos a menudo al trabajar con patrones de velas japonesas.
Este problema puede abordarse estableciendo reglas explícitas y valores umbral que puedan ajustarse según un instrumento (símbolo) en particular y otras necesidades.
bool CTALib::CDLHAMMER(double open, double high, double low, double close, double min_body_percentage = 0.2, // To avoid being a doji double lower_shadow_ratio = 2.0, // Lower shadow at least 2x the body double upper_shadow_max_ratio = 0.3) // Upper shadow must be small { double body_size = MathAbs(open - close); double total_range = high - low + DBL_EPSILON; double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); double body_percentage = body_size / total_range; return ( body_percentage >= min_body_percentage && lower_shadow >= lower_shadow_ratio * body_size && upper_shadow <= upper_shadow_max_ratio * body_size ); }
Dado que tanto la vela de la libélula como la vela martillo tienen una sombra inferior más larga, un cuerpo pequeño y una sombra superior corta, tuvimos que introducir el parámetro min_body_percentage para comprobar qué tamaño mínimo debe tener el cuerpo en relación con su rango total (máximo-mínimo) para que una vela se considere un martillo, al tiempo que se comprueba, por defecto, si su sombra inferior es el doble de larga que su sombra superior.
Martillo invertido
Es similar al martillo, pero tiene un cuerpo pequeño en la parte inferior, una sombra superior larga y una sombra inferior pequeña.
Este patrón se observa en las tendencias bajistas, ya que indica que está a punto de producirse un cambio de tendencia alcista en el mercado.

Podemos programarlo de forma similar al martillo, solo que con pequeñas modificaciones en las sombras.
bool CTALib::CDLINVERTEDHAMMER(double open, double high, double low, double close, double min_body_percentage = 0.2, // Avoid doji double upper_shadow_ratio = 2.0, // Upper shadow must be long double lower_shadow_max_ratio = 0.3) // Lower shadow should be small { double body_size = MathAbs(open - close); double total_range = high - low + DBL_EPSILON; double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); double body_percentage = body_size / total_range; return ( body_percentage >= min_body_percentage && upper_shadow >= upper_shadow_ratio * body_size && lower_shadow <= lower_shadow_max_ratio * body_size ); }
Peonza
Esta es una vela con un cuerpo pequeño en el centro y sombras largas a ambos lados.

Cuando aparece esta vela, significa indecisión en el mercado, lo que a menudo implica la continuación de la tendencia actual, ya que ni compradores ni vendedores tienen la sartén por el mango.
Dado que esta vela es similar a una vela doji (solo que tiene un cuerpo más grande), debemos asegurarnos expresamente de que el cuerpo no sea pequeño como el de una doji y de que se sitúe en el centro de las sombras largas con respecto al cuerpo.
bool CTALib::CDLSPINNINGTOP(double open, double high, double low, double close, double body_percentage_threshold = 0.3, double shadow_ratio = 2.0, double shadow_symmetry_tolerance = 0.3) { double body_size = MathAbs(open - close); double total_range = high - low + DBL_EPSILON; double upper_shadow = upperShadowCalc(open, close, high); double lower_shadow = lowerShadowCalc(open, close, low); double body_percentage = body_size / total_range; //--- Calculate shadow symmetry ratio double shadow_diff = MathAbs(upper_shadow - lower_shadow); double shadow_sum = upper_shadow + lower_shadow + DBL_EPSILON; double symmetry_ratio = shadow_diff / shadow_sum; // Closer to 0 = more balanced return ( body_percentage < body_percentage_threshold && // Body is small compared to candle size upper_shadow > body_size * shadow_ratio && // Both shadows are significantly larger than the body lower_shadow > body_size * shadow_ratio && symmetry_ratio <= shadow_symmetry_tolerance //Shadows are roughly equal (symmetrical) ); }
Marubozu alcista
El nombre «Marubozu» proviene de la palabra japonesa que significa «cortado al ras», y hace referencia a una vela sin sombras.
Una vela Marubozu alcista es una vela alcista con sombras inferiores y superiores (mechas) pequeñas o inexistentes; constituye una señal alcista clara que indica impulso.

Podemos añadir un valor de tolerancia en puntos para comprobar si los precios de apertura y cierre están muy próximos a sus máximos y mínimos.
bool CTALib::CDLBULLISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2) { return (MathAbs(open - low) <= (tolerance*_Point) && MathAbs(close - high) <= (tolerance*_Point) && close > open); }
Marubozu bajista
Una vela Marubozu bajista es una vela bajista con sombras inferiores y superiores (mechas) pequeñas o inexistentes; constituye una señal bajista clara que indica un impulso bajista.

Al igual que en el caso de la vela Marubozu alcista, disponemos de un valor de tolerancia en puntos para comprobar si los precios de apertura y cierre se encuentran muy próximos a sus máximos y mínimos.
bool CTALib::CDLBEARISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2) { return (MathAbs(open - high) <= (tolerance*_Point) && MathAbs(close - low) <= (tolerance*_Point) && close < open); }
Por ahora, solo estamos considerando la detección de patrones de velas japonesas y sus señales basándonos en su aspecto; sin embargo, según mis fuentes, la forma correcta de extraer las señales de una vela japonesa debe incluir la detección de tendencias; por ejemplo, para que un martillo se considere una señal alcista, debe aparecer en una tendencia bajista.
Las tendencias son un factor clave que quizá quieras tener en cuenta si deseas llevar este proyecto más allá.
Indicador de detección de patrones de velas japonesas
Visualicemos los patrones de velas japonesas utilizando el código que empleamos para derivarlos. Simplemente porque a menudo estos patrones me parecen abstractos y confusos, y dado que tenemos la intención de utilizar estos datos para fines de aprendizaje automático, donde sabemos que la calidad de los datos es lo más importante. Al visualizar estos patrones, nos aseguramos de que conozcas cómo se han calculado y cuál es su representación gráfica.
No dudes en ajustar algunos parámetros y modificar el código como mejor te parezca.
Asegurémonos de que, al menos, el código que acabamos de escribir sea capaz de identificar estos patrones que nosotros, como seres humanos, también podemos detectar en el mercado.
Este indicador basado en velas tendrá 5 buffers y una línea en el gráfico principal.
Nombre del archivo: Candlestick Identifier.mq5
#property indicator_chart_window #property indicator_buffers 5 #property indicator_plots 1 #property indicator_type1 DRAW_COLOR_CANDLES #property indicator_color1 clrDodgerBlue, clrOrange, clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 double OpenBuff[]; double HighBuff[]; double LowBuff[]; double CloseBuff[]; double ColorBuff[]; #include <ta-lib.mqh> //!important for candlestick patterns
Dado que la biblioteca ta-lib.mqh es una clase estática, no es necesario inicializar sus clases, por lo que podemos llamar a las funciones para detectar patrones de velas japonesas directamente dentro de la función OnCalculate.
int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- if (rates_total<1) return rates_total; for(int i = prev_calculated; i < rates_total; i++) { OpenBuff[i] = open[i]; HighBuff[i] = high[i]; LowBuff[i] = low[i]; CloseBuff[i] = close[i]; //--- if (close[i]>open[i]) ColorBuff[i] = 1.0; else ColorBuff[i] = 0.0; //--- double padding = MathAbs(high[i] - low[i]) * 0.2; // 20% padding if (CTALib::CDLDOJI(open[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding, "Doji", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLDRAGONFLYDOJI(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"DragonFly Doji", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLGRAVESTONEDOJI(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"GraveStone Doji", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLHAMMER(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Hammer", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLINVERTEDHAMMER(open[i], high[i], low[i], close[i])) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Inverted Hammer", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLSPINNINGTOP(open[i], high[i], low[i], close[i], 0.3, 2.0)) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Spinning Top", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLBULLISHMARUBOZU(open[i], high[i], low[i], close[i], 2)) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bullish Marubozu", clrBlack, 90.0); ColorBuff[i] = 2.0; } if (CTALib::CDLBEARISHMARUBOZU(open[i], high[i], low[i], close[i], 2)) { TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bearish Marubozu", clrBlack, 90.0); ColorBuff[i] = 2.0; } } //--- return value of prev_calculated for next call return(rates_total); }
Por defecto, las velas alcistas aparecen en naranja, mientras que las bajistas aparecen en azul; cualquier vela que se detecte con los patrones descritos anteriormente en este artículo se marcará en rojo, acompañada de un texto orientado en un ángulo de 90 grados que indique el tipo de patrón de velas al que pertenece la vela marcada en rojo.

Como puede ver en la imagen anterior, nuestro indicador de detección de velas japonesas identifica bastante bien estos patrones de velas; ahora podemos estar seguros de la lógica que hemos aplicado en nuestro código.
Ahora procedamos a recopilar estos patrones y a guardarlos en un archivo CSV mediante un script; a continuación, utilizaremos esta información para entrenar modelos de aprendizaje automático.
Recopilación de patrones de velas japonesas para el aprendizaje automático
Dado que algunos patrones de velas japonesas no aparecen con frecuencia en el mercado, especialmente en los marcos temporales superiores, donde hay muy pocas barras en el historial, recopilemos nuestros datos desde el 1 de enero de 2005 hasta el 1 de enero de 2023.
Este periodo de 18 años debería proporcionarnos una gran cantidad de barras del gráfico diario y, por lo tanto, numerosos patrones que nuestros modelos de aprendizaje automático podrán analizar.
#include <ta-lib.mqh> //Contains CTALib class for candlestick patterns detection #include <MALE5\Pandas\pandas.mqh> //https://www.mql5.com/en/articles/17030 input datetime start_date = D'2005.01.01'; input datetime end_date = D'2023.01.01'; input string symbol = "XAUUSD"; input ENUM_TIMEFRAMES timeframe = PERIOD_D1; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- vector open, high, low, close; open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date); high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date); low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date); close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date); CDataFrame df; vector cdl_patterns = {}; cdl_patterns = CTALib::CDLWHITECANDLE(open, close); df.insert("White Candle", cdl_patterns); cdl_patterns = CTALib::CDLBLACKCANDLE(open, close); df.insert("Black Candle", cdl_patterns); cdl_patterns = CTALib::CDLDOJI(open, close); df.insert("Doji Candle", cdl_patterns); cdl_patterns = CTALib::CDLDRAGONFLYDOJI(open, high, low, close); df.insert("Dragonflydoji Candle", cdl_patterns); cdl_patterns = CTALib::CDLGRAVESTONEDOJI(open, high, low, close); df.insert("Gravestonedoji Candle", cdl_patterns); cdl_patterns = CTALib::CDLHAMMER(open, high, low, close); df.insert("Hammer Candle", cdl_patterns); cdl_patterns = CTALib::CDLINVERTEDHAMMER(open, high, low, close); df.insert("Invertedhammer Candle", cdl_patterns); cdl_patterns = CTALib::CDLSPINNINGTOP(open, high, low, close); df.insert("Spinningtop Candle", cdl_patterns); cdl_patterns = CTALib::CDLBULLISHMARUBOZU(open, high, low, close); df.insert("BullishMarubozu Candle", cdl_patterns); cdl_patterns = CTALib::CDLBEARISHMARUBOZU(open, high, low, close); df.insert("BearishMarubozu Candle", cdl_patterns); df.insert("Open", open); df.insert("High", high); df.insert("Low", low); df.insert("Close", close); df.to_csv(StringFormat("CandlestickPatterns.%s.%s.csv",symbol,EnumToString(timeframe)), true); }
También recopilamos los valores OHLC (apertura, máximo, mínimo y cierre) para preparar la variable objetivo, y por si acaso surge algún imprevisto y los necesitamos.
Entrenamiento de un modelo de IA para realizar predicciones basadas en patrones de velas japonesas
Ahora que disponemos de un conjunto de datos, vamos a cargarlos en un script de Python (Cuaderno Jupyter).
import pandas as pd symbol = "XAUUSD" df = pd.read_csv(f"/kaggle/input/forex-candlestick-patterns/CandlestickPatterns.{symbol}.PERIOD_D1.csv") df
Resultados
| Vela blanca | Vela negra | Vela Doji | Vela Doji de libélula | Vela Doji de lápida | Vela de martillo | Vela de martillo invertido | Vela de peonza | Vela Marubozu alcista | Vela Marubozu bajista | Apertura | Alto | Bajo | Cierre | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 438.45 | 438.71 | 426.72 | 429.55 |
| 1 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 429.52 | 430.18 | 423.71 | 427.51 |
| 2 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 427.50 | 428.77 | 425.10 | 426.58 |
| 3 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 426.31 | 427.85 | 420.17 | 421.37 |
| 4 | 0.0 | 1.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 421.39 | 425.48 | 416.57 | 419.02 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
El mayor reto que surge al trabajar con estos datos de velas japonesas es definir la variable objetivo.
En la mayoría de los problemas de clasificación relacionados con los mercados financieros, solemos definir la variable objetivo basándonos en la evolución futura de los precios, utilizando un parámetro que a menudo denominamos «lookahead».
Este parámetro especifica cuántos compases (o intervalos de tiempo) nos adelantamos en los datos. Por ejemplo, si el avance está fijado en 1, comparamos el precio de cierre de la barra actual con el precio de cierre de la siguiente barra:
Si Close[next bar] > Close[current bar], indica un movimiento alcista, por lo que asignamos una etiqueta de objetivo de 1.
De lo contrario, si no se cumple esta condición, sugiere un movimiento bajista, por lo que le asignamos una etiqueta de 0.
Podemos hacer lo mismo aquí para asegurarnos de usar nuestras características para predecir la información con anticipación, pero como se describe en la tabla anterior, tenemos muchos ceros en estos datos, ya que a menudo no tenemos ninguna barra especial detectada en cada fila aparte de las velas blancas y negras, que no son patrones especiales y no las consideramos patrones de velas japonesas.
Esto significa que, en la mayoría de los casos, proporcionaremos valores nulos a nuestros modelos de aprendizaje automático y obligaremos a los modelos a comprender la relación y a predecir la variable objetivo sin disponer de ningún dato significativo; esto hará que nuestros modelos se basen en exceso en las velas blancas y negras. Algo que no deseamos.
Podemos abordar este problema eliminando todas las filas con valores nulos para todos los patrones de velas japonesas y entrenando el modelo con datos de velas japonesas puros; esto también nos obligaría a evitar que se repita la misma situación durante la ejecución del modelo.
Otra forma de abordar esto consiste en introducir la señal de la clase «hold», indicada por -1, en todas las filas en las que todos los patrones de velas eran 0 (falso), excepto en las columnas de velas blancas y negras; sin embargo, esto planteará un grave problema de desequilibrio entre clases, que ya tratamos en el artículo anterior, pero, aun así, este enfoque no logró resolver el problema.
Por ahora, procedamos a preparar la variable de destino de todos modos.
lookahead = 1 new_df = df.copy() new_df["future_close"] = new_df["Close"].shift(-lookahead) new_df.dropna(inplace=True) # Drop NaNs caused by the shift operation signal = [] for i in range(len(new_df)): # Iterate over rows, not columns if new_df["future_close"].iloc[i] > new_df["Close"].iloc[i]: signal.append(1) else: signal.append(0) new_df["Signal"] = signal
A continuación, dividimos las variables predictoras en una matriz bidimensional denominada X, descartando al mismo tiempo las características no deseadas, como los valores OHLC, la columna que queremos predecir (variable objetivo) y la característica de cierre future_close, que utilizamos para derivar la columna objetivo. También asignamos la columna de destino denominada Signal a la matriz y.
X = new_df.drop(columns=[ "Signal", "Open", "High", "Low", "Close", "future_close" ]) y = new_df["Signal"] # Split data into train and test sets X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, shuffle=False)
Para este problema, he optado por trabajar con un modelo Catboost, ya que contamos con numerosas columnas categóricas que, en teoría, deberían funcionar bien con el clasificador Catboost.
from catboost import CatBoostClassifier from sklearn.utils.class_weight import compute_class_weight # Automatically calculate class weights classes = np.unique(y) weights = compute_class_weight(class_weight='balanced', classes=classes, y=y) class_weights = dict(zip(classes, weights)) # Define the base model model = CatBoostClassifier( iterations=1000, learning_rate=0.01, depth=5, loss_function='Logloss', class_weights=class_weights, verbose=100 ) model.fit(X_train, y_train) # Training the classifier
Resultados.
0: learn: 0.6930586 total: 3.64ms remaining: 3.64s 100: learn: 0.6897625 total: 136ms remaining: 1.21s 200: learn: 0.6888030 total: 269ms remaining: 1.07s 300: learn: 0.6883559 total: 401ms remaining: 931ms 400: learn: 0.6881469 total: 532ms remaining: 795ms 500: learn: 0.6879966 total: 661ms remaining: 658ms 600: learn: 0.6879013 total: 789ms remaining: 524ms 700: learn: 0.6878311 total: 916ms remaining: 391ms 800: learn: 0.6877729 total: 1.04s remaining: 260ms 900: learn: 0.6877273 total: 1.17s remaining: 129ms 999: learn: 0.6876900 total: 1.3s remaining: 0us <catboost.core.CatBoostClassifier at 0x798cc6d08dd0>
Evaluemos este modelo con los datos que aún no ha visto (la muestra de prueba).
y_pred = model.predict(X_test)
print("\nClassification Report:\n", classification_report(y_test, y_pred)) Resultados.
Classification Report: precision recall f1-score support 0 0.49 0.55 0.52 429 1 0.58 0.52 0.55 511 accuracy 0.53 940 macro avg 0.53 0.53 0.53 940 weighted avg 0.54 0.53 0.53 940
Los resultados indican que el modelo presenta una precisión media de 0,58 y 0,49 para las clases 1 y 0, respectivamente; aunque podemos confiar en este modelo para predecir la clase 1 —lo cual hace con un 58 % de certeza—, no podemos confiar en él para predecir la clase 0; en este caso, es mejor adivinar.
Una precisión global del 53 % sobre el 100 %, lo cual es realista en este ámbito de negociación; es mejor que lanzar una moneda al aire o adivinar al azar, lo que garantiza una probabilidad de acierto del 50 %.
Analicemos el gráfico de importancia de las características para ver cuáles fueron las que más influyeron en este modelo.
import matplotlib.pyplot as plt # Get feature importances importances = model.get_feature_importance() feature_names = X_train.columns if hasattr(X_train, 'columns') else [f'feature_{i}' for i in range(X_train.shape[1])] # Create DataFrame for plotting feat_imp_df = pd.DataFrame({ 'Feature': feature_names, 'Importance': importances }).sort_values(by='Importance', ascending=False) # Plot plt.figure(figsize=(7, 3)) plt.barh(feat_imp_df['Feature'], feat_imp_df['Importance']) plt.gca().invert_yaxis() # Highest importance on top plt.title('Feature Importances') plt.xlabel('Importance') plt.ylabel('Feature') plt.tight_layout() plt.show()
Resultados.

La formación de velas «peonza» fue el elemento más relevante de este modelo, seguida de la vela «Doji», mientras que la vela «Marubozu bajista» fue la que tuvo menor relevancia.
Según lo que he leído sobre estos patrones de velas japonesas, algunos de ellos sirven para indicar la tendencia o lo que va a suceder en el mercado durante un periodo de tiempo prolongado o un horizonte temporal determinado; por ejemplo, una vela doji que aparezca en la parte superior de una tendencia alcista podría indicar que está a punto de producirse una tendencia bajista durante algún tiempo.
Hemos creado la variable de destino basándonos en un valor de anticipación de 1, por lo que estamos utilizando estos patrones de velas japonesas para indicar una barra por delante, en lugar de un número determinado de barras por delante.
Por lo tanto, no dude en probar diferentes valores de anticipación superiores a 1 para observar el impacto que estos patrones de velas tienen en un periodo más amplio o en distintos horizontes temporales; en mi análisis hasta la fecha, he descubierto que el modelo entrenado con un valor de anticipación de 1 es el que ofrece mayor precisión.
Por lo tanto, no dude en probar diferentes valores de anticipación superiores a 1 para observar el impacto que estos patrones de velas tienen en un periodo más amplio o en distintos horizontes temporales; en mi análisis hasta la fecha, he descubierto que el modelo entrenado con un valor de anticipación de 1 es el que ofrece mayor precisión.
Implementarlo en un robot de trading
Ahora que hemos entrenado un modelo basado en patrones de velas japonesas, probémoslo en un entorno de negociación real y veamos si estos patrones pueden resultar útiles en el ámbito de la inteligencia artificial (IA).
En primer lugar, debemos guardar nuestro modelo en formato ONNX, que es compatible con MQL5 y MetaTrader 5.
model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X_train.shape[1]]))], target_opset={"": 12, "ai.onnx.ml": 2}, ) # And save. with open(f"CatBoost.CDLPatterns.{symbol}.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
Puede encontrar más información sobre cómo guardar este modelo de Catboost aquí.
Nuestro asesor experto (EA) es bastante sencillo.
#include <Trade\Trade.mqh> //The trading module #include <Trade\PositionInfo.mqh> //Position handling module #include <ta-lib.mqh> //For candlestick patterns #include <Catboost.mqh> //Has a class for deploying a catboost model CTrade m_trade; CPositionInfo m_position; CCatboostClassifier catboost; input int magic_number = 21042025; input int slippage = 100; input string symbol_ = "XAUUSD"; input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1; input int lookahead = 1; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if (!MQLInfoInteger(MQL_TESTER)) if (!ChartSetSymbolPeriod(0, symbol_, timeframe_)) { printf("%s failed to set symbol %s and timeframe %s, Check these values. Err = %d",__FUNCTION__,symbol_,EnumToString(timeframe_),GetLastError()); return INIT_FAILED; } //--- if (!catboost.Init(StringFormat("CatBoost.CDLPatterns.%s.onnx",symbol_), ONNX_COMMON_FOLDER)) //Initialize the catboost model return INIT_FAILED; //--- m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(slippage); m_trade.SetMarginMode(); m_trade.SetTypeFillingBySymbol(Symbol()); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- double open = iOpen(Symbol(), Period(), 1), high = iHigh(Symbol(), Period(), 1), low = iLow(Symbol(), Period(), 1), close = iClose(Symbol(), Period(), 1); vector x = { CTALib::CDLWHITECANDLE(open, close), CTALib::CDLBLACKCANDLE(open, close), CTALib::CDLDOJI(open, close), CTALib::CDLDRAGONFLYDOJI(open, high, low, close), CTALib::CDLGRAVESTONEDOJI(open, high, low, close), CTALib::CDLHAMMER(open, high, low, close), CTALib::CDLINVERTEDHAMMER(open, high, low, close), CTALib::CDLSPINNINGTOP(open, high, low, close), CTALib::CDLBULLISHMARUBOZU(open, high, low, close), CTALib::CDLBEARISHMARUBOZU(open, high, low, close) }; long signal = catboost.predict(x).cls; //Predicted class MqlTick ticks; if (!SymbolInfoTick(Symbol(), ticks)) { printf("Failed to obtain ticks information, Error = %d",GetLastError()); return; } double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); if (signal == 1) { if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL)) m_trade.Buy(volume_, Symbol(), ticks.ask,0,0); } if (signal == 0) { if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY)) m_trade.Sell(volume_, Symbol(), ticks.bid,0,0); } CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe }
Tras inicializar el modelo Catboost en formato ONNX que se guardó en la carpeta común.
Dentro de la función OnTick, obtenemos los valores de apertura, máximo, mínimo y cierre de la barra cerrada anteriormente y los procesamos mediante las funciones de CTALib para detectar patrones de velas japonesas; a continuación, utilizamos los resultados para realizar predicciones en un vector denominado x.
Debemos tener en cuenta las características y su orden tal y como se utilizaron en el entrenamiento del modelo Catboost definitivo dentro del script de Python.
X_train.columns
En nuestro modelo final teníamos.
Index(['White Candle', 'Black Candle', 'Doji Candle', 'Dragonflydoji Candle', 'Gravestonedoji Candle', 'Hammer Candle', 'Invertedhammer Candle', 'Spinningtop Candle', 'BullishMarubozu Candle', 'BearishMarubozu Candle'], dtype='object')
Este orden se mantuvo dentro del asesor técnico.
Actualmente, no disponemos de un stop loss ni de su correspondiente nivel de take profit, por lo que cerramos las operaciones abiertas (posiciones) una vez transcurrido un número determinado de barras en el marco temporal establecido.
Configuraciones en el Probador de estrategias.

Resultados.

Lo que me fascina son las similitudes en los resultados entre las operaciones largas y las cortas; durante este periodo de dos años se abrieron 257 operaciones cortas. 2 operaciones menos que las posiciones largas, que fueron 259.
Esto es inadecuado y podemos afirmar que, a pesar de que el modelo tiene en cuenta todas las formaciones de velas, las más influyentes son las velas blancas y negras, ya que son las que aparecen en cada barra; además, cerramos la operación tras una sola barra (valor de anticipación = 1). Este problema se debe a la forma en que preparamos la variable objetivo y entrenamos el modelo con datos que contenían valores nulos (falsos) en muchas características.
Para garantizar que se respeten los patrones de velas únicos, debemos comprobar en cada ocasión si todos los patrones de velas especiales han dado 0 (falso) —es decir, si el modelo no los ha detectado— y evitar abrir una operación cuando esto ocurra.
Solo queremos abrir una operación cuando se detecte algún patrón de velas técnico específico, distinto de las velas blancas y negras.
void OnTick() { //--- double open = iOpen(Symbol(), Period(), 1), high = iHigh(Symbol(), Period(), 1), low = iLow(Symbol(), Period(), 1), close = iClose(Symbol(), Period(), 1); vector x = { CTALib::CDLWHITECANDLE(open, close), CTALib::CDLBLACKCANDLE(open, close), CTALib::CDLDOJI(open, close), CTALib::CDLDRAGONFLYDOJI(open, high, low, close), CTALib::CDLGRAVESTONEDOJI(open, high, low, close), CTALib::CDLHAMMER(open, high, low, close), CTALib::CDLINVERTEDHAMMER(open, high, low, close), CTALib::CDLSPINNINGTOP(open, high, low, close), CTALib::CDLBULLISHMARUBOZU(open, high, low, close), CTALib::CDLBEARISHMARUBOZU(open, high, low, close) }; vector patterns = { CTALib::CDLDOJI(open, close), CTALib::CDLDRAGONFLYDOJI(open, high, low, close), CTALib::CDLGRAVESTONEDOJI(open, high, low, close), CTALib::CDLHAMMER(open, high, low, close), CTALib::CDLINVERTEDHAMMER(open, high, low, close), CTALib::CDLSPINNINGTOP(open, high, low, close), CTALib::CDLBULLISHMARUBOZU(open, high, low, close), CTALib::CDLBEARISHMARUBOZU(open, high, low, close) }; //Store all the special patterns long signal = catboost.predict(x).cls; //Predicted class MqlTick ticks; if (!SymbolInfoTick(Symbol(), ticks)) { printf("Failed to obtain ticks information, Error = %d",GetLastError()); return; } double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); if (signal == 1 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade { if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL)) m_trade.Buy(volume_, Symbol(), ticks.ask,0,0); } if (signal == 0 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade { if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY)) m_trade.Sell(volume_, Symbol(), ticks.bid,0,0); } CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe }
Resultados de las pruebas.


Ahora se ve mucho mejor; se han abierto algunas operaciones, lo que refleja la escasez de estos patrones de velas en el marco temporal superior, tal y como se ha entrenado nuestro modelo en el marco temporal diario.
El porcentaje de operaciones rentables es del 54,55 %, lo cual se aproxima mucho a la precisión global de 0,53 (53 %) obtenida en el informe de clasificación; esta similitud indica que vamos por buen camino.
Conclusión
Por lo tanto, es posible utilizar patrones de velas japonesas al trabajar con modelos de Inteligencia Artificial (IA) y utilizar el resultado final para hacer predicciones en el mercado; sin embargo, a diferencia del uso de indicadores y cálculos matemáticos como cualquier dato típico que solemos utilizar para pronosticar los mercados, los patrones de velas japonesas requieren muchas consideraciones y una atención minuciosa a los pequeños detalles al recopilar los datos y crear características derivadas de las velas japonesas observables en el mercado. Una pequeña interpretación errónea de una vela podría conducir a un resultado muy diferente.
Se dice que nuestros deseos y creencias influyen en cómo percibimos e interpretamos la información. En otras palabras, vemos lo que queremos ver.
Creo que esto es lo que sucede principalmente al trabajar con patrones de velas japonesas: si buscas un martillo, un doji de libélula podría parecerse a un martillo y viceversa.
Creo que se requieren muchas pruebas y errores al preparar los datos basados en gráficos de velas japonesas para lograr un rendimiento óptimo al utilizar estos datos en el aprendizaje automático.
Saludos cordiales.
Fuentes y referencias
- What Is a Doji Candle Pattern, and What Does It Tell You?
- Hammer Candlestick: What It Is and How Investors Use It
- Shooting Star: What It Means in Stock Trading, With an Example
- What is a Spinning top candlestick pattern?
- Marubozu: What it Means, How it Works, Why it's Used
- The candlestick trading bible
Tabla de archivos adjuntos
| Nombre del archivo | Descripción/Uso |
|---|---|
| Experts\CandlestickPatterns AI-EA.mq5 | Un asesor experto (EA) que utiliza el modelo CatBoost para realizar predicciones basadas en patrones de velas japonesas. |
| Indicators\Candlestick Identifier.mq5 | Un indicador para mostrar patrones de velas japonesas en el gráfico. |
| Scripts\Candlestick Patterns Collect.mq5 | Un script para recopilar patrones de velas japonesas y guardar esta información en un archivo CSV. |
| Include\Catboost.mqh | Una biblioteca que contiene clases para cargar, inicializar e implementar el clasificador Catboost con el fin de realizar predicciones sobre el mercado. |
| Include\pandas.mqh | Módulo Pandas, similar a Python, para el almacenamiento y la manipulación de datos. |
| Include\ta-lib.mqh | Biblioteca de análisis técnico que contiene una clase para detectar patrones de velas japonesas. |
| Common\Files\*.csv | Archivos CSV que contienen datos de gráficos de velas para su uso en el aprendizaje automático. |
| Common\Files\*.onnx | Modelos de aprendizaje automático en formato ONNX. |
| CandlestickMarket Prediction.ipynb | Un script de Python (cuaderno Jupyter) para entrenar el modelo Catboost. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17832
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Utilizando redes neuronales en MetaTrader
Estudiamos la predicción conformal de series temporales financieras
Particularidades del trabajo con números del tipo double en MQL4
Características del Wizard MQL5 que debe conocer (Parte 60): Aprendizaje por inferencia (Wasserstein-VAE) con patrones de media móvil y oscilador estocástico
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Esto ya lo expliqué en el artículo.
Ahora mismo, sólo estamos considerando la detección de patrones de velas y sus señales basadas en su apariencia, pero la forma correcta de extraer las señales de una vela según mis fuentes, debe incluir la detección de la tendencia por ejemplo, para que un martillo sea considerado una señal alcista tiene que aparecer en una tendencia bajista.
La tendencia es una parte crucial de la ecuación que quizás quieras considerar si quieres llevar este proyecto más lejos.
La acción del precio es importante, eso no se puede negar.
Ya lo expliqué en el artículo.
Ahora mismo, sólo estamos considerando la detección de patrones de velas y sus señales basadas en su apariencia, pero la forma correcta de extraer las señales de una vela según mis fuentes, debe incluir la detección de la tendencia por ejemplo, para que un martillo sea considerado una señal alcista tiene que aparecer en una tendencia bajista.
La tendencia es una parte crucial de la ecuación que quizás quieras tener en cuenta si quieres llevar este proyecto más lejos.
La acción del precio es importante, eso no se puede negar.
Su cita no está relacionada con la idea principal de mi punto.
Hola proyecto ML estará en estas líneas.
Te deseo Feliz Navidad ...