English Русский 中文 Español Deutsch 日本語
preview
Ciência de Dados e ML (Parte 36): Lidando com Mercados Financeiros Tendenciosos

Ciência de Dados e ML (Parte 36): Lidando com Mercados Financeiros Tendenciosos

MetaTrader 5 — Sistemas de negociação |
65 3
Omega J Msigwa
Omega J Msigwa

Conteúdo


Introdução

Diferentes mercados de Forex e instrumentos financeiros apresentam comportamentos distintos em diferentes períodos. Enquanto alguns mercados financeiros, como ações e índices, frequentemente apresentam tendências de alta no longo prazo, outros, como o mercado Forex, podem apresentar movimentos de baixa e outros padrões de comportamento. Essa incerteza aumenta a complexidade da tarefa de prever o mercado utilizando técnicas de Inteligência Artificial (IA) e modelos de Machine Learning (ML).

fonte da imagem: economictimes.indiatimes.com

Vamos selecionar alguns mercados financeiros (símbolos de trading) e visualizar a direção de 1.000 barras do mercado no timeframe diário. Se o preço de fechamento de uma barra estiver acima do preço de abertura, podemos classificá-la como uma barra de alta (1). Caso contrário, podemos classificá-la como uma barra de baixa (0).

import pandas as pd
import numpy as np

symbols = [
    "EURUSD",
    "USTEC",
    "XAUUSD",
    "USDJPY",
    "BTCUSD",
    "CA60",
    "UK100"
]

for symbol in symbols:
    
    df = pd.read_csv(fr"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\1640F6577B1C4EC659BF41EA9F6C38ED\MQL5\Files\{symbol}.PERIOD_D1.data.csv")
    df["Candle type"] = (df["Close"] > df["Open"]).astype(int)
    
    print(f"{symbol}(unique):",np.unique(df["Candle type"], return_counts=True))

Resultado.

EURUSD(unique): (array([0, 1]), array([496, 504]))
USTEC(unique): (array([0, 1]), array([472, 528]))
XAUUSD(unique): (array([0, 1]), array([472, 528]))
USDJPY(unique): (array([0, 1]), array([408, 592]))
BTCUSD(unique): (array([0, 1]), array([478, 522]))
CA60(unique): (array([0, 1]), array([470, 530]))
UK100(unique): (array([0, 1]), array([463, 537]))

Como podemos observar no resultado acima, nenhum dos símbolos de trading está perfeitamente equilibrado, pois historicamente ocorreram quantidades diferentes de candles de baixa e de alta.

Não há nada de errado em o mercado apresentar um viés em determinada direção. No entanto, esse viés presente nos dados históricos pode causar alguns problemas durante o treinamento de modelos de machine learning. Veja como:

Suponha que desejemos treinar um modelo para o USDJPY utilizando o conjunto de dados atual, que possui 1.000 barras. Temos 408 candles de baixa (identificados como 0), correspondentes a 40,8% de todos os sinais de trading, enquanto temos 592 candles de alta (identificados como 1), que representam 59,2% de todos os sinais de trading.

A predominância dos sinais de alta em relação aos sinais de baixa tende a influenciar os modelos de machine learning, pois eles geralmente favorecem a classe dominante e, consequentemente, passam a produzir previsões que favorecem essa classe.

Como todos os modelos buscam atingir o menor valor de perda possível acompanhado da maior acurácia possível, eles tenderão a favorecer a classe de alta, que ocorreu em 59,2% dos casos, como uma maneira simples de alcançar um valor elevado de acurácia.

Isso não é difícil de entender. Utilizando apenas essa informação e ignorando todos os preditores e tudo o que está acontecendo no mercado, poderíamos simplesmente afirmar, ao prever o USDJPY, que todas as barras serão de alta. Fazendo isso, estaríamos corretos em aproximadamente 59,2% das vezes. Nada mal, certo? Errado!

Ao fazer isso, estaríamos supondo que aquilo que aconteceu no passado necessariamente voltará a acontecer, uma suposição extremamente equivocada no mundo do trading.

Como podemos perceber, ter uma variável-alvo desbalanceada em dados de classificação para machine learning representa um problema. A seguir, veremos algumas das principais limitações decorrentes dessa situação.


