English Русский 中文 Deutsch 日本語
preview
Explorando técnicas avanzadas de aprendizaje automático en la estrategia Darvas Box Breakout

Explorando técnicas avanzadas de aprendizaje automático en la estrategia Darvas Box Breakout

MetaTrader 5Trading |
35 2
Zhuo Kai Chen
Zhuo Kai Chen

Introducción

La estrategia Darvas Box Breakout, creada por Nicolas Darvas, es un enfoque técnico de negociación que detecta posibles señales de compra cuando el precio de una acción sube por encima de un rango establecido, lo que sugiere un fuerte impulso alcista. En este artículo, aplicaremos este concepto estratégico como ejemplo para explorar tres técnicas avanzadas de aprendizaje automático. Entre ellas se incluyen el uso de un modelo de aprendizaje automático para generar señales en lugar de filtrar operaciones, el empleo de señales continuas en lugar de discretas y el uso de modelos entrenados en diferentes marcos temporales para confirmar las operaciones. Estos métodos ofrecen nuevas perspectivas sobre cómo el aprendizaje automático puede mejorar el comercio algorítmico más allá de las prácticas tradicionales.

Este artículo profundizará en las características y la teoría que hay detrás de tres técnicas avanzadas que los formadores rara vez tratan, ya que son innovadoras en comparación con los métodos tradicionales. También ofrecerá información sobre temas avanzados como la ingeniería de características y el ajuste de hiperparámetros durante el proceso de entrenamiento del modelo. Sin embargo, no cubrirá en detalle todos los pasos del flujo de trabajo de entrenamiento del modelo de aprendizaje automático. Para los lectores que tengan curiosidad por conocer los procedimientos omitidos, consulten este enlace para ver el proceso de implementación completo.


Generación de señales

El aprendizaje automático consta de tres tipos principales: aprendizaje supervisado, aprendizaje no supervisado y aprendizaje por refuerzo. En el trading cuantitativo, los operadores utilizan principalmente el aprendizaje supervisado por encima de los demás por dos razones clave.
  1. El aprendizaje no supervisado suele ser demasiado básico para captar las complejas relaciones entre los resultados de las operaciones bursátiles y las características del mercado. Sin etiquetas, le cuesta alinearse con los objetivos de predicción y es más adecuado para predecir datos indirectos que los resultados directos de una estrategia comercial.
  2. El aprendizaje por refuerzo requiere la creación de un entorno de entrenamiento con una función de recompensa destinada a maximizar los beneficios a largo plazo, en lugar de centrarse en predicciones individuales precisas. Este enfoque implica una configuración complicada para la sencilla tarea de predecir resultados, lo que lo hace menos rentable para los comerciantes minoristas.

Aun así, el aprendizaje supervisado ofrece muchas aplicaciones en el comercio algorítmico. Un método habitual es utilizarlo como filtro: se empieza con una estrategia original que genera muchas muestras y, a continuación, se entrena un modelo para identificar cuándo es probable que la estrategia tenga éxito o fracase. El nivel de confianza del modelo ayuda a filtrar las malas operaciones que predice.

Otro enfoque, que exploraremos en este artículo, es el uso del aprendizaje supervisado para generar señales. Para tareas de regresión típicas, como predecir el precio, es sencillo: comprar cuando el modelo predice que el precio subirá y vender cuando predice una caída. Pero, ¿cómo combinamos esto con una estrategia básica como la de Darvas Box Breakout?

En primer lugar, desarrollaremos un EA para recopilar los datos de características y etiquetas necesarios para entrenar el modelo en Python más adelante.

La estrategia Darvas Box Breakout define un rango utilizando una serie de velas de rechazo tras un máximo o mínimo, lo que desencadena una operación cuando el precio sale de este rango. En cualquier caso, necesitamos una señal para empezar a recopilar datos sobre las características y predecir resultados futuros. Por lo tanto, estableceremos el desencadenante como el momento en que el precio rompa el rango inferior o superior. Esta función detecta si existe una caja Darvas para un periodo de retrospectiva y una cantidad de velas de confirmación determinados, asigna el valor del rango alto/bajo a las variables y traza la caja en el gráfico.

double high;
double low;
bool boxFormed = false;

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

A continuación se muestran algunos ejemplos de la caja de Darvas en un gráfico:

Caja Darvas

En comparación con su uso como filtro, este método tiene desventajas. Necesitaríamos predecir resultados equilibrados con probabilidades iguales, como si las próximas 10 barras serán más altas o más bajas, o si el precio subirá o bajará primero 10 pips. Otro inconveniente es que perdemos la ventaja inherente a una estrategia fundamental: La ventaja depende totalmente de la capacidad predictiva del modelo. En el lado positivo, no estás limitado por las muestras que proporciona una estrategia troncal solo cuando se activa, lo que te ofrece un tamaño de muestra inicial mayor y un mayor potencial alcista. Implementamos la lógica de negociación en la función OnTick() de la siguiente manera:

input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

Para esta estrategia, utilizar el mismo tamaño de take profit y stop loss es más coherente que realizar un seguimiento de los resultados de las siguientes 10 barras. El primero vincula nuestra predicción directamente con el beneficio final, mientras que el segundo añade incertidumbre con rendimientos variables en cada periodo de 10 barras. Cabe señalar que utilizamos el take profit y el stop loss como porcentaje del precio, lo que lo hace más adaptable a diferentes activos y más adecuado para activos con tendencia, como el oro o los índices. Los lectores pueden probar la alternativa quitando el take profit y el stop loss de la función de compra.

