English Русский 中文 Español Deutsch 日本語
preview
Explorando Técnicas Avançadas de Aprendizado de Máquina na Estratégia de Rompimento da Caixa de Darvas

Explorando Técnicas Avançadas de Aprendizado de Máquina na Estratégia de Rompimento da Caixa de Darvas

MetaTrader 5Negociação |
25 2
Zhuo Kai Chen
Zhuo Kai Chen

Introdução

A estratégia de rompimento da Caixa de Darvas, criada por Nicolas Darvas, é uma abordagem de negociação técnica que identifica potenciais sinais de compra quando o preço de uma ação sobe acima de um intervalo definido de "caixa", sugerindo forte momentum de alta. Neste artigo, aplicaremos esse conceito de estratégia como exemplo para explorar três técnicas avançadas de aprendizado de máquina. Estas incluem usar um modelo de aprendizado de máquina para gerar sinais em vez de filtrar negociações, empregar sinais contínuos em vez de discretos, e utilizar modelos treinados em diferentes períodos gráficos para confirmar negociações. Esses métodos oferecem novas perspectivas sobre como o aprendizado de máquina pode aprimorar o trading algorítmico além das práticas tradicionais.

Este artigo se aprofundará nas características e na teoria por trás de três técnicas avançadas que educadores raramente abordam, pois são inovadoras em comparação com métodos tradicionais. Também oferecerá insights sobre tópicos avançados como engenharia de atributos e ajuste de hiperparâmetros durante o processo de treinamento do modelo. No entanto, não cobrirá todas as etapas do fluxo de trabalho de treinamento de modelo de aprendizado de máquina em detalhe. Para leitores curiosos sobre os procedimentos omitidos, consulte este link do artigo para o processo completo de implementação.


Geração de Sinais

O aprendizado de máquina consiste em três tipos principais: aprendizado supervisionado, aprendizado não supervisionado e aprendizado por reforço. No trading quantitativo, os traders utilizam principalmente o aprendizado supervisionado em relação aos outros por duas razões principais.
  1. O aprendizado não supervisionado frequentemente é básico demais para capturar as relações complexas entre os resultados de negociação e as características do mercado. Sem rótulos, ele tem dificuldade em se alinhar aos objetivos de previsão e é mais adequado para prever dados indiretos em vez dos resultados diretos de uma estratégia de negociação.
  2. O aprendizado por reforço exige a configuração de um ambiente de treinamento com uma função de recompensa voltada para maximizar o lucro de longo prazo, em vez de focar em previsões individuais precisas. Essa abordagem envolve uma configuração complexa para a tarefa simples de prever resultados, tornando-a menos custo-efetiva para traders de varejo.

Ainda assim, o aprendizado supervisionado oferece muitas aplicações no trading algorítmico. Um método comum é utilizá-lo como filtro: você começa com uma estratégia original que gera muitas amostras, depois treina um modelo para identificar quando a estratégia provavelmente terá sucesso ou falhará. O nível de confiança do modelo ajuda a filtrar as negociações ruins que ele prevê.

Outra abordagem, que exploraremos neste artigo, é usar aprendizado supervisionado para gerar sinais. Para tarefas típicas de regressão como previsão de preço, é simples—comprar quando o modelo prevê que o preço subirá, vender quando prevê uma queda. Mas como combinamos isso com uma estratégia base como a Breakout da Caixa de Darvas?

Primeiro, desenvolveremos um EA para coletar os dados de atributos necessários e os dados de rótulos para treinar o modelo posteriormente em Python.

A Estratégia de Breakout da Caixa de Darvas define uma caixa usando uma série de velas de rejeição após uma máxima ou mínima, acionando uma negociação quando o preço rompe esse intervalo. De qualquer forma, precisamos de um sinal para começar a coletar dados de atributos e prever resultados futuros. Assim, definiremos o gatilho como o momento em que o preço rompe o limite inferior ou superior. Esta função detecta se existe uma caixa de Darvas para um determinado período de look back e quantidade de velas de confirmação, atribui o valor do intervalo de máxima/mínima às variáveis e plota a caixa no 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;
}