Limitações de Variáveis-Alvo Desbalanceadas em Machine Learning

  1. Baixo desempenho na classe minoritária

    Como mencionado anteriormente, o modelo tende a direcionar suas previsões para a classe majoritária porque busca otimizar a acurácia geral. Por exemplo, em dados de detecção de fraude com 99% de casos legítimos e 1% de casos fraudulentos, como a maioria das pessoas não comete fraude, um modelo poderia sempre prever "não fraude" e ainda assim alcançar 99% de acurácia, mas falharia em detectar as fraudes.

  2. Métricas de avaliação enganosas

    O valor da acurácia torna-se pouco confiável. Podemos ter um modelo com acurácia geral de 72% sem perceber que uma das classes foi prevista com 95% de acurácia, enquanto outra apresentou apenas 50%.

  3. Overfitting dos modelos na classe majoritária

    Os modelos podem memorizar ruídos presentes na classe majoritária e tomar decisões enviesadas, em vez de aprender os padrões gerais presentes nos preditores. Por exemplo, em dados de diagnóstico médico com 95% de indivíduos saudáveis e 5% de indivíduos com determinada doença, o modelo pode simplesmente ignorar os casos da doença.

  4. Baixa capacidade de generalização para dados não vistos anteriormente (dados reais)

    Em dados do mundo real, a distribuição pode mudar de forma rápida e frequente. Se um modelo tiver sido treinado em um ambiente enviesado, provavelmente apresentará falhas mais cedo ou mais tarde, pois foi treinado com uma distribuição que não representa adequadamente as condições reais. 


Técnicas para Lidar com o Problema de Conjunto de Dados Desbalanceados

Agora que conhecemos as limitações associadas a uma variável-alvo desbalanceada (enviesada) em um problema de classificação, vamos discutir diferentes maneiras de lidar com essa questão.

01: Escolhendo a Métrica de Avaliação Adequada

A primeira técnica para lidar com dados desbalanceados é escolher uma métrica de avaliação adequada. Como vimos anteriormente, a acurácia de um classificador — calculada dividindo-se o número total de previsões corretas pelo número total de previsões — pode ser enganosa quando os dados estão desbalanceados.

Em problemas com dados desbalanceados, outras métricas são muito mais úteis do que a acurácia. Entre elas estão a precision, que mede a precisão das previsões do classificador para uma determinada classe, e o recall, que mede a capacidade do classificador de identificar corretamente uma classe.

Ao trabalhar com conjunto de dados desbalanceados, muitos especialistas em machine learning utilizam o F1-score, pois ele é mais adequado para esse tipo de situação.

Ele corresponde à média harmônica entre precision e recall, representada pela seguinte fórmula.

f1 score

Portanto, se o classificador prever a classe minoritária, mas essa previsão estiver incorreta e o número de falsos positivos aumentar, a precision será baixa e, consequentemente, o F1-score também será baixo.

Da mesma forma, se o classificador tiver dificuldade para identificar corretamente a classe minoritária, o número de falsos negativos aumentará e, consequentemente, o recall e o F1-score serão baixos.

O F1-score só aumenta quando melhoram tanto a cobertura quanto a qualidade geral das previsões.

Para entender melhor esse conceito, vamos treinar um classificador RandomForest simples utilizando os dados desbalanceados do USDJPY.

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report

# Global variables

symbol = "USDJPY"
timeframe = "PERIOD_D1"
lookahead = 1
common_path = r"C:\Users\Omega Joctan\AppData\Roaming\MetaQuotes\Terminal\Common\Files"

df = pd.read_csv(f"{common_path}\{symbol}.{timeframe}.data.csv")

# Target variable

df["future_close"] = df["Close"].shift(-lookahead) # future closing price based on lookahead value
df.dropna(inplace=True)

df["Signal"] = (df["future_close"] > df["Close"]).astype(int)

print("Signals(unique): ",np.unique(df["Signal"], return_counts=True))

X = df.drop(columns=["Signal", "future_close"])
y = df["Signal"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, shuffle=False)

model = RandomForestClassifier(n_estimators=100,
                               max_depth=5,
                               min_samples_split=3,
                               random_state=42)

model.fit(X_train, y_train)

Após o treinamento, podemos salvar o modelo no formato ONNX para utilizá-lo posteriormente no MetaTrader 5.

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
import os

def saveModel(model, n_features: int, technique_name: str):
    
    initial_type = [("input", FloatTensorType([None, n_features]))]
    
    onnx_model = convert_sklearn(model, initial_types=initial_type, target_opset=14)

    with open(os.path.join(common_path, f"{symbol}.{timeframe}.{technique_name}.onnx"), "wb") as f:
        f.write(onnx_model.SerializeToString())
saveModel(model=model, n_features=X_train.shape[1], technique_name="no-sampling")

Precisei utilizar o método classification report para observar as diferentes métricas disponíveis no Scikit-Learn.

Train Classification report
               precision    recall  f1-score   support

           0       0.98      0.41      0.57       158
           1       0.68      1.00      0.81       204

    accuracy                           0.74       362
   macro avg       0.83      0.70      0.69       362
weighted avg       0.81      0.74      0.71       362

Análise do classification report do treinamento 

A acurácia geral do modelo durante o treinamento foi de 0,74, o que pode parecer um resultado razoável à primeira vista. No entanto, uma análise mais detalhada das métricas de cada classe revela um desequilíbrio significativo no desempenho do modelo entre as duas classes. A classe 0 apresenta uma precision muito alta, de 0,98, mas um recall baixo, de 0,41, resultando em um F1-score de apenas 0,57.