Para los datos de características utilizados para predecir los resultados, seleccioné los tres últimos rendimientos normalizados, la distancia normalizada desde el máximo y el mínimo del rango, y algunos indicadores estacionarios comunes. Almacenamos estos datos en una matriz múltiple, que luego se guarda en un archivo CSV utilizando la clase CFileCSV. Asegúrese de que todos los marcos temporales y símbolos estén configurados como se indica a continuación para poder cambiar fácilmente entre marcos temporales y activos.
string data[50000][12];
int indexx = 0;

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator


CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

El código final para el asesor experto de obtención de datos tendrá este aspecto:

#include <Trade/Trade.mqh>
CTrade trade;
#include <FileCSV.mqh>
CFileCSV csvFile;
string fileName = "box.csv";
string headers[] = {
    "Average Directional Movement Index", 
    "Average Directional Movement Index by Welles Wilder",  
    "DeMarker", 
    "Relative Strength Index", 
    "Relative Vigor Index", 
    "Stochastic Oscillator",
    "Stationary",
    "Box Size",
    "Stationary2",
    "Stationary3",
    "Distance High",
    "Distance Low"
};

input double lott = 0.01;
input int Magic = 0;
input int checkBar = 30;
input int lookBack = 100;
input int countMax = 10;
input double slp = 0.003;
input double tpp = 0.003;
input bool saveData = true;

string data[50000][12];
int indexx = 0;
int barsTotal = 0;
int count = 0;
double high;
double low;
bool boxFormed = false;
double lastClose;
double lastlastClose;

int handleAdx;     // Average Directional Movement Index - 3
int handleWilder;  // Average Directional Movement Index by Welles Wilder - 3
int handleDm;      // DeMarker - 1
int handleRsi;     // Relative Strength Index - 1
int handleRvi;     // Relative Vigor Index - 2
int handleSto;     // Stochastic Oscillator - 2

int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3
handleWilder=iADXWilder(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index by Welles Wilder - 3
handleDm=iDeMarker(_Symbol,PERIOD_CURRENT,14);//DeMarker - 1
handleRsi=iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);//Relative Strength Index - 1
handleRvi=iRVI(_Symbol,PERIOD_CURRENT,10);//Relative Vigor Index - 2
handleSto=iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_SMA,STO_LOWHIGH);//Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   if (!saveData) return;
   if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI))
     {
      //Write the header
      csvFile.WriteHeader(headers);
      //Write data rows
      csvFile.WriteLine(data);
      //Close the file
      csvFile.Close();
     }
   else
     {
      Print("File opening error!");
     }
  }

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax ){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); 
    }
 }

void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       trade.Buy(lott,_Symbol,ask,sl,tp);
       if(PositionsTotal()>0)getData();
}

bool DetectDarvasBox(int n = 100, int M = 3)
{
   // Clear previous Darvas box objects
   for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--)
   {
      string name = ObjectName(0, k);
      if (StringFind(name, "DarvasBox_") == 0)
         ObjectDelete(0, name);
   }
   bool current_box_active = false;
   // Start checking from the oldest bar within the lookback period
   for (int i = M+1; i <= n; i++)
   {
      // Get high of current bar and previous bar
      double high_current = iHigh(_Symbol, PERIOD_CURRENT, i);
      double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1);
      // Check for a new high
      if (high_current > high_prev)
      {
         // Check if the next M bars do not exceed the high
         bool pullback = true;
         for (int k = 1; k <= M; k++)
         {
            if (i - k < 0) // Ensure we don't go beyond available bars
            {
               pullback = false;
               break;
            }
            double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k);
            if (high_next > high_current)
            {
               pullback = false;
               break;
            }
         }

         // If pullback condition is met, define the box
         if (pullback)
         {
            double top = high_current;
            double bottom = iLow(_Symbol, PERIOD_CURRENT, i);

            // Find the lowest low over the bar and the next M bars
            for (int k = 1; k <= M; k++)
            {
               double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k);
               if (low_next < bottom)
                  bottom = low_next;
            }

            // Check for breakout from i - M - 1 to the current bar (index 0)
            int j = i - M - 1;
            while (j >= 0)
            {
               double close_j = iClose(_Symbol, PERIOD_CURRENT, j);
               if (close_j > top || close_j < bottom)
                  break; // Breakout found
               j--;
            }
            j++; // Adjust to the bar after breakout (or 0 if no breakout)

            // Create a unique object name
            string obj_name = "DarvasBox_" + IntegerToString(i);

            // Plot the box
            datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i);
            datetime time_end;
            if (j > 0)
            {
               // Historical box: ends at breakout
               time_end = iTime(_Symbol, PERIOD_CURRENT, j);
            }
            else
            {
               // Current box: extends to the current bar
               time_end = iTime(_Symbol, PERIOD_CURRENT, 0);
               current_box_active = true;
            }
            high = top;
            low = bottom;
            ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom);
            ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
            ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID);
            ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1);
            boxFormed = true;

            // Since we're only plotting the most recent box, break after finding it
            break;
         }
      }
   }

   return current_box_active;
}

void getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