Aqui estão alguns exemplos da caixa de Darvas em um gráfico:

Caixa de Darvas

Comparado ao uso como filtro, este método tem desvantagens. Precisaríamos prever resultados equilibrados com probabilidades iguais, como se as próximas 10 barras serão mais altas ou mais baixas, ou se o preço atingirá primeiro 10 pips acima ou abaixo. Outra desvantagem é que perdemos a vantagem embutida de uma estratégia base—o benefício depende inteiramente do poder preditivo do modelo. Por outro lado, você não fica limitado pelas amostras que uma estratégia base fornece apenas quando é acionada, oferecendo um tamanho de amostra inicial maior e maior potencial de ganho. Implementamos a lógica de negociação na função onTick() assim:

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 estratégia, usar o mesmo tamanho de take profit e stop loss é mais consistente do que acompanhar os resultados das próximas 10 barras. O primeiro vincula nossa previsão diretamente ao lucro final, enquanto o segundo adiciona incerteza com retornos variáveis ao longo de cada período de 10 barras. Vale notar que usamos take profit e stop loss como uma porcentagem do preço, tornando-o mais adaptável entre diferentes ativos e mais adequado para ativos em tendência como ouro ou índices. Os leitores podem testar a alternativa descomentando o código comentado e removendo take profit e stop loss da função de compra.

Para os dados de atributos usados para prever resultados, selecionei os três retornos normalizados passados, a distância normalizada da máxima e mínima do intervalo, e alguns indicadores estacionários comuns. Armazenamos esses dados em um multi-array, que é então salvo em um arquivo CSV usando a classe CFileCSV de um arquivo incluído. Certifique-se de que todos os períodos gráficos e símbolos estejam definidos conforme listado abaixo para alternar facilmente entre os período gráficos e ativos.
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++;
}

O código final para o expert advisor de coleta de dados ficará assim:

#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++;
}

Pretendemos negociar esta estratégia no timeframe de 15 minutos do XAUUSD devido à sólida volatilidade do ativo, e porque 15 minutos oferece um equilíbrio entre redução de ruído e geração de um número maior de amostras. Uma negociação típica seria assim:

exemplo de negociação

Usamos os dados de 2020-2024 como dados de treinamento e validação, e testaremos o resultado em 2024-2025 no terminal MetaTrader 5 posteriormente. Após executar este EA no testador de estratégia, o arquivo CSV será salvo no diretório /Tester/Agent-sth000 na desinicialização do EA.

Além disso, clique com o botão direito para obter o relatório Excel do backtest assim:

relatório excel

Observe o número da linha da linha "Deals", que usaremos como entrada posteriormente.

encontrar linha

Depois disso, treinamos nosso modelo em Python.

O modelo que selecionamos para este artigo é um modelo baseado em árvore de decisão, ideal para problemas de classificação, assim como o que usamos em este artigo.

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}")
Usamos este código para rotular o relatório Excel, atribuindo 1 às negociações com lucro positivo e 0 às que não possuem. Em seguida, combinamos isso com os dados de atributos coletados do arquivo CSV do EA de coleta de dados. Tenha em mente que o valor skiprow corresponde ao número da linha 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)

Em seguida, atribuimos o array de rótulos à variável y e o data frame de atributos à variável 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)

Depois, dividimos os dados na proporção 9:1 em conjuntos de treinamento e validação e iniciamos o treinamento do modelo. A configuração padrão da função train-test split no sklearn inclui embaralhamento, o que não é ideal para dados de séries temporais, portanto certifique-se de definir shuffle=False nos parâmetros. É uma boa ideia ajustar os hiperparâmetros para evitar overfitting ou underfitting, dependendo do tamanho da sua amostra. Pessoalmente, descobri que interromper a iteração em torno de 0.1 de log loss funciona bem.

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()