Isso significa que, embora o modelo apresente alta precisão quando prevê a classe 0, ele deixa de identificar uma grande quantidade de amostras que realmente pertencem a essa classe, indicando baixa sensibilidade.

A classe 1, por outro lado, apresenta recall de 1,00 e F1-score de 0,81, mas uma precision relativamente menor, de 0,68.

Isso sugere que o modelo apresenta um viés excessivo em favor da previsão da classe 1, possivelmente resultando em um grande número de falsos positivos.

O recall perfeito de 1,00 para a classe 1 é um sinal de alerta, pois pode indicar overfitting ou um viés em favor da classe majoritária.

O modelo está prevendo quase tudo como classe 1 e deixando de identificar muitas amostras que realmente pertencem à classe 0, como podemos observar pelo baixo recall de 0,41 da classe 0.

De modo geral, essas métricas não apenas demonstram o desbalanceamento, como também levantam dúvidas sobre a capacidade de generalização do modelo e sobre a consistência de seu desempenho entre as diferentes classes. Claramente, há algo errado aqui.

Vamos utilizar técnicas de oversampling para melhorar nosso modelo e buscar um maior equilíbrio nas previsões.


Um Expert Advisor (EA) para Testes

Sempre existe uma diferença entre os resultados obtidos na análise de um modelo de machine learning, como o classification report apresentado acima, e os resultados reais das operações no MetaTrader 5. Como salvaremos o modelo no formato ONNX para utilizá-lo posteriormente, podemos criar um robô de trading simples que utilize modelos treinados com cada uma das técnicas de reamostragem discutidas neste artigo e e usá-los para tomar decisões de trading no Strategy Tester utilizando a amostra de treinamento.

Os dados utilizados foram coletados pelo arquivo chamado Collectdata.mq5, um script que coleta os dados de treinamento de 01.01.2025 até 01.01.2023. Você pode encontrá-lo nos arquivos anexados a este artigo.

No Expert Advisor (EA) chamado Test Resampling Techniques.mq5, inicializamos o modelo no formato ONNX e, em seguida, o utilizamos para realizar previsões.

#include <Random Forest.mqh> 
CRandomForestClassifier random_forest; //A class for loading the RFC in ONNX format

#include <Trade\Trade.mqh>
#include <Trade\PositionInfo.mqh>

CTrade m_trade;
CPositionInfo m_position;

input string symbol_ = "USDJPY";
input int magic_number= 14042025;
input int slippage = 100;
input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1;
input string technique_name = "randomoversampling";

int lookahead = 1;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   if (!random_forest.Init(StringFormat("%s.%s.%s.onnx", symbol_, EnumToString(timeframe_), technique_name), ONNX_COMMON_FOLDER)) //Initializing the RFC in ONNX format from a commmon folder
     return INIT_FAILED;
     
//--- Setting up the CTrade module
   
   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetMarginMode();
   m_trade.SetTypeFillingBySymbol(symbol_);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
      
   vector x = {
      iOpen(symbol_, timeframe_, 1),
      iHigh(symbol_, timeframe_, 1),
      iLow(symbol_, timeframe_, 1),
      iClose(symbol_, timeframe_, 1)
   };
   
   long signal = random_forest.predict_bin(x); //Predicted class
   double proba = random_forest.predict_proba(x).Max(); //Maximum predicted probability

   MqlTick ticks;
   if (!SymbolInfoTick(symbol_, ticks))
      {
         printf("Failed to obtain ticks information, Error = %d",GetLastError());
         return;
      }
      
   double volume_ = SymbolInfoDouble(symbol_, SYMBOL_VOLUME_MIN);
   
   
   if (signal == 1) 
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, symbol_, ticks.ask,0,0);
     }
     
   if (signal == 0)
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, symbol_, ticks.bid,0,0);
     } 