//2 means 2 decimal places
data[indexx][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[indexx][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[indexx][2] = DoubleToString(dm[0], 2);       // DeMarker
data[indexx][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[indexx][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[indexx][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[indexx][6] = DoubleToString(stationary,2);
data[indexx][7] = DoubleToString(boxSize,2);
data[indexx][8] = DoubleToString(stationary2,2);
data[indexx][9] = DoubleToString(stationary3,2);
data[indexx][10] = DoubleToString(highDistance,2);
data[indexx][11] = DoubleToString(lowDistance,2);
indexx++;
}

Tenemos la intención de operar esta estrategia en el marco temporal de 15 minutos del XAUUSD debido a la sólida volatilidad del activo y porque 15 minutos logran un equilibrio entre la reducción del ruido y la generación de un mayor número de muestras. Una operación típica sería así:

Ejemplo de una operación

Utilizamos los datos de 2020-2024 como datos de entrenamiento y datos de validación, y más adelante comprobaremos el resultado en 2024-2025 en el terminal MetaTrader 5. Después de ejecutar este EA en el probador de estrategias, el archivo CSV se guardará en el directorio /Tester/Agent-sth000 tras la desinicialización del EA.

Además, haz clic con el botón derecho para obtener el informe de Excel de la prueba retrospectiva como este:

Informe de Excel

Tenga en cuenta el número de fila de la fila «Deals», que utilizaremos como entrada más adelante.

Localizar la fila

Después de eso, entrenamos nuestro modelo en Python.

El modelo que hemos seleccionado para este artículo es un modelo basado en árboles de decisión, ideal para problemas de clasificación, igual que el que utilizamos en este artículo.

import pandas as pd

# Replace 'your_file.xlsx' with the path to your file
input_file = 'box.xlsx'

# Load the Excel file and skip the first {skiprows} rows
data1 = pd.read_excel(input_file, skiprows=4417)

# Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions
profit_data = data1["Profit"][1:-1] 
profit_data = profit_data[profit_data.index % 2 == 0]  # Filter for rows with odd indices
profit_data = profit_data.reset_index(drop=True)  # Reset index
# Convert to float, then apply the condition to set values to 1 if > 0, otherwise to 0
profit_data = pd.to_numeric(profit_data, errors='coerce').fillna(0)  # Convert to float, replacing NaN with 0
profit_data = profit_data.apply(lambda x: 1 if x > 0 else 0)  # Apply condition

# Load the CSV file with semicolon separator
file_path = 'box.csv'
data2 = pd.read_csv(file_path, sep=';')

# Drop rows with any missing or incomplete values
data2.dropna(inplace=True)

# Drop any duplicate rows if present
data2.drop_duplicates(inplace=True)

# Convert non-numeric columns to numerical format
for col in data2.columns:
    if data2[col].dtype == 'object':
        # Convert categorical to numerical using label encoding
        data2[col] = data2[col].astype('category').cat.codes

# Ensure all remaining columns are numeric and cleanly formatted for CatBoost
data2 = data2.apply(pd.to_numeric, errors='coerce')
data2.dropna(inplace=True)  # Drop any rows that might still contain NaNs after conversion

# Merge the two DataFrames on the index
merged_data = pd.merge(profit_data, data2, left_index=True, right_index=True, how='inner')

# Save the merged data to a new CSV file
output_csv_path = 'merged_data.csv'
merged_data.to_csv(output_csv_path)

print(f"Merged data saved to {output_csv_path}")
Utilizamos este código para etiquetar el informe de Excel, asignando un 1 a las operaciones con beneficios positivos y un 0 a las que no lo tienen. A continuación, lo combinamos con los datos de características recopilados del archivo CSV del EA de obtención de datos. Ten en cuenta que el valor "skiprows" coincide con el número de fila de «Deals».
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings("ignore")
data = pd.read_csv("merged_data.csv",index_col=0)

XX = data.drop(columns=['Profit'])
yy = data['Profit']
y = yy.values
X = XX.values
pd.DataFrame(X,y)

A continuación, asignamos la matriz de etiquetas a la variable y y el marco de datos de características a la variable X.

import numpy as np
import pandas as pd
import warnings
import seaborn as sns
warnings.filterwarnings("ignore")
from sklearn.model_selection import train_test_split
import catboost as cb
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Split data
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, shuffle=False)

# Identify categorical features
cat_feature_indices = [i for i, col in enumerate(XX.columns) if XX[col].dtype == 'object']

# Train CatBoost classifier
model = cb.CatBoostClassifier(   
    iterations=5000,             # Number of trees (similar to n_estimators)
    learning_rate=0.02,          # Learning rate
    depth=5,                    # Depth of each tree
    l2_leaf_reg=5,
    bagging_temperature=1,
    early_stopping_rounds=50,
    loss_function='Logloss',    # Use 'MultiClass' if it's a multi-class problem
    verbose=1000)
model.fit(X_train, y_train, cat_features=cat_feature_indices)

A continuación, dividimos los datos en una proporción de 9:1 en conjuntos de entrenamiento y validación, y comenzamos a entrenar el modelo. La función de división entrenamiento-prueba de sklearn activa la mezcla de datos por defecto, y eso no es adecuado para series temporales. Para evitar distorsiones, conviene desactivar la mezcla usando "shuffle=False" en los parámetros. Es recomendable ajustar los hiperparámetros para evitar el sobreajuste o el subajuste, dependiendo del tamaño de la muestra. Personalmente, he descubierto que detener la iteración alrededor de una pérdida logarítmica de 0,1 funciona bien.

import numpy as np
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# Assuming you already have y_test, X_test, and model defined
# Predict probabilities
y_prob = model.predict_proba(X_test)[:, 1]  # Probability for positive class

# Compute ROC curve and AUC (for reference)
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc_score = roc_auc_score(y_test, y_prob)
print(f"AUC Score: {auc_score:.2f}")

# Define confidence thresholds to test (e.g., 50%, 60%, 70%, etc.)
confidence_thresholds = np.arange(0.5, 1.0, 0.05)  # From 50% to 95% in steps of 5%
accuracies = []
coverage = []  # Fraction of samples classified at each threshold

for thresh in confidence_thresholds:
    # Classify only when probability is >= thresh (positive) or <= (1 - thresh) (negative)
    y_pred_confident = np.where(y_prob >= thresh, 1, np.where(y_prob <= (1 - thresh), 0, -1))
    
    # Filter out unclassified samples (where y_pred_confident == -1)
    mask = y_pred_confident != -1
    y_test_confident = y_test[mask]
    y_pred_confident = y_pred_confident[mask]
    
    # Calculate accuracy and coverage
    if len(y_test_confident) > 0:  # Avoid division by zero
        acc = np.mean(y_pred_confident == y_test_confident)
        cov = len(y_test_confident) / len(y_test)
    else:
        acc = 0
        cov = 0
    
    accuracies.append(acc)
    coverage.append(cov)

# Plot Accuracy vs Confidence Threshold
plt.figure(figsize=(10, 6))
plt.plot(confidence_thresholds, accuracies, marker='o', label='Accuracy', color='blue')
plt.plot(confidence_thresholds, coverage, marker='s', label='Coverage', color='green')
plt.xlabel('Confidence Threshold')
plt.ylabel('Metric Value')
plt.title('Accuracy and Coverage vs Confidence Threshold')
plt.legend(loc='best')
plt.grid(True)
plt.show()

# Also show the original ROC curve for reference
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {auc_score:.2f})', color='blue')
plt.plot([0, 1], [0, 1], 'k--', label='Random Guess')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver Operating Characteristic (ROC) Curve')
plt.legend(loc='lower right')
plt.grid(True)
plt.show()

A continuación, trazamos las visualizaciones de los resultados para comprobar la prueba de validación. En el enfoque típico de entrenamiento-validación-prueba, la etapa de validación ayuda a seleccionar los mejores hiperparámetros y evalúa inicialmente si el modelo entrenado tiene capacidad predictiva. Actúa como buffer antes de pasar a la prueba final.

Umbral de confianza de precisión

Puntuación AUC

Aquí observamos que la puntuación AUC está por encima de 0,5 y que la precisión mejora a medida que aumentamos el umbral de confianza, lo que suele ser una señal positiva. Si estas dos métricas no coinciden, no se preocupe, intente ajustar primero los hiperparámetros antes de descartar el modelo por completo.
# Feature importance
feature_importance = model.get_feature_importance()
importance_df = pd.DataFrame({
    'feature': XX.columns,
    'importance': feature_importance
}).sort_values('importance', ascending=False)
print("Feature Importances:")
print(importance_df)
plt.figure(figsize=(10, 6))
sns.barplot(x='importance', y='feature', data=importance_df)
plt.title(' Feature Importance')
plt.xlabel('Importance')
plt.ylabel('Feature')
x = 100/len(XX.columns)
plt.axvline(x,color = 'red', linestyle = '--')
plt.show()

Importancia de las características

Este bloque de código trazará la importancia de las características, así como la línea media. Hay muchas formas de definir la importancia de las características en el campo del aprendizaje automático, como por ejemplo:

  1. Importancia basada en árboles: Mide la reducción de impurezas (por ejemplo, Gini) en árboles de decisión o conjuntos como Random Forest y XGBoost.
  2. Importancia de la permutación: Evalúa la caída del rendimiento cuando se barajan los valores de una característica.
  3. Valores SHAP: Calcula la contribución de una característica a las predicciones basándose en los valores de Shapley.
  4. Magnitud del coeficiente: Utiliza el valor absoluto de los coeficientes en modelos lineales.

En nuestro ejemplo utilizamos CatBoost, un modelo basado en árboles de decisión. La importancia de las características muestra cuánto desorden (impureza) reduce cada característica cuando se utiliza para dividir el árbol de decisión en los datos de la muestra. Es fundamental tener en cuenta que, aunque seleccionar las características más importantes como conjunto final a menudo puede aumentar la eficiencia del modelo, no siempre mejora la previsibilidad por las siguientes razones:

  • La importancia de las características se calcula a partir de los datos de la muestra, sin tener en cuenta los datos fuera de la muestra.
  • La importancia de una característica depende de las demás características que se estén considerando. Si la mayoría de las características elegidas carecen de capacidad predictiva, eliminar las más débiles no servirá de nada.
  • La importancia refleja la eficacia con la que una característica divide el árbol, no necesariamente lo crucial que es para el resultado final de la decisión.

Estas ideas me llamaron la atención cuando descubrí inesperadamente que seleccionar las características menos importantes aumentaba la precisión fuera de la muestra. Pero, en general, elegir las características más importantes y eliminar las menos importantes ayuda a aligerar el modelo y probablemente mejora la precisión en general.

from onnx.helper import get_attribute_value
import onnxruntime as rt
from skl2onnx import convert_sklearn, update_registered_converter
from skl2onnx.common.shape_calculator import (
    calculate_linear_classifier_output_shapes,
)  # noqa
from skl2onnx.common.data_types import (
    FloatTensorType,
    Int64TensorType,
    guess_tensor_type,
)
from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name
from catboost import CatBoostClassifier
from catboost.utils import convert_to_onnx_object

def skl2onnx_parser_castboost_classifier(scope, model, inputs, custom_parsers=None):
    
    options = scope.get_options(model, dict(zipmap=True))
    no_zipmap = isinstance(options["zipmap"], bool) and not options["zipmap"]

    alias = _get_sklearn_operator_name(type(model))
    this_operator = scope.declare_local_operator(alias, model)
    this_operator.inputs = inputs

    label_variable = scope.declare_local_variable("label", Int64TensorType())
    prob_dtype = guess_tensor_type(inputs[0].type)
    probability_tensor_variable = scope.declare_local_variable(
        "probabilities", prob_dtype
    )
    this_operator.outputs.append(label_variable)
    this_operator.outputs.append(probability_tensor_variable)
    probability_tensor = this_operator.outputs

    if no_zipmap:
        return probability_tensor

    return _apply_zipmap(
        options["zipmap"], scope, model, inputs[0].type, probability_tensor
    )

def skl2onnx_convert_catboost(scope, operator, container):
    """
    CatBoost returns an ONNX graph with a single node.
    This function adds it to the main graph.
    """
    onx = convert_to_onnx_object(operator.raw_operator)
    opsets = {d.domain: d.version for d in onx.opset_import}
    if "" in opsets and opsets[""] >= container.target_opset:
        raise RuntimeError("CatBoost uses an opset more recent than the target one.")
    if len(onx.graph.initializer) > 0 or len(onx.graph.sparse_initializer) > 0:
        raise NotImplementedError(
            "CatBoost returns a model initializers. This option is not implemented yet."
        )
    if (
        len(onx.graph.node) not in (1, 2)
        or not onx.graph.node[0].op_type.startswith("TreeEnsemble")
        or (len(onx.graph.node) == 2 and onx.graph.node[1].op_type != "ZipMap")
    ):
        types = ", ".join(map(lambda n: n.op_type, onx.graph.node))
        raise NotImplementedError(
            f"CatBoost returns {len(onx.graph.node)} != 1 (types={types}). "
            f"This option is not implemented yet."
        )
    node = onx.graph.node[0]
    atts = {}
    for att in node.attribute:
        atts[att.name] = get_attribute_value(att)
    container.add_node(
        node.op_type,
        [operator.inputs[0].full_name],
        [operator.outputs[0].full_name, operator.outputs[1].full_name],
        op_domain=node.domain,
        op_version=opsets.get(node.domain, None),
        **atts,
    )

update_registered_converter(
    CatBoostClassifier,
    "CatBoostCatBoostClassifier",
    calculate_linear_classifier_output_shapes,
    skl2onnx_convert_catboost,
    parser=skl2onnx_parser_castboost_classifier,
    options={"nocl": [True, False], "zipmap": [True, False, "columns"]},
)
model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

# And save.
with open("box2024.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

Por último, exportamos el archivo ONNX y lo guardamos en el directorio MQL5/Files.

Ahora, volvamos al editor de código MetaTrader 5 para crear el EA.

Solo tenemos que modificar el EA original de obtención de datos importando algunos archivos de inclusión para gestionar el modelo CatBoost.

#resource "\\Files\\box2024.onnx" as uchar catboost_onnx[]
#include <CatOnnx.mqh>
CCatBoost cat_boost;
string data[1][12];
vector xx;
vector prob;

A continuación, ajustaremos la función getData() para que devuelva un vector.

vector getData(){
double close = iClose(_Symbol,PERIOD_CURRENT,1);
double close2 = iClose(_Symbol,PERIOD_CURRENT,2);
double close3 = iClose(_Symbol,PERIOD_CURRENT,3);
double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close;
double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2;
double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3;
double highDistance = 1000*(close-high)/close;
double lowDistance = 1000*(close-low)/close;
double boxSize = 1000*(high-low)/close;
double adx[];       // Average Directional Movement Index
double wilder[];    // Average Directional Movement Index by Welles Wilder
double dm[];        // DeMarker
double rsi[];       // Relative Strength Index
double rvi[];       // Relative Vigor Index
double sto[];       // Stochastic Oscillator

CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
data[0][6] = DoubleToString(stationary,2);
data[0][7] = DoubleToString(boxSize,2);
data[0][8] = DoubleToString(stationary2,2);
data[0][9] = DoubleToString(stationary3,2);
data[0][10] = DoubleToString(highDistance,2);
data[0][11] = DoubleToString(lowDistance,2);

vector features(12);    
   for(int i = 0; i < 12; i++)
    {
      features[i] = StringToDouble(data[0][i]);
    }
    return features;
}

La lógica de negociación final en la función OnTick() tendrá el siguiente aspecto:

void OnTick()
  {
  int bars = iBars(_Symbol,PERIOD_CURRENT);

  if (barsTotal!= bars){
     barsTotal = bars;
     boxFormed = false;
     bool NotInPosition = true;
 
     lastClose = iClose(_Symbol, PERIOD_CURRENT, 1);
     lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2);
     
     for(int i = 0; i<PositionsTotal(); i++){
         ulong pos = PositionGetTicket(i);
         string symboll = PositionGetSymbol(i);
         if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;}
            /*count++;
            if(count >=countMax){
              trade.PositionClose(pos);  
              count = 0;}
            }}*/
     DetectDarvasBox(lookBack,checkBar);
     if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        prob = cat_boost.predict_proba(xx);
        if(prob[1]>threshold)executeBuy(); 
        if(prob[0]>threshold)executeSell();
        }
    }
 }

En la lógica de la señal, primero comprueba que no haya ninguna posición abierta en ese momento para garantizar que solo se realice una operación a la vez. A continuación, detecta si hay una ruptura en cualquiera de los lados del rango. A continuación, llama a la función getData() para obtener el vector de características. Este vector se pasa al modelo CatBoost como entrada, y el modelo genera la confianza de predicción para cada resultado en la matriz prob. Basándonos en los niveles de confianza para cada resultado, realizamos una apuesta comercial sobre el resultado previsto. Básicamente, estamos utilizando el modelo para generar las señales de compra o venta.

Realizamos una prueba retrospectiva en el Probador de estrategias de MetaTrader 5 utilizando datos de muestra de 2020 a 2024 para verificar que nuestros datos de entrenamiento no tuvieran errores y que la fusión de características y resultados fuera correcta. Si todo es correcto, la curva de capital debería tener un aspecto casi perfecto, como este:

En la muestra

A continuación, realizamos una prueba retrospectiva de la prueba fuera de muestra de 2024 a 2025 para ver si la estrategia es rentable en el período más reciente. Establecemos el umbral en 0,7, por lo que el modelo solo realizará una operación en una dirección si el nivel de confianza de que se alcance el beneficio de esa dirección es del 70 % o superior, según los datos de entrenamiento.

Configuración de la prueba retrospectiva

Parámetros

Curva de la equidad

Resultado

Podemos observar que el modelo tuvo un rendimiento excepcional durante la primera mitad del año, pero comenzó a rendir por debajo de lo esperado a medida que pasaba el tiempo. Esto es habitual en los modelos de aprendizaje automático, ya que la ventaja obtenida a partir de datos anteriores suele ser temporal y tiende a erosionarse con el tiempo. Esto sugiere que una proporción menor entre pruebas y entrenamiento podría funcionar mejor para futuras implementaciones de operaciones en vivo. En general, el modelo muestra cierta previsibilidad, ya que siguió siendo rentable incluso después de tener en cuenta los costes de negociación.


Señal continua

En el trading algorítmico, los traders suelen ceñirse a un método sencillo que consiste en utilizar señales discrecionales, ya sea comprar o vender con un riesgo fijo por operación. Esto facilita el manejo y mejora el análisis del rendimiento de la estrategia. Algunos operadores han intentado perfeccionar este método de señales discrecionales utilizando una señal aditiva, en la que ajustan el riesgo de la operación en función del grado de cumplimiento de las condiciones de la estrategia. Las señales continuas llevan este enfoque aditivo más allá, aplicándolo a condiciones estratégicas más abstractas y generando un nivel de riesgo entre cero y uno.

La idea básica detrás de esto es que no todas las operaciones que cumplen los criterios de entrada son iguales. Algunos parecen tener más probabilidades de tener éxito porque sus señales son más fuertes, basadas en reglas no lineales vinculadas a la estrategia. Esto puede verse como una herramienta de gestión de riesgos: apueste en grande cuando la confianza es alta y reduzca la inversión cuando una operación parezca menos prometedora, incluso si todavía tiene un retorno esperado positivo. Sin embargo, debemos recordar que esto agrega otro factor al desempeño de la estrategia, y que el sesgo de anticipación y los riesgos de sobreajuste siguen siendo problemáticos si no tenemos cuidado durante la implementación.

Para aplicar este concepto en nuestro EA, primero debemos ajustar las funciones de compra/venta para calcular el tamaño del lote en función del riesgo que estamos dispuestos a perder si se alcanza el stop loss. La función de cálculo de lote se ve así:  

double calclots(double slpoints, string symbol, double risk)
{  
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskAmount = balance* risk / 100;

   double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickvalue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   double lotstep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);

   double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
   double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
   lots = MathMin(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX));
   lots = MathMax(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN));
   return lots;
}
Luego actualizamos las funciones de compra/venta para que llamen a esta función calclots() y tomen el multiplicador de riesgo como entrada:  
void executeSell(double riskMultiplier) {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = lastClose*(1+slp);
       double tp = lastClose*(1-tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Sell(lots,_Symbol,bid,sl,tp);  
       }

void executeBuy(double riskMultiplier) {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = lastClose*(1-slp);
       double tp = lastClose*(1+tpp);
       double lots = 0.1;
       lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier);
       trade.Buy(lots,_Symbol,ask,sl,tp);
}
Dado que nuestro modelo de aprendizaje automático ya genera el nivel de confianza, podemos usarlo directamente como entrada del multiplicador de riesgo. Si queremos ajustar cuánto afecta el nivel de confianza al riesgo de cada operación, podemos simplemente aumentar o disminuir el nivel de confianza según sea necesario.
if(prob[1]>threshold)executeBuy(prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]);