Em seguida, plotamos as visualizações dos resultados para verificar o teste de validação. Na abordagem típica de treinar-validar-testar, a etapa de validação ajuda a escolher os melhores hiperparâmetros e inicialmente avalia se o modelo treinado possui poder preditivo. Ela serve como uma camada de validação antes do teste final.

limiar de confiança de acurácia

pontuação AUC

Aqui, observamos que a pontuação AUC está acima de 0.5, e a acurácia melhora à medida que aumentamos o limiar de confiança, o que geralmente é um sinal positivo. Se essas duas métricas não estiverem alinhadas, não entre em pânico, tente ajustar os hiperparâmetros primeiro antes de descartar completamente o modelo.
# 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()

importância dos atributos

Este bloco de código irá plotar a importância dos atributos, bem como a linha mediana. Existem muitas maneiras de definir a importância dos atributos no campo do aprendizado de máquina, como:

  1. Importância Baseada em Árvore: Mede a redução de impureza (ex.: Gini) em árvores de decisão ou ensembles como Random Forest e XGBoost.
  2. Importância por Permutação: Avalia a queda de desempenho quando os valores de um atributo são embaralhados.
  3. Valores SHAP: Calcula a contribuição de um atributo para as previsões com base nos valores de Shapley.
  4. Magnitude do Coeficiente: Usa o valor absoluto dos coeficientes em modelos lineares.

Em nosso exemplo, estamos usando CatBoost, um modelo baseado em árvore de decisão. A importância dos atributos mostra quanto de desordem (impureza) cada atributo reduz quando usado para dividir a árvore de decisão nos dados in-sample. É importante perceber que, embora selecionar os atributos mais importantes como conjunto final frequentemente aumente a eficiência do modelo, isso nem sempre melhora a capacidade preditiva pelos seguintes motivos:

  • A importância dos atributos é calculada a partir dos dados in-sample, sem conhecimento dos dados out-of-sample.
  • A importância dos atributos depende dos outros atributos considerados. Se a maioria dos atributos escolhidos não possui poder preditivo, remover os mais fracos não ajudará.
  • A importância reflete o quão eficaz um atributo divide a árvore, não necessariamente o quão crítico ele é para o resultado final da decisão.

Esses insights surgiram quando descobri inesperadamente que selecionar os atributos menos importantes na verdade aumentou a acurácia out-of-sample. Mas, em geral, escolher os atributos mais importantes e remover os menos importantes ajuda a tornar o modelo mais leve e provavelmente melhora a acurácia geral.

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 fim, exportamos o arquivo ONNX e o salvamos no diretório MQL5/Files.

Agora, vamos voltar ao editor de código do MetaTrader 5 para criar o EA de negociação.

Precisamos apenas ajustar o EA original de coleta de dados importando alguns arquivos include para lidar com o modelo CatBoost.

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

Em seguida, ajustaremos a função getData() para retornar um vetor.

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;
}

A lógica final de negociação na função OnTick() ficará assim:

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();
        }
    }
 }

Na lógica de sinal, primeiro verifica se nenhuma posição está atualmente aberta para garantir apenas uma negociação por vez. Em seguida, detecta se há um breakout em qualquer lado do intervalo. Depois disso, chama a função getData() para obter o vetor de atributos. Esse vetor é passado para o modelo CatBoost como entrada, e o modelo gera a confiança de previsão para cada resultado no array prob. Com base nos níveis de confiança para cada resultado, realizamos uma negociação apostando no resultado previsto. Essencialmente, estamos usando o modelo para gerar os sinais de compra ou venda.

Executamos um backtest no testador de estratégia do MetaTrader 5 usando dados in-sample de 2020 a 2024 para verificar se nossos dados de treinamento não tinham erros e se a junção de atributos e resultados estava correta. Se tudo estiver correto, a curva de equity deve parecer quase perfeita, assim:

in-sample

Em seguida, realizamos o backtest do teste out-of-sample de 2024 a 2025 para verificar se a estratégia possui lucratividade no período mais recente. Definimos o limiar em 0.7, então o modelo só realizará uma negociação em uma direção se o nível de confiança para o take-profit dessa direção ser atingido for de 70% ou mais, com base nos dados de treinamento.

configuração de backtest (discreto)

parâmetros

curva de equity (discreto)

resultado (discreto)

Podemos ver que o modelo teve um desempenho excepcional na primeira metade do ano, mas começou a apresentar queda de desempenho com o passar do tempo. Isso é comum em modelos de aprendizado de máquina, porque a vantagem obtida a partir de dados passados geralmente é temporária, e essa vantagem tende a se deteriorar ao longo do tempo. Isso sugere que uma proporção menor entre teste e treinamento pode funcionar melhor para futuras implementações em trading ao vivo. No geral, o modelo demonstra alguma previsibilidade, já que permaneceu lucrativo mesmo após considerar os custos de negociação.


Sinal Contínuo

No trading algorítmico, os traders normalmente seguem um método simples de uso de sinais discretos—ou comprar ou vender com um risco fixo por negociação. Isso torna as coisas mais fáceis de gerenciar e melhores para analisar o desempenho da estratégia. Alguns traders tentaram refinar esse método de sinal discreto usando um sinal aditivo, onde ajustam o risco da negociação com base na intensidade com que as condições da estratégia são atendidas. Sinais contínuos levam essa abordagem aditiva adiante, aplicando-a a condições mais abstratas da estratégia e produzindo um nível de risco entre zero e um.

A ideia básica por trás disso é que nem todas as negociações que atendem aos critérios de entrada são iguais. Algumas parecem mais propensas a ter sucesso porque seus sinais são mais fortes, com base em regras não lineares vinculadas à estratégia. Isso pode ser visto como uma ferramenta de gestão de risco—apostar alto quando a confiança é elevada e reduzir quando uma negociação parece menos promissora, mesmo que ainda tenha um retorno esperado positivo. No entanto, precisamos lembrar que isso adiciona outro fator ao desempenho da estratégia, e que o viés de antecipação e os riscos de overfitting ainda são problemáticos se não tivermos cuidado durante a implementação.

Para aplicar esse conceito em nosso EA de negociação, primeiro precisamos ajustar as funções de compra/venda para calcular o tamanho do lote com base no risco que estamos dispostos a perder caso o stop loss seja atingido. A função de cálculo de lote é assim:  

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;
}
Em seguida, atualizamos as funções de compra/venda para que chamem essa função calclots() e recebam o multiplicador de risco 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);
}
Como nosso modelo de aprendizado de máquina já fornece o nível de confiança, podemos usá-lo diretamente como entrada do multiplicador de risco. Se quisermos ajustar o quanto o nível de confiança afeta o risco de cada negociação, podemos simplesmente escalar o nível de confiança para cima ou para baixo conforme necessário.
if(prob[1]>threshold)executeBuy(prob[1]); 
if(prob[0]>threshold)executeSell(prob[0]);

Por exemplo, se quisermos amplificar a importância das diferenças no nível de confiança, poderíamos multiplicar a probabilidade por ela mesma três vezes. Isso aumentaria a diferença de proporção entre probabilidades, tornando o impacto dos níveis de confiança mais pronunciado.

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

Agora, tentamos observar o resultado no backtest. 

configuração de backtest (contínuo)

curva de equity (contínuo)

resultado (contínuo)