//---
   
   CloseTradeAfterTime((Timeframe2Minutes(timeframe_)*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool PosExists(ENUM_POSITION_TYPE type)
 {
    for (int i=PositionsTotal()-1; i>=0; i--)
      if (m_position.SelectByIndex(i))
         if (m_position.Symbol()==symbol_ && m_position.Magic() == magic_number && m_position.PositionType()==type)
            return (true);
            
    return (false);
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
bool ClosePos(ENUM_POSITION_TYPE type)
 {
    for (int i=PositionsTotal()-1; i>=0; i--)
      if (m_position.SelectByIndex(i))
         if (m_position.Symbol() == symbol_ && m_position.Magic() == magic_number && m_position.PositionType()==type)
            {
              if (m_trade.PositionClose(m_position.Ticket()))
                return true;
            }
            
    return (false);
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CloseTradeAfterTime(int period_seconds)
{
   for (int i = PositionsTotal() - 1; i >= 0; i--)
      if (m_position.SelectByIndex(i))
         if (m_position.Magic() == magic_number)
            if (TimeCurrent() - m_position.Time() >= period_seconds)
               m_trade.PositionClose(m_position.Ticket(), slippage);
}
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

int Timeframe2Minutes(ENUM_TIMEFRAMES tf)
{
    switch(tf)
    {
        case PERIOD_M1:  return 1;
        case PERIOD_M2:  return 2;
        case PERIOD_M3:  return 3;
        case PERIOD_M4:  return 4;
        case PERIOD_M5:  return 5;
        case PERIOD_M6:  return 6;
        case PERIOD_M10: return 10;
        case PERIOD_M12: return 12;
        case PERIOD_M15: return 15;
        case PERIOD_M20: return 20;
        case PERIOD_M30: return 30;
        case PERIOD_H1:  return 60;
        case PERIOD_H2:  return 120;
        case PERIOD_H3:  return 180;
        case PERIOD_H4:  return 240;
        case PERIOD_H6:  return 360;
        case PERIOD_H8:  return 480;
        case PERIOD_H12: return 720;
        case PERIOD_D1:  return 1440; // 1 day = 1440 minutes
        case PERIOD_W1:  return 10080; // 1 week = 7 * 1440 minutes
        case PERIOD_MN1: return 43200; // Approx. 1 month = 30 * 1440 minutes

        default:
            PrintFormat("Unknown timeframe: %d", tf);
            return 0;
    }
}

Como treinamos o modelo utilizando uma variável-alvo baseada em um valor de número de barras à frente igual a 1, precisamos encerrar a operação depois que o número de barras definido pelo antecipação tiver transcorrido no timeframe atual. Dessa forma, garantimos que o valor de antecipação seja respeitado, mantendo e encerrando nossas operações de acordo com o horizonte de previsão do modelo.

Antes de analisarmos os resultados das operações realizadas pelos modelos treinados com dados reamostrados, vamos observar o desempenho de trading de um modelo treinado com os dados originais, sem reamostragem (dados brutos).

Configurações do Strategy Tester.

Parâmetros de entrada: technique_name = no-sampling.

Resultados do Strategy Tester.

Apesar de o modelo ter conseguido identificar alguns bons sinais e apresentar resultados operacionais relevantes, com as operações lucrativas representando 62,24% de todas as operações realizadas, existe um desequilíbrio evidente. Ao observarmos separadamente as operações de venda e de compra vencedoras, podemos perceber uma proporção de aproximadamente 1:4 entre operações curtas e longas.

Das 519 operações realizadas, 102 foram operações curtas, com uma taxa de acerto de 70,59%, enquanto 417 foram operações curtas, com uma taxa de acerto de 60,19%. Claramente, há algo fora do esperado, pois, se analisarmos a direção dos candles entre 1º de janeiro de 2023 e 1º de janeiro de 2025 utilizando um valor de antecipação igual a 1:

print("classes in y: ",np.unique(y, return_counts=True))

Resultado.

classes in y:  (array([0, 1]), array([225, 293]))

Podemos observar que ocorreram 225 movimentos de baixa e 293 movimentos de alta. Como a maioria dos candles do USDJPY apresentou movimentos de alta durante esse período de dois anos (de 1º de janeiro de 2023 a 1º de janeiro de 2025), até mesmo um modelo ruim que favorecesse os movimentos de alta poderia gerar lucro. Não é tão difícil assim.

Agora podemos entender que a principal razão pela qual o modelo conseguiu gerar algum lucro foi o fato de ter favorecido as operações longas em uma proporção quatro vezes maior do que as operações curtas.

Como o mercado apresentou predominantemente movimentos de alta durante esse período, o modelo conseguiu gerar algum lucro.

Vamos prosseguir com as técnicas de reamostragem e analisar como podemos reduzir esse viés na tomada de decisões dos nossos modelos.


Técnicas de Oversampling

Random Oversampling

Essa é uma técnica utilizada para lidar com o desbalanceamento entre classes em conjunto de dados por meio da duplicação aleatória de amostras existentes da classe minoritária.

Ela consiste em selecionar aleatoriamente exemplos existentes da classe minoritária e duplicá-los para aumentar sua representatividade nos dados de treinamento, buscando equilibrar a distribuição das classes em conjunto de dados desbalanceados.

A ferramenta mais utilizada para essa tarefa é a imbalanced-learn. Abaixo, apresentamos uma maneira simples de utilizá-la.

from imblearn.over_sampling import RandomOverSampler

print("b4 Target: ",np.unique(y_train, return_counts=True))

rus = RandomOverSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

print("After Target: ",np.unique(y_resampled, return_counts=True))

Saídas.

b4 Target:  (array([0, 1]), array([304, 395]))
After Target:  (array([0, 1]), array([395, 395]))

Podemos ajustar o mesmo RandomForestClassifier utilizado anteriormente aos dados reamostrados e observar a diferença nos resultados em comparação com aqueles obtidos sem a reamostragem dos dados.

model.fit(X_resampled, y_resampled)

Avaliação do modelo.

y_train_pred = model.predict(X_train)

print("Train Classification report\n",classification_report(y_train, y_train_pred))

Resultado.

Train Classification report
               precision    recall  f1-score   support

           0       0.82      0.85      0.83       158
           1       0.88      0.86      0.87       204

    accuracy                           0.85       362
   macro avg       0.85      0.85      0.85       362
weighted avg       0.85      0.85      0.85       362

Excelente! Esses resultados indicam uma melhora significativa em todas as métricas. Os F1-scores de 0,87 para ambas as classes sugerem que o modelo está produzindo previsões mais equilibradas e consistentes, indicando uma boa capacidade de generalização e um aprendizado bem distribuído entre as classes da variável-alvo.

Apesar de sua simplicidade e eficácia, o oversampling pode aumentar o risco de overfitting ao criar cópias das amostras da classe minoritária, que podem não acrescentar novas informações ao modelo.

Utilizando as mesmas configurações do Strategy Tester, podemos testar o modelo treinado com esses dados.

Parâmetros de entrada: technique_name = randomoversampling.

Resultados do Strategy Tester.

Como podemos observar, houve melhorias em diversos aspectos do trading. Esse modelo é mais robusto do que aquele treinado com os dados brutos. Agora, o robô abre mais operações curtas, o que resultou em uma redução significativa na quantidade de operações longas.

Novamente, neste período de treinamento, o mercado apresentou 225 movimentos de baixa e 293 de alta, respectivamente, conforme a definição da variável-alvo; este novo modelo, treinado com dados submetidos a sobreamostragem, parece abrir 238 operações short (venda) e 281 de long (compra), respectivamente. Esse é um bom indício de que o modelo não apresenta viés, uma vez que toma decisões baseadas principalmente nos padrões aprendidos.


Técnicas de Undersampling

Existem diversas técnicas de undersampling disponíveis em diferentes módulos do Python. Algumas delas são:

Random Undersampling

Essa é uma técnica utilizada para lidar com o desbalanceamento entre classes em conjunto de dados, reduzindo o número de amostras da classe majoritária para equilibrá-la com a classe minoritária.

Ela consiste em remover, de maneira aleatória ou estratégica, amostras da classe majoritária.

De maneira semelhante ao que fizemos com o oversampling, podemos aplicar undersampling à classe majoritária da seguinte forma.

from imblearn.under_sampling import RandomUnderSampler

print("b4 Target: ",np.unique(y_train, return_counts=True))

rus = RandomUnderSampler(random_state=42)
X_resampled, y_resampled = rus.fit_resample(X_train, y_train)

print("After Target: ",np.unique(y_resampled, return_counts=True))

Resultado.

b4 Target:  (array([0, 1]), array([304, 395]))
After Target:  (array([0, 1]), array([304, 304]))

O Random Undersampling e outras técnicas de undersampling podem reduzir o desempenho do modelo ao remover amostras relevantes da classe majoritária, resultando em um conjunto de treinamento menos representativo. Isso pode potencialmente causar underfitting no modelo.

Essa técnica melhorou o desempenho do modelo nos dados de treinamento para ambas as classes.

Train Classification report
               precision    recall  f1-score   support

           0       0.76      0.90      0.82       158
           1       0.91      0.78      0.84       204

    accuracy                           0.83       362
   macro avg       0.83      0.84      0.83       362
weighted avg       0.84      0.83      0.83       362

Utilizando as mesmas configurações do Strategy Tester, podemos testar o modelo treinado com esses dados.

Parâmetros de entrada: technique_name = randomundersampling.

Resultados do Strategy Tester.

Essa técnica resultou em 282 operações curtas e 237 operações longas. Embora o modelo tenha favorecido as operações short em relação às operações longas — algo que não refletia o comportamento predominante do mercado —, ainda assim conseguiu gerar mais lucro do que o modelo enviesado treinado com dados brutos e do que o modelo submetido a oversampling, que favorecia os movimentos de alta.

Esses resultados mostram que o modelo pode gerar lucro operando em ambas as direções do mercado, independentemente do comportamento predominante observado historicamente.

Tomek Links

Tomek Links referem-se a pares de amostras pertencentes a classes diferentes que estão muito próximas umas das outras, sendo frequentemente consideradas vizinhas mais próximas. A seguir, apresentamos uma explicação simples de como a técnica Tomek Links funciona ao aplicar undersampling em dados de machine learning.

Imagine que temos dois pontos, A e B, pertencentes a classes diferentes. O ponto A pertence à classe majoritária, enquanto o ponto B pertence à classe minoritária (ou vice-versa).

Se esses dois pontos (A e B) estiverem próximos um do outro, ou seja, forem vizinhos, uma observação pertencente à classe majoritária — neste caso, A — será removida.

Essa técnica ajuda a tornar os limites de decisão mais claros e as classes mais distintas, ao mesmo tempo em que remove algumas amostras da classe majoritária.

from imblearn.under_sampling import TomekLinks

tl = TomekLinks()
X_resampled, y_resampled = tl.fit_resample(X_train, y_train)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Saídas.

Before --> y (unique): (array([0, 1]), array([304, 395]))
After  --> y (unique): (array([0, 1]), array([304, 283]))

Essa técnica pode gerar resultados preditivos equilibrados e impressionantes a partir do modelo, mas é limitada à classificação binária, menos eficaz em casos de dados com alta sobreposição e, assim como outras técnicas de amostragem, pode levar à perda de dados.

Essa técnica também apresentou um desempenho melhor nos dados de treinamento.

Train Classification report
               precision    recall  f1-score   support

           0       0.69      0.94      0.80       158
           1       0.93      0.68      0.78       204

    accuracy                           0.79       362
   macro avg       0.81      0.81      0.79       362
weighted avg       0.83      0.79      0.79       362

Utilizando a mesma configuração de teste, podemos testar o modelo treinado com esses dados no Testador de Estratégias.

Parâmetros de entrada: technique_name = tomek-links.

Resultados do Strategy Tester.

Assim como ocorreu com a técnica Random Undersampling, o Tomek Links favoreceu as operações curtas: foram abertas 303 operações curtas e 216 operações longas. Mesmo assim, o modelo conseguiu gerar lucro.

Cluster centroids

Essa é uma técnica de undersampling na qual a classe majoritária é reduzida substituindo suas amostras pelos centroides dos clusters, geralmente obtidos por meio do algoritmo K-means.

Ela funciona da seguinte maneira.

  1. O algoritmo K-means é aplicado à classe majoritária.
  2. É definido o número K de amostras desejadas.
  3. As amostras da classe majoritária são substituídas pelos K centros dos clusters.
  4. O resultado é combinado com a classe minoritária para criar um conjunto de dados balanceado.

from imblearn.under_sampling import ClusterCentroids

cc = ClusterCentroids(random_state=42)
X_resampled, y_resampled = cc.fit_resample(X, y)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Saídas.

Before --> y (unique): (array([0, 1]), array([158, 204]))
After  --> y (unique): (array([0, 1]), array([225, 225]))

Abaixo está o desempenho, nos dados de treinamento, do modelo treinado com dados submetidos a undersampling utilizando Cluster Centroids.

Train Classification report
               precision    recall  f1-score   support

           0       0.64      0.86      0.73       158
           1       0.85      0.62      0.72       204

    accuracy                           0.73       362
   macro avg       0.75      0.74      0.73       362
weighted avg       0.76      0.73      0.73       362

Até o momento, essa técnica apresentou o menor valor de acurácia. O valor de 0,73 pode indicar que o modelo apresenta menos overfitting do que os anteriores e, portanto, pode ser o melhor modelo obtido até agora. Vamos analisar seu desempenho em um ambiente de trading.

Parâmetros de entrada: technique_name = cluster-centroids.

Resultados do Strategy Tester.

Essa técnica apresentou o maior número de operações lucrativas: 343 de um total de 519 operações, correspondendo a uma taxa de acerto de 66,09%, enquanto o lucro obtido ficou próximo do valor do depósito inicial. Embora o modelo tenha favorecido as operações curtas, ele apresentou alta precisão na previsão dos sinais de alta, alcançando uma expressiva taxa de 75,97% de operações longas vencedoras.


Métodos Híbridos

SMOTE + Tomek Links

Primeiro, o SMOTE é aplicado e, em seguida, o Tomek Links é utilizado para remover ruídos dos dados.

from imblearn.combine import SMOTETomek

smt = SMOTETomek(random_state=42)
X_resampled, y_resampled = smt.fit_resample(X_train, y_train)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Saídas.

Before --> y (unique): (array([0, 1]), array([158, 204]))
After  --> y (unique): (array([0, 1]), array([159, 159]))

Abaixo está o resultado do modelo treinado com dados de treinamento reamostrados por meio dessa técnica.

Train Classification report
               precision    recall  f1-score   support

           0       0.74      0.73      0.73       158
           1       0.79      0.80      0.80       204

    accuracy                           0.77       362
   macro avg       0.77      0.77      0.77       362
weighted avg       0.77      0.77      0.77       362

Abaixo estão os resultados das operações.

Parâmetros de entrada: technique_name = smote-tomeklinks.

Resultados do Strategy Tester.

Foram realizadas 220 operações curtas e 299 operações longas, respectivamente. Nada mal.

SMOTE + ENN (Edited Nearest Neighbors)

O SMOTE gera amostras sintéticas e, em seguida, o ENN remove as amostras classificadas incorretamente.

from imblearn.combine import SMOTEENN

sme = SMOTEENN(random_state=42)
X_resampled, y_resampled = sme.fit_resample(X_train, y_train)

print(f"Before --> y (unique): {np.unique(y_train, return_counts=True)}\nAfter  --> y (unique): {np.unique(y_resampled, return_counts=True)}")

Essa técnica removeu uma quantidade significativa de dados, reduzindo o conjunto de treinamento para apenas 61 amostras.

Before --> y (unique): (array([0, 1]), array([158, 204]))
After  --> y (unique): (array([0, 1]), array([37, 24]))

Abaixo está o classification report da amostra de treinamento.

Train Classification report
               precision    recall  f1-score   support

           0       0.46      0.76      0.58       158
           1       0.63      0.32      0.42       204

    accuracy                           0.51       362
   macro avg       0.55      0.54      0.50       362
weighted avg       0.56      0.51      0.49       362

Como esperado, o modelo resultante apresentou baixo desempenho, pois foi treinado com apenas 61 amostras, uma quantidade insuficiente para que qualquer modelo consiga aprender padrões relevantes. Vamos observar os resultados das operações.

Parâmetros de entrada: technique_name = smote-enn.

Resultados do Strategy Tester.

Essa técnica não ajudou e, na verdade, piorou os resultados. Ela introduziu um viés nas operações, pois o robô abriu 180 operações de compra e 339 operações de venda, de um total de 519 operações.

Isso não significa que a técnica seja ruim; apenas indica que ela não é a opção mais adequada para esta situação.


Conclusão

Vivemos em um mundo imperfeito, no qual nem todos os fenômenos possuem explicações adequadas ou caminhos claramente definidos. Isso também se aplica ao mundo do trading, onde os mercados mudam de maneira rápida e frequente, fazendo com que muitas das nossas estratégias se tornem obsoletas rapidamente.

Embora não possamos controlar o que acontece no mercado, o melhor que podemos fazer é garantir que tenhamos sistemas e estratégias de trading robustos, desenvolvidos para funcionar mesmo em condições extremas.

Como a história nem sempre se repete, é importante desenvolver sistemas de trading menos enviesados e capazes de funcionar em diferentes condições de mercado. Esses sistemas devem considerar os padrões observados anteriormente, sem depender excessivamente deles para tomar decisões de trading. Por mais úteis que essas técnicas possam ser, é importante considerar suas limitações e os trade-offs associados ao uso de técnicas de reamostragem em dados de machine learning. Entre essas limitações estão o risco de overfitting causado pelo oversampling, a possível perda de informações relevantes ao aplicar undersampling e a introdução de ruído ou viés caso a reamostragem não seja realizada de maneira adequada.

Encontrar o equilíbrio adequado é fundamental para desenvolver modelos robustos, capazes de generalizar bem diante de condições de mercado ainda não observadas.

Atenciosamente.

Fique ligado e contribua para o desenvolvimento de algoritmos de machine learning para a linguagem MQL5 neste repositório do GitHub.


Tabela de Anexos

Nome do arquivo Descrição/Uso
Experts\Test Resampling Techniques.mq5
Um Expert Advisor (EA) para utilizar arquivos .ONNX no MQL5
Include\pandas.mqh
Biblioteca Pandas semelhante à do Python para manipulação e armazenamento de dados
Scripts\Collectdata.mq5
Um script para coletar os dados de treinamento
Common\*.onnx
Modelos de machine learning no formato ONNX
Common\*.csv
Dados de treinamento de diferentes instrumentos para utilização em machine learning


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

Arquivos anexados |
Attachments.zip (153.84 KB)
Últimos Comentários | Ir para discussão (3)
linfo2
linfo2 | 23 abr. 2025 em 22:56
Obrigado, Omega. Agradeço por ter preparado isso. O Bais é algo que todos nós tememos. Baixei os anexos. Gostaria de sugerir que incluam todos os componentes necessários. Felizmente, você tem o GitHub, então consegui encontrar e instalar os pré-requisitos (preprossing.mqh, plots.mqh, Matrixextend.mqh, metrics.mqh e Random Forest.mqh). Infelizmente, fico preso na mensagem “Init - Identificador não declarado” na linha if (!random_forest.Init(StringFormat("%s.%s.%s.onnx", symbol_, EnumToString(timeframe_), technique_name), ONNX_COMMON_FOLDER)) // Inicializando o RFC no formato ONNX a partir de uma pasta comum. Verifiquei e, de fato, tenho o arquivo USDJPY.PERIOD_D1.randomundersampling.onnx na pasta MQL5\Common
Omega J Msigwa
Omega J Msigwa | 24 abr. 2025 em 06:59
linfo2 USDJPY.PERIOD_D1.randomundersampling.onnx na pasta MQL5\Common

Os componentes necessários são a versão mais recente de tudo o que foi importado para o notebook; você pode executar o comando `pip install` sem se preocupar com conflitos de versões. Como alternativa, você pode seguir o link na tabela de anexos, que o levará ao site Kaggle.com, onde é possível editar e modificar o código.

Identificador não declarado: isso pode significar que uma variável ou um objeto não está definido. Verifique seu código ou me envie uma mensagem direta com uma captura de tela do código.

zhai nan
zhai nan | 14 jun. 2025 em 11:52
Os resultados do teste não são confiáveis. O período do teste abrange o mesmo período dos dados de treinamento. O modelo terá um bom desempenho no conjunto de treinamento e um desempenho ruim no conjunto de teste. É possível observar que muitos resultados do teste estão no intervalo de tempo de 06/07/2024 a 01/01/2025. O modelo apresenta um desempenho ruim, enquanto o desempenho do modelo no período anterior é excelente. Calculei que o conjunto de treinamento representa 80%. Os dados não são randomizados para alocação aleatória, e os conjuntos de treinamento e teste são alocados diretamente em ordem. O modelo reconhece os dados de treinamento anteriores, mas não os dados de teste subsequentes. Você utilizou os dados de treinamento no teste, o que levará a resultados gerais inflacionados, e o desempenho nos dados de teste subsequentes é muito ruim, com perdas. Assim, o centroide do cluster também não é confiável. O agrupamento por clusters utiliza todos os dados, e os dados gerais serão usados para o agrupamento e, em seguida, para o treinamento. Portanto, os dados de treinamento utilizados por esse método abrangem o intervalo de tempo de 01/01/2023 a 01/01/2025. Pode-se dizer que se trata de um teste realizado inteiramente com os dados de treinamento, sem avaliação em dados desconhecidos. Os dados de treinamento e os dados de teste devem ser rigorosamente diferenciados em ordem cronológica. Somente os dados de teste podem ser utilizados para avaliar o desempenho do modelo, como, por exemplo, 01/01/2023 a 01/01/2024 como dados de treinamento e 01/01/2024 a 01/01/2025 como dados de teste. O modelo de teste só pode ser testado utilizando dados de teste. Não deve haver risco de vazamento de informações; caso contrário, todo o bom desempenho do modelo será considerado não confiável.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 60): Aprendizado por Inferência (Wasserstein-VAE) com Padrões da Média Móvel e do Oscilador Estocástico Técnicas do MQL5 Wizard que você deve conhecer (Parte 60): Aprendizado por Inferência (Wasserstein-VAE) com Padrões da Média Móvel e do Oscilador Estocástico
Concluímos nossa análise sobre o uso complementar da Média Móvel (MA) e do Oscilador Estocástico examinando o papel que o aprendizado por inferência pode desempenhar após as etapas de aprendizado supervisionado e aprendizado por reforço. Existem diversas maneiras de abordar o aprendizado por inferência nesse contexto. No entanto, nossa abordagem consiste em utilizar Variational Autoencoders (VAEs). Primeiro, exploraremos essa abordagem em Python e, em seguida, exportaremos nosso modelo treinado no formato ONNX para utilizá-lo em um Expert Advisor desenvolvido com o MQL5 Wizard no MetaTrader.
Recursos do Assistente MQL5 que você precisa conhecer (Parte 67): Uso dos padrões do TRIX e da Faixa Percentual de Williams Recursos do Assistente MQL5 que você precisa conhecer (Parte 67): Uso dos padrões do TRIX e da Faixa Percentual de Williams
O Oscilador de Média Móvel Exponencial Tripla (Triple Exponential Moving Average Oscillator, TRIX) e o oscilador Faixa Percentual de Williams (Williams Percentage Range Oscillator) formam mais um par de indicadores que pode ser utilizado em conjunto em um EA MQL5. Assim como os pares analisados recentemente, esses dois indicadores se complementam: o TRIX identifica a tendência, enquanto a Faixa Percentual de Williams confirma os níveis de suporte e resistência. Como sempre, utilizamos o Assistente MQL5 para avaliar o potencial desses indicadores.
Automatizando Estratégias de Trading em MQL5 (Parte 15): Padrão Harmônico Cypher Baseado em Price Action com Visualização Automatizando Estratégias de Trading em MQL5 (Parte 15): Padrão Harmônico Cypher Baseado em Price Action com Visualização
Neste artigo, exploramos a automatização do padrão harmônico Cypher em MQL5, detalhando sua detecção e visualização nos gráficos do MetaTrader 5. Implementamos um Expert Advisor que identifica pontos de oscilação, valida padrões com base em Fibonacci e executa operações com anotações gráficas claras. O artigo termina com orientações sobre backtesting e otimização do programa para tornar a negociação mais eficiente.
Criação de EAs auto-otimizáveis em MQL5 (Parte 7): Negociação simultânea com indicadores de vários períodos Criação de EAs auto-otimizáveis em MQL5 (Parte 7): Negociação simultânea com indicadores de vários períodos
Nesta série de artigos, analisamos várias maneiras de determinar o melhor período para utilizar nossos indicadores técnicos. Hoje, mostraremos como aplicar a lógica oposta, ou seja, em vez de selecionar um único período considerado o mais adequado, veremos como utilizar de forma eficiente todos os períodos disponíveis. Essa abordagem reduz a quantidade de dados descartados e oferece formas alternativas de aplicar algoritmos de machine learning, indo além da simples previsão de preços.