Por ejemplo, si queremos amplificar la significancia de las diferencias en los niveles de confianza, podríamos multiplicar la probabilidad por sí misma tres veces. Esto aumentaría la diferencia de proporción entre probabilidades, haciendo más pronunciado el impacto de los niveles de confianza.

if(prob[1]>threshold)executeBuy(prob[1]*prob[1]*prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]*prob[0]*prob[0]);

Ahora, intentamos ver el resultado en la prueba retrospectiva. 

Configuración de la prueba retrospectiva

Curva de equidad

Resultado

Las operaciones realizadas siguen siendo las mismas que en la versión de señal discrecional, pero el factor de beneficio y el ratio Sharpe mejoraron ligeramente. Esto sugiere que, en este escenario específico, la señal continua mejoró el rendimiento general de la prueba fuera de la muestra, que está libre de sesgo de anticipación, ya que solo realizamos una prueba. Sin embargo, es importante señalar que este enfoque solo supera al método de riesgo fijo si la precisión de la predicción del modelo es mayor cuando su nivel de confianza es mayor. De lo contrario, el enfoque original de riesgo fijo podría ser mejor. Además, dado que hemos reducido el tamaño medio de las posiciones aplicando multiplicadores de riesgo entre cero y uno, tendríamos que aumentar el valor de la variable de riesgo si queremos obtener un beneficio total similar al anterior.