As negociações realizadas ainda são as mesmas da versão de sinal discreto, mas o fator de lucro e o índice de Sharpe melhoraram ligeiramente. Isso sugere que, neste cenário específico, o sinal contínuo melhorou o desempenho geral do teste out-of-sample, que está livre de viés de antecipação, já que testamos apenas uma vez. No entanto, é importante observar que essa abordagem só supera o método de risco fixo se a precisão das previsões do modelo for maior quando seu nível de confiança for mais alto. Caso contrário, a abordagem original de risco fixo pode ser melhor. Além disso, como reduzimos o tamanho médio do lote ao aplicar multiplicadores de risco entre zero e um, precisaríamos aumentar o valor da variável de risco se quisermos alcançar um lucro total semelhante ao anterior.


Validação Multi-Timeframe

Treinar modelos de aprendizado de máquina separados, cada um utilizando um timeframe diferente de atributos para prever o mesmo resultado, pode oferecer uma forma poderosa de melhorar a filtragem de negociações e a geração de sinais. Ao ter um modelo focado em dados de curto prazo, outro em médio prazo e talvez um terceiro em tendências de longo prazo, você obtém insights especializados que, quando combinados, podem validar previsões de forma mais confiável do que um único modelo. Essa abordagem de múltiplos modelos pode aumentar a confiança nas decisões de negociação ao cruzar sinais, reduzir o risco de agir com base em ruído específico de um timeframe e apoiar a gestão de risco ao permitir ponderar a saída de cada modelo para ajustar o tamanho da negociação ou os stops com base na força do consenso.

Por outro lado, essa estratégia pode complicar o sistema, especialmente quando você atribui diferentes pesos às previsões de múltiplos modelos. Isso pode introduzir seus próprios vieses ou erros se não for cuidadosamente ajustado. Cada modelo também pode sofrer overfitting ao seu timeframe específico, ignorando dinâmicas mais amplas do mercado, e discrepâncias entre suas previsões podem gerar confusão, atrasando decisões ou reduzindo a confiança. 

Essa abordagem depende de duas suposições principais: nenhum viés de antecipação é introduzido no timeframe superior (devemos usar o valor da última barra, não o atual), e o modelo de timeframe superior possui sua própria capacidade preditiva (ele tem desempenho melhor que o acaso em testes out-of-sample).  

Para implementar isso, primeiro modificamos o código no EA de coleta de dados alterando todos os timeframes relacionados à extração de atributos para um timeframe superior, como 1 hora. Isso inclui indicadores, cálculos de preço e quaisquer outros atributos utilizados.

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;
}

Depois disso, seguimos os mesmos passos de antes: coleta de dados, treinamento do modelo e exportação, exatamente como discutido anteriormente.

Em seguida, no EA de negociação, criamos uma segunda função para obter os atributos de entrada, que será alimentada ao segundo modelo de ML que importamos para obter a saída do nível de confiança.

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;
}

Supondo que queremos atribuir o mesmo peso à saída dos dois modelos, simplesmente calculamos a média de suas saídas e a tratamos como a única saída usada 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);
        }
    }

Com essas duas variáveis calculadas como acima, agora podemos combiná-las em um único nível de confiança e usá-lo para validação, seguindo a mesma abordagem utilizada anteriormente.


Conclusão

Neste artigo, primeiro exploramos a ideia de usar um modelo de aprendizado de máquina como gerador de sinais em vez de filtro, demonstrado por meio de uma estratégia de breakout da caixa de Darvas. Apresentamos brevemente o processo de treinamento do modelo e discutimos a importância dos limiares de nível de confiança e da relevância dos atributos. Em seguida, introduzimos o conceito de sinais contínuos e comparamos seu desempenho com sinais discretos. Descobrimos que, neste exemplo, os sinais contínuos melhoraram o desempenho do backtest porque o modelo tendia a ter maior precisão de previsão conforme os níveis de confiança aumentavam. Por fim, abordamos o conceito de utilizar múltiplos modelos de aprendizado de máquina treinados em diferentes timeframes para validar sinais em conjunto.  