Validación en múltiples marcos temporales

Entrenar modelos de aprendizaje automático independientes, cada uno con un marco temporal diferente de características para predecir el mismo resultado, puede ser una forma muy eficaz de mejorar el filtrado de operaciones y la generación de señales. Al tener un modelo centrado en datos a corto plazo, otro en datos a medio plazo y quizás un tercero en tendencias a largo plazo, se obtienen conocimientos especializados que, al combinarlos, pueden validar las predicciones de forma más fiable que un único modelo. Este enfoque multimodelo puede aumentar la confianza en las decisiones comerciales al verificar las señales, reduciendo el riesgo de actuar basándose en el ruido específico de un marco temporal, y respalda la gestión de riesgos al permitirle sopesar los resultados de cada modelo para ajustar el tamaño de la operación o los stops en función de la fuerza del consenso.

Por otro lado, esta estrategia puede complicar el sistema, especialmente cuando se asignan diferentes pesos a las predicciones de múltiples modelos. Esto podría introducir sesgos o errores propios si no se ajusta cuidadosamente. Cada modelo también podría ajustarse excesivamente a su marco temporal específico, pasando por alto la dinámica general del mercado, y las discrepancias entre sus predicciones podrían crear confusión, retrasar las decisiones o socavar la confianza. 

Este enfoque se basa en dos supuestos clave: No se introduce ningún sesgo de anticipación en el marco temporal superior (debemos utilizar el valor de la última barra, no el actual) y el modelo del marco temporal superior tiene su propia previsibilidad (funciona mejor que las conjeturas aleatorias en las pruebas fuera de la muestra).  

Para implementar esto, primero modificamos el código en el EA de obtención de datos cambiando todos los marcos temporales relacionados con la extracción de características a un marco temporal superior, como 1 hora. Esto incluye indicadores, cálculos de precios y cualquier otra función utilizada.

int OnInit()
{
   trade.SetExpertMagicNumber(Magic);
   handleAdx = iADX(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index - 3
   handleWilder = iADXWilder(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index by Welles Wilder - 3
   handleDm = iDeMarker(_Symbol, PERIOD_H1, 14); // DeMarker - 1
   handleRsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE); // Relative Strength Index - 1
   handleRvi = iRVI(_Symbol, PERIOD_H1, 10); // Relative Vigor Index - 2
   handleSto = iStochastic(_Symbol, PERIOD_H1, 5, 3, 3, MODE_SMA, STO_LOWHIGH); // Stochastic Oscillator - 2
   return(INIT_SUCCEEDED);
}

void getData()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
}

Después de esto, seguimos los mismos pasos que antes: Obtener los datos, entrenar el modelo y exportarlo, tal y como hemos comentado anteriormente.

A continuación, en el EA, creamos una segunda función para obtener la entrada de características, que alimentaremos al segundo modelo de aprendizaje automático que hemos importado para obtener el nivel de confianza.

vector getData2()
{
   double close = iClose(_Symbol, PERIOD_H1, 1);
   double close2 = iClose(_Symbol, PERIOD_H1, 2);
   double close3 = iClose(_Symbol, PERIOD_H1, 3);
   double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close;
   double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2;
   double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3;
   double highDistance = 1000 * (close - high) / close;
   double lowDistance = 1000 * (close - low) / close;
   double boxSize = 1000 * (high - low) / close;
   double adx[];       // Average Directional Movement Index
   double wilder[];    // Average Directional Movement Index by Welles Wilder
   double dm[];        // DeMarker
   double rsi[];       // Relative Strength Index
   double rvi[];       // Relative Vigor Index
   double sto[];       // Stochastic Oscillator

   CopyBuffer(handleAdx, 0, 1, 1, adx);         // Average Directional Movement Index
   CopyBuffer(handleWilder, 0, 1, 1, wilder);   // Average Directional Movement Index by Welles Wilder
   CopyBuffer(handleDm, 0, 1, 1, dm);           // DeMarker
   CopyBuffer(handleRsi, 0, 1, 1, rsi);         // Relative Strength Index
   CopyBuffer(handleRvi, 0, 1, 1, rvi);         // Relative Vigor Index
   CopyBuffer(handleSto, 0, 1, 1, sto);         // Stochastic Oscillator

   data[0][0] = DoubleToString(adx[0], 2);      // Average Directional Movement Index
   data[0][1] = DoubleToString(wilder[0], 2);   // Average Directional Movement Index by Welles Wilder
   data[0][2] = DoubleToString(dm[0], 2);       // DeMarker
   data[0][3] = DoubleToString(rsi[0], 2);     // Relative Strength Index
   data[0][4] = DoubleToString(rvi[0], 2);     // Relative Vigor Index
   data[0][5] = DoubleToString(sto[0], 2);     // Stochastic Oscillator
   data[0][6] = DoubleToString(stationary, 2);
   data[0][7] = DoubleToString(boxSize, 2);
   data[0][8] = DoubleToString(stationary2, 2);
   data[0][9] = DoubleToString(stationary3, 2);
   data[0][10] = DoubleToString(highDistance, 2);
   data[0][11] = DoubleToString(lowDistance, 2);

   vector features(12);    
   for(int i = 0; i < 12; i++)
   {
      features[i] = StringToDouble(data[0][i]);
   }
   return features;
}