No geral, este artigo teve como objetivo apresentar ideias não convencionais sobre a aplicação de modelos de aprendizado de máquina supervisionado no trading CTA. Seu objetivo não é afirmar definitivamente qual abordagem funciona melhor, pois tudo depende do cenário específico, mas sim inspirar os leitores a pensar de forma criativa e expandir conceitos iniciais simples. No final, nada é totalmente novo—a inovação frequentemente surge da combinação de ideias existentes para criar algo novo.

Tabela de Arquivos

Nome do Arquivo Uso dos Arquivos
Darvas_Box.ipynb O arquivo Jupyter Notebook para treinamento do modelo de ML
Darvas Box Data.mq5 O EA para coleta de dados para treinamento do modelo
Darvas Box EA.mq5 O EA de negociação no artigo
CatOnnx.mqh Um arquivo include para processamento do modelo CatBoost
FileCSV.mqh Um arquivo include para salvar dados em CSV

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/17466

Arquivos anexados |
Darvas_ML.zip (136.61 KB)
Últimos Comentários | Ir para discussão (2)
linfo2
linfo2 | 24 mar. 2025 em 00:42
Obrigado, Zhou, pelo artigo interessante e pelos exemplos de código. Para mim, tive que instalar manualmente alguns dos componentes do Python para fazê-lo funcionar, o que pode ajudar outros usuários: pip install catboost! pip install onnxruntime ! pip install skl2onnx. após a conclusão, posso testar. mas se eu tentar carregar o EA relacionado, terei como retorno 'Failed to set the Output[1] shape Err=5802. Não tenho certeza de onde isso vem ou se é importante e não consigo descobrir de onde vem... a documentação diz ERR_ONNX_NOT_SUPPORTED

5802

Propriedade ou valor não suportado pela MQL5, seguido pela mensagem ONNX Model Initialised... você tem alguma sugestão?
Zhuo Kai Chen
Zhuo Kai Chen | 24 mar. 2025 em 01:09
linfo2 catboost! pip install onnxruntime ! pip install skl2onnx. após a conclusão, posso testar. mas se eu tentar carregar o EA relacionado, terei como retorno 'Failed to set the Output[1] shape Err=5802. Não tenho certeza de onde isso vem ou se é importante e não consigo descobrir de onde vem... a documentação diz ERR_ONNX_NOT_SUPPORTED

5802

Propriedade ou valor não suportado pela MQL5, seguido pela mensagem ONNX Model Initialised... você tem alguma sugestão?

Obrigado por lembrar. A parte da instalação do pip foi ignorada, mas os usuários precisam instalar a biblioteca relacionada, caso ainda não o tenham feito.

Seu erro pode ser causado pelo fato de as dimensões usadas no treinamento do modelo serem diferentes das usadas no seu EA. Por exemplo, se você treinou um modelo com 5 recursos, também deve inserir 5 recursos no seu EA, e não 4 ou 6. Um passo a passo mais detalhado está no link deste artigo. Espero que isso ajude. Caso contrário, forneça mais contexto.

Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Redes neurais em trading: Desvendando os componentes estruturais da série (SCNN) Redes neurais em trading: Desvendando os componentes estruturais da série (SCNN)
Vamos conhecer o framework inovador SCNN, que leva a análise de séries temporais a um novo nível ao separar claramente os dados em componentes de longo prazo, sazonais, de curto prazo e residuais. Essa abordagem aumenta significativamente a precisão da previsão, permitindo que o modelo se adapte a uma dinâmica de mercado complexa e em constante mudança.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Do iniciante ao especialista: Criação de um EA de notícias animado em MQL5(V): sistema de lembretes de eventos Do iniciante ao especialista: Criação de um EA de notícias animado em MQL5(V): sistema de lembretes de eventos
Nesta discussão, veremos aprimoramentos adicionais, ao integrarmos uma lógica avançada de alertas para os eventos do calendário econômico exibidos pelo EA "Manchetes de notícias". Esse aprimoramento é decisivo, pois garante que os usuários recebam notificações em tempo hábil pouco antes dos principais eventos programados. Acompanhe esta discussão para saber mais.