Supongamos que queremos asignar el mismo peso a los resultados de los dos modelos. Simplemente tomamos el promedio de sus resultados y lo consideramos como el resultado único que utilizamos anteriormente.

if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){
        xx = getData();
        xx2 = getData2();
        prob = cat_boost.predict_proba(xx);
        prob2 = cat_boost.predict_proba(xx2);
        double probability_buy = (prob[1]+prob2[1])/2;
        double probability_sell = (prob[0]+prob2[0])/2;

        if(probability_buy>threshold)executeBuy(probability_buy); 
        if(probability_sell>threshold)executeSell(probability_sell);
        }
    }

Una vez calculadas estas dos variables como se ha indicado anteriormente, ahora podemos combinarlas en un único nivel de confianza y utilizarlo para la validación, siguiendo el mismo enfoque que hemos utilizado anteriormente.


Conclusión

En este artículo, primero exploramos la idea de utilizar un modelo de aprendizaje automático como generador de señales en lugar de como filtro, lo cual demostramos mediante una estrategia de ruptura de la caja de Darvas. Repasamos brevemente el proceso de entrenamiento del modelo y discutimos la importancia de los umbrales de nivel de confianza y la significación de las características. A continuación, introdujimos el concepto de señales continuas y comparamos su rendimiento con el de las señales discrecionales. Descubrimos que, en este ejemplo, las señales continuas mejoraban el rendimiento de las pruebas retrospectivas porque el modelo tendía a tener una mayor precisión de predicción a medida que aumentaban los niveles de confianza. Por último, hablamos del plan para utilizar varios modelos de aprendizaje automático entrenados en diferentes marcos temporales para validar las señales de forma conjunta.  

En general, este artículo tenía como objetivo presentar ideas poco convencionales sobre la aplicación de modelos de aprendizaje automático en el aprendizaje supervisado para el comercio CTA. Su objetivo no es afirmar de manera definitiva qué enfoque funciona mejor, ya que todo depende del escenario específico, sino inspirar a los lectores a pensar de forma creativa y ampliar conceptos iniciales sencillos. Al fin y al cabo, nada es completamente nuevo: la innovación suele surgir de la combinación de ideas existentes para crear algo nuevo.

Tabla de archivos

Nombre del archivo Uso del archivo
Darvas_Box.ipynb El archivo Jupyter Notebook para entrenar el modelo de aprendizaje automático.
Darvas Box Data.mq5 EA para obtener datos para el entrenamiento del modelo.
Darvas Box EA.mq5 EA negociador del artículo.
CatOnnx.mqh Archivo de inclusión para procesar el modelo CatBoost.
FileCSV.mqh Archivo de inclusión para guardar datos en CSV.

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17466

Archivos adjuntos |
Darvas_ML.zip (136.61 KB)
linfo2
linfo2 | 24 mar 2025 en 00:42
Gracias Zhou por el interesante artículo y ejemplos de código . para mí tuve que instalar manualmente algunos de los componentes de Python para que funcione. lo que puede ayudar a otros usuarios !pip install catboost!pip install onnxruntime !pip install skl2onnx. al finalizar puedo probar . pero si trato de cargar el EA relacionado , he devuelto 'Failed to set the Output[1] shape Err=5802. No estoy seguro de dónde viene esto o si es importante y yo soy incapaz de averiguar de dónde viene . . la documentación dice ERR_ONNX_NOT_SUPPORTED

5802

Propiedad o valor no soportado por MQL5 , esto es seguido por el mensaje ONNX Modelo Inicializado ? tiene alguna sugerencia
Zhuo Kai Chen
Zhuo Kai Chen | 24 mar 2025 en 01:09
linfo2 catboost!pip install onnxruntime !pip install skl2onnx. al finalizar puedo probar . pero si trato de cargar el EA relacionado , he devuelto 'Failed to set the Output[1] shape Err=5802. No estoy seguro de dónde viene esto o si es importante y yo soy incapaz de averiguar de dónde viene . . la documentación dice ERR_ONNX_NOT_SUPPORTED

5802

Propiedad o valor no soportado por MQL5 , esto es seguido por el mensaje ONNX Modelo Inicializado ? tiene alguna sugerencia

Gracias por recordármelo. La parte de instalación pip fue ignorada, pero los usuarios tienen que instalar la biblioteca relacionada si no lo han hecho ya.

Su error puede deberse a que las dimensiones utilizadas en el entrenamiento de su modelo son diferentes a las utilizadas en su EA. Por ejemplo, si ha entrenado un modelo con 5 características, debería introducir también 5 características en su EA, no 4 ó 6. Encontrará un tutorial más detallado en este artículo. Espero que esto le ayude. Si no, por favor proporcione más contexto.

Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Algoritmo de optimización de neuroboides 2 — Neuroboids Optimization Algorithm 2 (NOA2) Algoritmo de optimización de neuroboides 2 — Neuroboids Optimization Algorithm 2 (NOA2)
El nuevo algoritmo de optimización de autor, NOA2 (Neuroboids Optimisation Algorithm 2), combina los principios de la inteligencia de enjambre con el control neuronal. El NOA2 combina la mecánica del comportamiento de los enjambres de neuroboids con un sistema neuronal adaptativo que permite a los agentes ajustar de forma autónoma su comportamiento a medida que buscan un óptimo. El algoritmo se está desarrollando activamente y muestra potencial para resolver problemas complejos de optimización.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Automatización de estrategias de trading en MQL5 (Parte 12): Implementación de la estrategia Mitigation Order Blocks (MOB) Automatización de estrategias de trading en MQL5 (Parte 12): Implementación de la estrategia Mitigation Order Blocks (MOB)
En este artículo creamos un sistema de trading en MQL5 que se encarga de detectar de forma automática los "order blocks", un concepto utilizado en el método Smart Money. Describimos las reglas de la estrategia, implementamos la lógica en MQL5 e integramos la gestión de riesgos para una ejecución eficaz de las operaciones. Por último, realizamos pruebas retrospectivas del sistema para evaluar su rendimiento y perfeccionarlo con el fin de obtener resultados óptimos.