English Русский 中文 Español Deutsch 日本語
preview
Machine Learning e Data Science (Parte 38): Aplicação de Transfer Learning nos mercados de câmbio

Machine Learning e Data Science (Parte 38): Aplicação de Transfer Learning nos mercados de câmbio

MetaTrader 5Experts |
23 1
Omega J Msigwa
Omega J Msigwa

Conteúdo


O que é Transfer Learning?

Transfer Learning (aprendizado por transferência) é uma técnica de machine learning em que um modelo treinado para uma determinada tarefa é usado como base para resolver outra.

Em vez de treinar um modelo de machine learning do zero, transferimos o conhecimento adquirido por um modelo previamente treinado e realizamos uma atualização incremental do modelo para adaptá-lo a uma nova tarefa específica. Essa abordagem é especialmente útil quando:

  1. Não dispomos de dados rotulados suficientes para uma tarefa específica.
  2. Treinar um modelo do zero levaria tempo demais ou exigiria recursos computacionais excessivos.
  3. A tarefa atual apresenta semelhanças com aquela em que o modelo original foi treinado.

Veja um exemplo prático de uso de Transfer Learning:

Suponha que você esteja criando um classificador de imagens para distinguir "gato" de "cachorro", mas disponha de apenas 1.000 imagens. Treinar uma CNN profunda do zero seria difícil. Em vez disso, podemos usar um modelo como o ResNet50 ou o VGG16, já treinado no ImageNet, com milhões de imagens distribuídas em 1.000 classes, utilizar suas camadas convolucionais como extratores de atributos, adicionar nossas próprias camadas de classificação e realizar uma atualização incremental do modelo com um conjunto menor de imagens de gatos e cachorros.

Esse procedimento permite reutilizar o conhecimento adquirido pelo modelo, simplificando significativamente o desenvolvimento. Não é necessário reinventar a roda a cada vez: em vez de treinar um modelo do zero, podemos escalar o desenvolvimento aproveitando modelos existentes voltados a tarefas semelhantes.

Em geral, quem sabe patinar ou pratica patinação regularmente também consegue se sair razoavelmente bem no esqui, e vice-versa, mesmo sem treinamento intensivo em ambas as modalidades. Isso acontece porque os dois esportes compartilham elementos semelhantes.

O mesmo princípio se aplica aos mercados financeiros. Embora existam diferentes instrumentos (símbolos) representando diferentes ativos ou mercados financeiros, a maioria dos mercados apresenta comportamentos semelhantes, pois todos são movidos pela oferta e pela demanda.

Se analisarmos o mercado do ponto de vista técnico, veremos que todos os mercados sobem e descem, apresentam padrões de candles semelhantes, e os indicadores geram sinais parecidos em diferentes instrumentos, entre outros exemplos. É justamente por isso que muitas vezes estudamos uma estratégia de negociação baseada em análise técnica usando um determinado instrumento e depois aplicamos o conhecimento adquirido a outros mercados, independentemente das diferenças de escala de preços.

No entanto, os modelos de machine learning muitas vezes não reconhecem que esses mercados são comparáveis. Neste artigo, veremos como usar Transfer Learning para ajudar os modelos a identificar padrões em diferentes instrumentos financeiros e, assim, tornar o treinamento mais eficiente. Também analisaremos as vantagens e desvantagens desse método, além de aspectos importantes que devem ser considerados ao aplicá-lo.



Como funciona o Transfer Learning?

Transfer Learning é uma forma eficiente de reutilizar o conhecimento adquirido por um modelo ao resolver uma tarefa e aplicá-lo a outra tarefa relacionada. Isso economiza tempo e, muitas vezes, melhora o desempenho do modelo.

O que é um modelo pré-treinado?

Começamos com um modelo que já foi treinado em um grande conjunto de dados para executar a tarefa A. Esse modelo aprendeu a reconhecer padrões e atributos gerais que também podem ser úteis em tarefas semelhantes.

No trading, por exemplo, pode ser um modelo treinado com uma determinada estratégia ou com dados de um símbolo e que já compreende comportamentos típicos do mercado também presentes em outros instrumentos de câmbio.

Como o conhecimento é transferido?

Quando usamos uma rede neural, como uma CNN ou RNN, podemos aproveitar as camadas iniciais, responsáveis por capturar atributos mais gerais, e reutilizá-las. Essas camadas funcionam como uma base, identificando padrões amplos que são úteis tanto para a tarefa original quanto para a nova.

Atualização incremental do modelo para a nova tarefa

Em seguida, adaptamos o modelo à tarefa B, por exemplo, a outro instrumento ou estratégia, e ajustamos determinadas camadas ou parâmetros para que ele funcione corretamente com os novos dados. Essa etapa adapta o modelo às características da nova situação.


Por que usar Transfer Learning?

1. Treinamento mais rápido

Em vez de começar do zero, reutilizamos atributos que já foram extraídos anteriormente. Isso reduz significativamente o tempo de treinamento, especialmente em deep learning, em que essa abordagem pode economizar horas ou até mesmo dias.

2. Frequentemente melhora a acurácia

Modelos que utilizam Transfer Learning costumam apresentar resultados melhores, principalmente quando há poucos dados rotulados disponíveis. Um modelo pré-treinado já sabe identificar sinais importantes, como setups de trading ou indicadores, o que ajuda a produzir decisões mais bem fundamentadas na nova tarefa.

3. Funciona mesmo com conjuntos de dados pequenos ou ruidosos

Em alguns símbolos, pode ser difícil obter dados históricos ou dados de ticks de boa qualidade no MetaTrader 5. Alguns instrumentos simplesmente não dispõem de um volume de dados suficiente. Ao usar um modelo treinado em um conjunto de dados mais rico, podemos evitar o sobreajuste e construir um modelo robusto mesmo quando os dados disponíveis são limitados.

4. Reutilização do conhecimento entre instrumentos

Os mercados frequentemente apresentam comportamentos semelhantes do ponto de vista técnico. Em vez de treinar um novo modelo para cada símbolo, podemos reutilizar o mesmo conhecimento em diferentes instrumentos. Isso economiza tempo e aumenta a consistência dos resultados.


Modelo-base simples

Vamos treinar um classificador Random Forest simples como ponto de partida, ou seja, como modelo-base. Para simplificar, podemos usar os valores OHLC (Open, High, Low, Close).

Primeiro, vamos coletar dados OHLC dos principais e secundários pares de moedas e incluir também metais.

#include <pandas.mqh> //https://www.mql5.com/en/articles/17030

input datetime start_date = D'2005.01.01';
input datetime end_date = D'2023.01.01';

input string symbols = "EURUSD|GBPUSD|AUDUSD|USDCAD|USDJPY|USDCHF|NZDUSD|EURNZD|AUDNZD|GBPNZD|NZDCHF|NZDJPY|NZDCAD|XAUUSD|XAUJPY|XAUEUR|XAUGBP";
input ENUM_TIMEFRAMES timeframe = PERIOD_D1;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {

   string SymbolsArr[];
   ushort sep = StringGetCharacter("|",0);
   if (StringSplit(symbols, sep, SymbolsArr)<0)
     {
       printf("%s failed to split the symbols, Error %d",__FUNCTION__,GetLastError()); 
       return;
     }
     
//---
   
   vector open, high, low, close;
   for (uint i=0; i<SymbolsArr.Size(); i++)
    {
      string symbol = SymbolsArr[i];
      if (!SymbolSelect(symbol, true))
         {
            printf("%s failed to select symbol %s, Error = %d",__FUNCTION__,symbol,GetLastError());
            continue;
         }
      
    //---
    
      open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date);
      high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date);
      low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date);
      close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date);
   
      CDataFrame df;
      
      df.insert("Open", open);
      df.insert("High", high);
      df.insert("Low", low);
      df.insert("Close", close);
      
      df.to_csv(StringFormat("Fxdata.%s.%s.csv",symbol,EnumToString(timeframe)), true);
    }
  }

Depois de coletar os dados, podemos carregar diretamente os arquivos CSV em um script Python.

def getXandY(symbol: str, timeframe: str, lookahead: int) -> tuple:

    df = pd.read_csv(f"/kaggle/input/ohlc-eurusd/Fxdata.{symbol}.{timeframe}.csv")

    # Target variable

    df["future_close"] = df["Close"].shift(-lookahead)
    df.dropna(inplace=True)
    
    df["Signal"] = (df["future_close"] > df["Close"]).astype(int)
    
    # Splitting data into X and y

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

    y = df["Signal"]

    return (X, y)

Após a leitura do arquivo CSV, a função getXandY gera a variável-alvo com base em uma lógica simples: se o fechamento da próxima barra estiver acima do fechamento atual, temos um sinal de alta; se estiver abaixo, temos um sinal de baixa.

Vamos criar uma função para treinar o modelo com base em X e y e retornar o modelo treinado na forma de um pipeline do Scikit-learn.

def trainSymbol(X_train: pd.DataFrame, y_train: pd.DataFrame) -> Pipeline:
    
    # Training a model

    classifier = RandomForestClassifier(n_estimators=100, min_samples_split=3, max_depth = 5)

    pipeline = Pipeline([
        ("scaler", RobustScaler()),
        ("classifier", classifier)
    ])

    pipeline.fit(X_train, y_train)

    return pipeline

Também é útil implementar uma função para avaliar o modelo em diferentes instrumentos.

def evalSymbol(model: Pipeline, X: pd.DataFrame , y: pd.Series) -> int:
    
    # evaluating the model

    preds = model.predict(X)
    acc = accuracy_score(y, preds)
    
    return acc

Vamos treinar o modelo-base no EURUSD e, em seguida, avaliar seus resultados nos demais símbolos.

symbols = ["EURUSD","GBPUSD","AUDUSD","USDCAD","USDJPY","USDCHF","NZDUSD","EURNZD","AUDNZD","GBPNZD","NZDCHF","NZDJPY","NZDCAD","XAUUSD","XAUJPY","XAUEUR","XAUGBP"]

# training on EURUSD

lookahead = 1

X, y = getXandY(symbol=symbols[0], timeframe="PERIOD_H4", lookahead=lookahead)

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42, shuffle=True)

model = trainSymbol(X_train, y_train)

# Evaluating on the rest of symbols

trained_symbol = symbols[0]

print(f"Trained on {trained_symbol}")
for symbol in symbols:
    X, y = getXandY(symbol=symbol, timeframe="PERIOD_H4", lookahead=1)
    
    acc = evalSymbol(model, X, y)    
    print(f"--> {symbol} | acc: {acc}")

Resultados.

Trained on EURUSD
--> EURUSD | acc: 0.5478518727715607
--> GBPUSD | acc: 0.5009182736455464
--> AUDUSD | acc: 0.5026133634694165
--> USDCAD | acc: 0.4973701860284514
--> USDJPY | acc: 0.49477401129943505
--> USDCHF | acc: 0.5078731817539895
--> NZDUSD | acc: 0.4976826463824518
--> EURNZD | acc: 0.5071507150715071
--> AUDNZD | acc: 0.5005597760895641
--> GBPNZD | acc: 0.503459397596629
--> NZDCHF | acc: 0.4990389436737423
--> NZDJPY | acc: 0.4908841561794127
--> NZDCAD | acc: 0.5023507681974645
--> XAUUSD | acc: 0.48674396277970605
--> XAUJPY | acc: 0.4816082121471343
--> XAUEUR | acc: 0.4925268155442237
--> XAUGBP | acc: 0.49455864570737607

O modelo apresentou acurácia de 0,54 no instrumento em que foi treinado e entre 0,48 e 0,50 nos demais. À primeira vista, pode parecer que os resultados não diferem muito e que o modelo também funciona em outros instrumentos. No entanto, esse desempenho é extremamente ruim.

Uma acurácia de 0,5 (50%) equivale a lançar uma moeda.

Embora o modelo-base aparentemente consiga fazer previsões em outros instrumentos, surge um problema importante relacionado aos atributos contínuos, ou seja, aos valores OHLC.


O problema das variáveis contínuas

Como queremos criar um modelo-base robusto e universal, capaz de funcionar em diferentes símbolos, atributos contínuos como Open, High, Low e Close não são adequados. Isso ocorre porque eles não contêm padrões estáveis, refletindo apenas o movimento histórico dos preços.

Além disso, cada instrumento possui sua própria escala de preços. Por exemplo, os preços de fechamento de hoje são os seguintes:

SYMBOL DAILY CLOSING PRICE
USDJPY 142.17
EURUSD 1.13839
XAUUSD 3305.02

Isso significa que um modelo treinado em um instrumento pode apresentar desempenho ruim em outros devido às diferenças entre as faixas de preço.

Além de não conterem padrões transferíveis, modelos baseados em variáveis contínuas precisam ser treinados novamente com frequência, pois os mercados atingem novos máximos regularmente. Isso aumenta o custo computacional, exatamente o que queremos minimizar com Transfer Learning.

Somente variáveis estacionárias podem ajudar os modelos de machine learning a identificar padrões relevantes em diferentes mercados, pois seu valor esperado, sua variância e sua autocorrelação não se alteram ao longo do tempo, permanecendo constantes. Essa estabilidade estatística pode ser observada em diferentes instrumentos.

Para usar Transfer Learning, todos os atributos, incluindo indicadores e padrões, devem ser constantes ou estacionários.

Por exemplo, os valores do indicador RSI permanecem sempre no intervalo de 0 a 100, independentemente do instrumento, o que é fundamental para a identificação de padrões.

Engenharia de atributos

Existem diversos métodos para obter atributos estacionários. No nosso caso, podemos usar a variação percentual do preço de fechamento, as diferenças (diff) entre valores consecutivos de cada série OHLC ou indicadores estacionários.

(a) Variação percentual do preço de fechamento

res_df["pct_change"] = df["Close"].pct_change()

Apesar das diferenças na escala de preços, as variações percentuais permanecem comparáveis e são adequadas para identificar padrões de forma consistente entre diferentes instrumentos.

(b) Diferença entre os valores OHLC

res_df["diff_open"] = df["Open"].diff()
res_df["diff_high"] = df["High"].diff()
res_df["diff_low"] = df["Low"].diff()
res_df["diff_close"] = df["Close"].diff()

O método diff() calcula, por padrão, a diferença entre o valor atual e o anterior. Isso permite acompanhar como o preço de cada barra varia em relação à barra precedente em cada instrumento.

(c) Indicadores estacionários

Também podemos adicionar osciladores e indicadores de momentum que produzam valores estacionários.


Indicador

Intervalo de valores
# Relative Strength Index (RSI)
res_df['rsi'] = ta.momentum.RSIIndicator(df["Close"], window=14).rsi()
De 0 a 100.
# Stochastic Oscillator (Stoch)
res_df['stoch_k'] = ta.momentum.StochasticOscillator(df['High'], df['Low'], df['Close'], window=14).stoch()
De 0 a 100.
# Moving Average Convergence Divergence (MACD)
res_df['macd'] = ta.trend.MACD(df["Close"]).macd()
Pequenos valores positivos e negativos, geralmente entre -0,1 e +0,1.
# Commodity Channel Index (CCI)
res_df['cci'] = ta.trend.CCIIndicator(df['High'], df['Low'], df['Close'], window=20).cci()
Geralmente entre -300 e +300.
# Rate of Change (ROC)
res_df['roc'] = ta.momentum.ROCIndicator(df["Close"], window=12).roc()
Valor sem limite definido, podendo ser negativo ou positivo.
# Ultimate Oscillator (UO)
res_df['uo'] = ta.momentum.UltimateOscillator(df['High'], df['Low'], df['Close'], window1=7, window2=14, window3=28).ultimate_oscillator()
De 0 a 100.
# Williams %R
res_df['williams_r'] = ta.momentum.WilliamsRIndicator(df['High'], df['Low'], df['Close']).williams_r()
De -100 a 0.
# Average True Range (ATR)
res_df['atr'] = ta.volatility.AverageTrueRange(df['High'], df['Low'], df['Close'], window=14).average_true_range()
Pequenos valores positivos sem limite definido.
# Awesome Oscillator (AO)
res_df['ao'] = ta.momentum.AwesomeOscillatorIndicator(df['High'], df['Low']).awesome_oscillator()
Pequenos valores sem limite definido, geralmente entre -0,1 e +0,1.
# Average Directional Index (ADX)
res_df['adx'] = ta.trend.ADXIndicator(df['High'], df['Low'], df['Close'], window=14).adx()
De 0 a 100.
# True Strength Index (TSI)
res_df['tsi'] = ta.momentum.TSIIndicator(df['Close'], window_slow=25, window_fast=13).tsi()
Geralmente entre -100 e +100.

Esses são alguns exemplos de variáveis estacionárias. Você pode acrescentar outras de sua preferência.

Todas essas operações podem ser reunidas em uma função separada.

def getStationaryVars(df: pd.DataFrame) -> pd.DataFrame:

    res_df = pd.DataFrame()
    
    res_df["pct_change"] = df["Close"].pct_change()
    res_df["diff_open"] = df["Open"].diff()
    res_df["diff_high"] = df["High"].diff()
    res_df["diff_low"] = df["Low"].diff()
    res_df["diff_close"] = df["Close"].diff()
    
    # Relative Strength Index (RSI)
    res_df['rsi'] = ta.momentum.RSIIndicator(df["Close"], window=14).rsi()
    
    # Stochastic Oscillator (Stoch)
    res_df['stoch_k'] = ta.momentum.StochasticOscillator(df['High'], df['Low'], df['Close'], window=14).stoch()
    
    # Moving Average Convergence Divergence (MACD)
    res_df['macd'] = ta.trend.MACD(df["Close"]).macd()
    
    # Commodity Channel Index (CCI)
    res_df['cci'] = ta.trend.CCIIndicator(df['High'], df['Low'], df['Close'], window=20).cci()

# .... See the code in the notebook in the attachments and above 
# ....
# ....

    # True Strength Index (TSI)
    res_df['tsi'] = ta.momentum.TSIIndicator(df['Close'], window_slow=25, window_fast=13).tsi()
    
    return res_df

Agora vamos criar um modelo-base para transferir conhecimento entre diferentes instrumentos.


Transfer Learning

O Transfer Learning é aplicado com mais frequência a modelos de deep learning, principalmente a redes neurais convolucionais (CNN), por sua eficácia na extração de padrões.

Vamos encapsular o modelo CNN em uma função trainCNN.

import tensorflow as tf
from tensorflow.keras import layers, models, Model
from tensorflow.keras.callbacks import EarlyStopping
def trainCNN(train_set: tuple, val_set: tuple, learning_rate: float=1e-3, epochs: int=100, batch_size: int=32):
    
    X_train, y_train = train_set
    X_val, y_val = val_set

    input_shape = X_train.shape[1:]  
    num_classes = len(np.unique(y_train))

    model = models.Sequential([
        layers.Input(shape=input_shape),

        layers.Conv1D(64, kernel_size=3, activation='relu', padding='same'),
        layers.Conv1D(64, kernel_size=3, activation='relu', padding='same'),
        layers.GlobalAveragePooling1D(),

        layers.Dense(32, activation='tanh'),
        layers.Dense(num_classes, activation='softmax')
    ])

    # Compile with Adam optimizer
    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss='categorical_crossentropy',
        metrics=['accuracy']
    )

    # Early stopping callback
    early_stop = EarlyStopping(
        monitor='val_loss',    # Watch validation loss
        patience=10,            # Stop if no improvement 
        restore_best_weights=True
    )

    # Train the model
    model.fit(
        X_train, y_train,
        validation_data=(X_val, y_val),
        epochs=epochs,
        batch_size=batch_size,
        callbacks=[early_stop],
        verbose=1
    )

    # Save trained weights
    model.save_weights('cnn_pretrained.weights.h5')

    return model

Esse modelo sequencial contém duas camadas convolucionais unidimensionais (Conv1D) para extrair atributos da sequência de entrada.

A camada de global average pooling reduz a dimensionalidade antes que os dados sejam encaminhados para uma camada totalmente conectada (FNN

) com ativação tanh. A última camada utiliza softmax para obter as probabilidades das classes.

Salvamos os pesos do modelo, pois eles representam os padrões aprendidos e podem ser transferidos para outro modelo.

Usando o EURUSD no timeframe H4, obtemos os dados OHLC.

lookahead = 1
trained_symbol = symbols[0]
timeframe = "PERIOD_H4"

df = pd.read_csv(f"/kaggle/input/ohlc-eurusd/Fxdata.{trained_symbol}.{timeframe}.csv")

stationary_df = getStationaryVars(df)

stationary_df["Close"] = df["Close"] # add the close price for crafting the target variable

X, y = getXandY(df=stationary_df, lookahead=lookahead)

A função getXandY volta a gerar a variável-alvo com base no valor de Close e no parâmetro lookahead (1).

def getXandY(df: pd.DataFrame, lookahead: int) -> tuple:

    # Target variable

    df["future_close"] = df["Close"].shift(-lookahead)
    df.dropna(inplace=True)
    
    df["Signal"] = (df["future_close"] > df["Close"]).astype(int) # if next bar closed above the current one, thats a bullish signal otherwise bearish
    
    # Splitting data into X and y

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

    y = df["Signal"]

    return (X, y)

Os dados são divididos em conjuntos de treinamento e validação e, em seguida, escalados com um scaler. No nosso caso, usamos o Robust Scaler.

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

# Scalling the data

scaler = RobustScaler()

X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

Como a CNN exige dados de entrada em três dimensões, podemos reorganizá-los em janelas temporais para que o modelo identifique padrões dentro de cada janela.

def create_sequences(X, Y, time_step):

    if len(X) != len(Y):
        raise ValueError("X and y must have the same length")
    
    X = np.array(X)
    Y = np.array(Y)
    
    Xs, Ys = [], []
    
    for i in range(X.shape[0] - time_step):
        Xs.append(X[i:(i + time_step), :])  # Include all features with slicing
        Ys.append(Y[i + time_step])
        
    return np.array(Xs), np.array(Ys)
# Prepare data within a window

window = 10

X_train_seq, y_train_seq = create_sequences(X_train_scaled, y_train, window)
X_test_seq, y_test_seq = create_sequences(X_test_scaled, y_test, window)

Neste caso, aplicamos codificação one-hot à variável-alvo para representar as diferentes classes, pois isso permite distinguir as diferentes classes.

# One-hot encode the labels for multi-class classification

y_train_encoded = to_categorical(y_train_seq, num_classes=num_classes)
y_test_encoded = to_categorical(y_test_seq, num_classes=num_classes)

Por fim, podemos treinar o modelo-base.

base_model = trainCNN(train_set=(X_train_seq, y_train_encoded),
                 val_set=(X_test_seq, y_test_encoded),
                 learning_rate = 0.01,
                 epochs = 1000,
                 batch_size =32)

print("Test acc: ", base_model.evaluate(X_test_seq, y_test_encoded)[1])

Resultados.

Epoch 1/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 4s 4ms/step - accuracy: 0.4994 - loss: 0.6990 - val_accuracy: 0.5023 - val_loss: 0.6938
Epoch 2/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4976 - loss: 0.6939 - val_accuracy: 0.5023 - val_loss: 0.6936
Epoch 3/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4977 - loss: 0.6940 - val_accuracy: 0.5023 - val_loss: 0.6938
Epoch 4/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.5034 - loss: 0.6937 - val_accuracy: 0.4977 - val_loss: 0.6962
...
...
Epoch 16/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.5039 - loss: 0.6934 - val_accuracy: 0.5023 - val_loss: 0.6932
Epoch 17/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.4988 - loss: 0.6940 - val_accuracy: 0.4977 - val_loss: 0.6937
Epoch 18/1000
620/620 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - accuracy: 0.5013 - loss: 0.6943 - val_accuracy: 0.5023 - val_loss: 0.6931
266/266 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - accuracy: 0.5037 - loss: 0.6931
Test acc:  0.5022971034049988

Acabamos de treinar o modelo-base no EURUSD e obtivemos uma acurácia geral de 0,502.

Agora vamos usar esse modelo para aplicar Transfer Learning e transferir o conhecimento adquirido para modelos destinados a diferentes instrumentos. Vamos ver quais resultados serão obtidos.

for symbol in symbols:

    if symbol == trained_symbol: # skip transfer learning on the trained symbol
        continue
    
    print(f"Symbol: {symbol}")
    
    df = pd.read_csv(f"/kaggle/input/ohlc-eurusd/Fxdata.{symbol}.{timeframe}.csv")
    
    stationary_df = getStationaryVars(df)
    stationary_df["Close"] = df["Close"] # we add the close price for crafting the target variable
    
    X, y = getXandY(df=stationary_df, lookahead=lookahead)
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, shuffle=False, random_state=42)

    # Scalling the data
    
    scaler = RobustScaler()
    
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled = scaler.transform(X_test)

    # Prepare data within a window
    
    window = 10
    
    X_train_seq, y_train_seq = create_sequences(X_train_scaled, y_train, window)
    X_test_seq, y_test_seq = create_sequences(X_test_scaled, y_test, window)
    
    # One-hot encode the labels for multi-class classification
    
    y_train_encoded = to_categorical(y_train_seq, num_classes=num_classes)
    y_test_encoded = to_categorical(y_test_seq, num_classes=num_classes)    
    
    # Freeze all layers except the last one
    for layer in base_model.layers[:-1]:
        layer.trainable = False
    
    # Create new model using the base model's architecture
    model = models.clone_model(base_model)
    model.set_weights(base_model.get_weights())
    
    # Recompile with lower learning rate
    model.compile(optimizer=tf.keras.optimizers.Adam(0.01),
                  loss='categorical_crossentropy',
                  metrics=['accuracy'])
    
    early_stop = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)
    
    history = model.fit(X_train_seq, y_train_encoded, 
                       validation_data=(X_test_seq, y_test_encoded),
                       epochs=1000,  # More epochs for fine-tuning
                       batch_size=32,
                       callbacks=[early_stop],
                       verbose=1)
    
    print("Test acc:", model.evaluate(X_test_seq, y_test_encoded)[1])

As mesmas etapas são repetidas para dividir os dados, criar as sequências temporais e codificar a variável-alvo. Essas etapas são executadas separadamente para cada símbolo.

Para transferir o conhecimento, criamos um novo modelo com base no modelo-base e congelamos algumas camadas da CNN.

Congelamos todas as camadas da CNN, exceto a última, para que o modelo preserve os padrões aprendidos com os dados-base. Isso permite manter o conhecimento já adquirido e evita que esses padrões sejam alterados durante a atualização incremental do modelo.

Mantemos apenas a última camada descongelada para que seus pesos sejam recalibrados de acordo com os novos limites de decisão de cada símbolo. Em essência, fornecemos ao modelo novas distribuições da variável-alvo e permitimos que ele estabeleça relações entre os padrões aprendidos pelo modelo-base e as informações contidas na variável-alvo desses novos dados.

A arquitetura é clonada e os pesos são carregados a partir do modelo salvo. Vale lembrar que salvamos os pesos na função trainCNN. Você pode carregar os pesos de um arquivo ao importar o modelo de outra origem ou carregá-los diretamente do objeto do modelo, caso o modelo-base esteja no mesmo script ou arquivo Python, como descrito anteriormente.

Em seguida, compilamos o modelo usando dados de mercado obtidos de um instrumento diferente daquele utilizado no treinamento do modelo-base. Outros parâmetros também podem ser alterados.

Acurácia para diferentes símbolos:

SYMBOL GBPUSD AUDUSD USDCAD USDJPY USDCHF NZDUSD EURNZD AUDNZD GBPNZD NZDCHF NZDJPY NZDCAD XAUUSD XAUJPY XAUEUR XAUGBP
ACCURACY 0.505 0.506 0.501 0.516 0.506 0.497 0.505 0.502 0.504 0.505 0.51 0.505 0.506 0.514 0.507 0.504

Esse resultado, por si só, não nos diz muita coisa. Vamos analisar o relatório de classificação com mais detalhes.

preds = base_model.predict(X_test_seq)
pred_indices = preds.argmax(axis=1)  
pred_class_labels = [classes_in_y[i] for i in pred_indices]

print("Classification report\n", classification_report(pred_class_labels, y_test_seq))

Relatório do modelo-base.

Classification report
               precision    recall  f1-score   support

           0       1.00      0.50      0.66      8477
           1       0.00      0.00      0.00         0

    accuracy                           0.50      8477
   macro avg       0.50      0.25      0.33      8477
weighted avg       1.00      0.50      0.66      8477

Os resultados de acurácia nos diferentes símbolos foram muito ruins. O modelo apresentou um forte viés.

Isso indica que existe um problema de viés no nosso modelo, que pode ter origem tanto na própria arquitetura quanto nos dados.

Existem várias formas de lidar com esse problema, como vimos no artigo anterior. Por enquanto, vamos fazer alguns ajustes:

(a) Adicionar pesos às classes para corrigir o desbalanceamento.

from sklearn.utils.class_weight import compute_class_weight
def trainCNN:

#....
#....

    y_train_integers = np.argmax(y_train, axis=1) # return to non-one hot encoded
    class_weights = compute_class_weight('balanced', 
                                       classes=np.unique(y_train_integers), 
                                       y=y_train_integers)
    class_weight_dict = {i: weight for i, weight in enumerate(class_weights)}

    
    # Train the model
    model.fit(
        X_train, y_train,
        validation_data=(X_val, y_val),
        epochs=epochs,
        batch_size=batch_size,
        callbacks=[early_stop],
        class_weight=class_weight_dict,
        verbose=1
    )

(b) Adicionar mais uma camada convolucional e aumentar o número de neurônios na camada totalmente conectada.

def trainCNN:
    
# ...
# ...

    model = models.Sequential([
        layers.Input(shape=input_shape),

        layers.Conv1D(64, kernel_size=3, activation='relu', padding='same'),
        layers.Conv1D(64, kernel_size=3, activation='relu', padding='same'),
        layers.Conv1D(32, kernel_size=3, activation='relu', padding='same'),
        layers.GlobalAveragePooling1D(),

        layers.Dense(128, activation='relu'),
        layers.Dense(num_classes, activation='softmax')
    ])

(c) Como se trata de uma tarefa de classificação binária, nossa variável-alvo possui apenas duas classes: 0 para sinais de venda e 1 para sinais de compra. Vamos alterar a função de perda para binary_crossentropy e a métrica de avaliação para binary_accuracy.

    # Compile with Adam optimizer
    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss='binary_crossentropy',
        metrics=['binary_accuracy']
    )

Depois de treinar novamente o modelo CNN, os resultados melhoraram significativamente.

....
....
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - binary_accuracy: 0.5257 - loss: 0.6920 - val_binary_accuracy: 0.5043 - val_loss: 0.6933
Epoch 7/100
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - binary_accuracy: 0.5259 - loss: 0.6918 - val_binary_accuracy: 0.5027 - val_loss: 0.6934
Epoch 8/100
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - binary_accuracy: 0.5283 - loss: 0.6915 - val_binary_accuracy: 0.5042 - val_loss: 0.6936
Epoch 9/100
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - binary_accuracy: 0.5284 - loss: 0.6912 - val_binary_accuracy: 0.5028 - val_loss: 0.6937
Epoch 10/100
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - binary_accuracy: 0.5315 - loss: 0.6909 - val_binary_accuracy: 0.5036 - val_loss: 0.6938
Epoch 11/100
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - binary_accuracy: 0.5295 - loss: 0.6907 - val_binary_accuracy: 0.5042 - val_loss: 0.6940
Epoch 12/100
310/310 ━━━━━━━━━━━━━━━━━━━━ 1s 3ms/step - binary_accuracy: 0.5298 - loss: 0.6904 - val_binary_accuracy: 0.5074 - val_loss: 0.6941
619/619 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - binary_accuracy: 0.5101 - loss: 0.6926
265/265 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - binary_accuracy: 0.5018 - loss: 0.6933
Train acc: 0.5114434361457825 Test acc: 0.5050135850906372
265/265 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step
Classification report
               precision    recall  f1-score   support

           0       0.58      0.50      0.54      4870
           1       0.43      0.51      0.47      3607

    accuracy                           0.51      8477
   macro avg       0.51      0.51      0.50      8477
weighted avg       0.52      0.51      0.51      8477

Não houve um motivo específico para alterar esses parâmetros. Minha intenção foi apenas mostrar que, assim como em qualquer outro modelo baseado em redes neurais, a otimização e o ajuste dos parâmetros são extremamente importantes.

O resultado obtido talvez ainda não represente a solução ideal, pois existem inúmeras alternativas tanto para redes neurais convolucionais quanto para redes neurais em geral.

Por enquanto, vamos manter os parâmetros atuais, mas você pode experimentar outros valores.

Agora que temos um modelo-base razoavelmente satisfatório, podemos usá-lo para transferir conhecimento para outros instrumentos e salvar todos esses modelos no formato ONNX para testá-los no ambiente de trading.


Transfer Learning no EA

Para testar os modelos no ambiente de trading do MetaTrader 5, precisamos salvá-los no formato ONNX e carregá-los por meio da linguagem de programação MQL5.

Importação.

import onnxmltools
import tf2onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

Funções

def saveCNN(model, window: int, features: int, filename: str):

    model.output_names = ["output"]
    # Specifying the input signature for the model
    spec = (tf.TensorSpec((None, window, features), tf.float16, name="input"),)
    
    # Convert the Keras model to ONNX format
    onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=14)
    
    # Save the ONNX model to a file
    with open(filename, "wb") as f:
        f.write(onnx_model.SerializeToString())

Os modelos Keras não dispõem de um pipeline compatível com o que usamos para integrar todas as etapas de pré-processamento ao modelo Scikit-learn. Esse pipeline permitiria salvar o modelo e todas as etapas em um único arquivo ONNX. Por isso, será necessário salvar separadamente o modelo Keras e o scaler utilizado, em arquivos ONNX independentes.

def saveScaler(scaler, features: int, filename: str):
    
    # Convert to ONNX format
    initial_type = [("input", FloatTensorType([None, features]))]
    onnx_model = convert_sklearn(scaler, initial_types=initial_type, target_opset=14)
    
    
    with open(filename, "wb") as f:
        f.write(onnx_model.SerializeToString())

Chamamos essas funções ao salvar o modelo-base e os demais modelos.

Salvamento do modelo-base.

# ....
# ....

base_model = trainCNN(train_set=(X_train_seq, y_train_encoded),
                 val_set=(X_test_seq, y_test_encoded),
                 learning_rate = 0.01,
                 epochs = 1000,
                 batch_size =32)

saveCNN(model=base_model,
        window=window,
        features=X_train_seq.shape[2],
        filename=f"{trained_symbol}.basemodel.{timeframe}.onnx")

saveScaler(scaler=scaler,
           features=X_train.shape[1],
           filename=f"{trained_symbol}.{timeframe}.scaler.onnx")

Salvamento dos modelos treinados com Transfer Learning.

for symbol in symbols:

# ...    
# ...

    history = model.fit(X_train_seq, y_train_encoded, 
                       validation_data=(X_test_seq, y_test_encoded),
                       epochs=1000,  # More epochs for fine-tuning
                       batch_size=32,
                       callbacks=[early_stop],
                       verbose=1)

    saveCNN(model=model,
        window=window,
        features=X_train_seq.shape[2],
        filename=f"basesymbol={trained_symbol}.symbol={symbol}.model.{timeframe}.onnx")

    
    saveScaler(scaler=scaler,
               features=X_train.shape[1],
               filename=f"{symbol}.{timeframe}.scaler.onnx")

Depois de salvar os arquivos na pasta comum, podemos carregá-los no EA.

#include <ta.mqh> //similar to ta in Python --> https://www.mql5.com/en/articles/16931
#include <pandas.mqh> //similar to Pandas in Python --> https://www.mql5.com/en/articles/17030
#include <CNN.mqh> //For loading Convolutional Neural networks in ONNX format --> https://www.mql5.com/en/articles/15259
#include <preprocessing.mqh> //For loading the scaler transformer
#include <Trade\Trade.mqh> //The trading module
#include <Trade\PositionInfo.mqh> //Position handling module

CCNNClassifier cnn;
RobustScaler scaler;
CTrade m_trade;
CPositionInfo m_position;

input string base_symbol = "EURUSD";
input string symbol_ = "USDJPY";
input ENUM_TIMEFRAMES timeframe = PERIOD_H4;
input uint window_ = 10;
input uint lookahead = 1;
input uint magic_number = 28042025;
input uint slippage = 100;

long classes_in_y_[] = {0, 1};
int OldNumBars = -1;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
  if (!MQLInfoInteger(MQL_TESTER))
   if (!ChartSetSymbolPeriod(0, symbol_, timeframe))
      {
         printf("%s Failed to set symbol_ = %s and timeframe = %s, Error = %d",__FUNCTION__,symbol_,EnumToString(timeframe), GetLastError());
         return INIT_FAILED;
      }
   
//---
   
   string filename = StringFormat("basesymbol=%s.symbol=%s.model.%s.onnx",base_symbol, symbol_, EnumToString(timeframe));
   if (!cnn.Init(filename, ONNX_COMMON_FOLDER))
      {
         printf("%s failed to load a CNN model in ONNX format from the common folder '%s', Error = %d",__FUNCTION__,filename,GetLastError());
         return INIT_FAILED;
      }
      
//---
   
   filename = StringFormat("%s.%s.scaler.onnx", symbol_, EnumToString(timeframe));
   if (!scaler.Init(filename, ONNX_COMMON_FOLDER))
      {
         printf("%s failed to load a scaler in ONNX format from the common folder '%s', Error = %d",__FUNCTION__,filename,GetLastError());
         return INIT_FAILED;
      }
 }

Temos um módulo TA em MQL5, baseado na abordagem utilizada em Python, apresentado no artigo anterior. Com ele, podemos chamar funções de indicadores e armazenar os resultados em uma estrutura semelhante a um DataFrame do Pandas.

CDataFrame getStationaryVars(uint start = 1, uint bars = 50)
  {
    CDataFrame df; //Dataframe object
    
    vector open, high, low, close;
    open.CopyRates(Symbol(), Period(), COPY_RATES_OPEN, start, bars);
    high.CopyRates(Symbol(), Period(), COPY_RATES_HIGH, start, bars);
    low.CopyRates(Symbol(), Period(), COPY_RATES_LOW, start, bars);
    close.CopyRates(Symbol(), Period(), COPY_RATES_CLOSE, start, bars);
    
    vector pct_change = df.pct_change(close);
    vector diff_open = df.diff(open);
    vector diff_high = df.diff(high);
    vector diff_low = df.diff(low);
    vector diff_close = df.diff(close);
    
    df.insert("pct_change", pct_change);
    df.insert("diff_open", open);
    df.insert("diff_high", high);
    df.insert("diff_low", low);
    df.insert("diff_close", close);
    
    // Relative Strength Index (RSI)
    vector rsi = CMomentumIndicators::RSIIndicator(close);
    df.insert("rsi", rsi);
    
    // Stochastic Oscillator (Stoch)
    vector stock_k = CMomentumIndicators::StochasticOscillator(close,high,low).stoch;
    df.insert("stock_k", stock_k);
    
    // Moving Average Convergence Divergence (MACD)
    vector macd = COscillatorIndicators::MACDIndicator(close).main;
    df.insert("macd", macd);
    
    // Commodity Channel Index (CCI)
    vector cci = COscillatorIndicators::CCIIndicator(high,low,close);
    df.insert("cci", cci);
    
    // Rate of Change (ROC)
    vector roc = CMomentumIndicators::ROCIndicator(close);
    df.insert("roc", roc);
    
    // Ultimate Oscillator (UO)
    vector uo = CMomentumIndicators::UltimateOscillator(high,low,close);
    df.insert("uo", uo);
    
    // Williams %R
    vector williams_r = CMomentumIndicators::WilliamsR(high,low,close);
    df.insert("williams_r", williams_r);
    
    // Average True Range (ATR)
    vector atr = COscillatorIndicators::ATRIndicator(high,low,close);
    df.insert("atr", atr);
    
    // Awesome Oscillator (AO)
    vector ao = CMomentumIndicators::AwesomeOscillator(high,low);
    df.insert("ao", ao);
    
    // Average Directional Index (ADX)
    vector adx = COscillatorIndicators::ADXIndicator(high,low,close).adx;
    df.insert("adx", adx);
    
    // True Strength Index (TSI)
    vector tsi = CMomentumIndicators::TSIIndicator(close);
    df.insert("tsi", tsi);
    
    if (MQLInfoInteger(MQL_DEBUG))
      df.head();
    
    df = df.dropna(); //Drop not-a-number variables
    
    return df; //return the last rows = window from a dataframe which is the recent information fromthe market
 }

A cada barra, coletamos as 50 barras anteriores para calcular os indicadores, começando pela última barra fechada, de índice 1.

Escolhemos um limite de 50 barras para manter uma margem suficiente para o cálculo dos indicadores, já que alguns deles geram valores NaN (Not a Number), que não são úteis para o modelo.

O Awesome Oscillator é o indicador que exige a maior janela de dados históricos, com window2 igual a 34. Isso significa que 50 - 34 = 16 é a quantidade de dados restantes disponíveis para o modelo.

Ao executar o EA em modo de depuração, essas informações aparecem na aba Experts do MetaTrader 5.

MD      0       18:17:26.145    Transfer Learning EA (USDJPY,H4)        | Index | pct_change      | diff_open        | diff_high        | diff_low         | diff_close       | rsi     | stock_k     | macd     | cci     | roc     | uo      | williams_r     | atr     | ao      | adx            | tsi     |
FF      0       18:17:26.145    Transfer Learning EA (USDJPY,H4)        |     0 | nan             | 142.67000000     | 143.08800000     | 142.49100000     | 142.68300000     | nan     | nan         | nan      | nan     | nan     | nan     | nan            | nan     | nan     | 0.00000000     | nan     |
JO      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |     1 | -0.25300842     | 142.68400000     | 142.84900000     | 142.28700000     | 142.32200000     | nan     | nan         | nan      | nan     | nan     | nan     | nan            | nan     | nan     | 0.00000000     | nan     |
IR      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |     2 | 0.09977375      | 142.32300000     | 142.63500000     | 141.89900000     | 142.46400000     | nan     | nan         | nan      | nan     | nan     | nan     | nan            | nan     | nan     | 0.00000000     | nan     |
HF      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |     3 | -0.00070193     | 142.46400000     | 142.71900000     | 142.34400000     | 142.46300000     | nan     | nan         | nan      | nan     | nan     | nan     | nan            | nan     | nan     | 0.00000000     | nan     |
GJ      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |     4 | -0.04702976     | 142.37400000     | 142.47200000     | 142.18600000     | 142.39600000     | nan     | nan         | nan      | nan     | nan     | nan     | nan            | nan     | nan     | 0.00000000     | nan     |
IJ      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |  ...  |
NR      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |    45 | -0.22551954     | 142.33800000     | 142.38800000     | 141.98200000     | 142.01700000     | 28.79606321 | 1.70731707  | 0.20202343 | -149.46898289 | -0.42629273 | 28.03714657 | -48.58934169   | 0.58185714 | 0.84359706 | 29.65580624    | 8.31951160 |
NJ      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |    46 | 0.16054416      | 141.97800000     | 142.31600000     | 141.96400000     | 142.24500000     | 35.49705652 | 13.58800774 | 0.12993025 | -131.96513868 | -0.57316604 | 34.81743660 | -43.09139137   | 0.56978571 | 0.51217941 | 28.18573720    | 4.78996901 |
HQ      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |    47 | 0.19543745      | 142.24500000     | 142.58100000     | 142.12400000     | 142.52300000     | 43.03880625 | 27.03094778 | 0.09414295 | -86.63856716 | -0.76174826 | 43.61239023 | -36.38775018   | 0.57742857 | 0.21773529 | 26.19967843    | 3.09202782 |
FH      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |    48 | 0.04771160      | 142.52300000     | 142.61500000     | 142.29800000     | 142.59100000     | 44.85843867 | 30.31914894 | 0.07045611 | -66.64608781 | -0.57732936 | 49.55462139 | -34.74801061   | 0.56007143 | -0.01222353 | 24.37916904    | 2.01861384 |
MQ      0       18:17:26.146    Transfer Learning EA (USDJPY,H4)        |    49 | -0.19776844     | 142.59100000     | 142.75800000     | 142.25100000     | 142.30900000     | 38.91058297 | 16.68278530 | 0.02859940 | -70.14493704 | -0.77257229 | 41.99481159 | -41.54810707   | 0.52700000 | -0.13378529 | 23.02215655    | 0.05188403 |

Dentro da função OnTick, a primeira etapa consiste em calcular os atributos estacionários. Em seguida, aplicamos uma operação de slicing para garantir que a quantidade de barras nos dados resultantes corresponda ao tamanho da janela utilizado no treinamento do modelo CNN.

void OnTick()
  {
//---
   
   if (!isNewBar())
      return;
      
   CDataFrame x_df = getStationaryVars();
   
//--- Check if the number of rows received after indicator calculation is >= window size
   
   if ((uint)x_df.shape()[0]<window_)
      {
         printf("%s Fatal, Data received is less than the desired window=%u. Check your indicators or increase the number of bars in the function getSationaryVars()",__FUNCTION__,window_);
         DebugBreak();
         return;
      }
   
   ulong rows = (ulong)x_df.shape()[0];
   ulong cols = (ulong)x_df.shape()[1];
   
   //printf("Before scaled shape = (%I64u, %I64u)",rows, cols);
       
   matrix x = x_df.iloc((rows-window_), rows-1, 0, cols-1).m_values;
 }

Agora que temos uma matriz recortada com 10 linhas, de acordo com o tamanho da janela, e 16 atributos usados no treinamento, podemos passar esses dados pelo RobustScaler carregado e, depois, alimentar o modelo CNN para obter as previsões finais.

matrix x_scaled = scaler.transform(x); //Transform the data, very important
   
long signal = cnn.predict(x_scaled, classes_in_y_).cls; //Predicted class

Por fim, com base no sinal gerado pelo modelo, podemos implementar uma estratégia de trading simples: se o sinal for igual a 1, indicando alta, abrimos uma posição de compra; se o sinal for igual a 0, indicando baixa, abrimos uma posição de venda.

Cada operação será encerrada após a formação, no timeframe atual, de um número de barras igual ao valor do parâmetro lookahead.

//--- Trading functionality

   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) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, Symbol(), ticks.ask,0,0);
     }
     
   if (signal == 0) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, Symbol(), ticks.bid,0,0);
     } 
  
    CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe

É isso. Agora vamos executar esse EA em alguns dos instrumentos utilizados no processo de Transfer Learning e analisar os resultados das previsões no período de 1º de janeiro de 2023 a 1º de janeiro de 2025.

Timeframe: PERIOD_H4. Modelling: 1 Minute OHLC.

Symbol: XAUEUR

Symbol: XAUUSD

Dos 17 instrumentos utilizados no processo de Transfer Learning com base no modelo-base treinado no EURUSD, apenas 2 apresentaram resultados razoáveis. Os demais tiveram desempenho muito ruim.

Isso pode significar duas coisas. Primeiro, os padrões observados no EURUSD podem apresentar uma forte relação ou semelhança com aqueles formados no XAUUSD e no XAUEUR. Isso parece plausível, já que ambos os instrumentos estão relacionados às moedas EUR e USD, que compõem o símbolo-base EURUSD.

Segundo, isso também pode indicar que os modelos CNN utilizados não são ideais, pois não realizamos uma otimização completa da arquitetura nem dos hiperparâmetros. Além disso, não testamos diferentes símbolos-base nem analisamos os resultados obtidos em cada caso.

Seria possível adotar várias etapas adicionais, mas isso foge do escopo deste artigo. Se quiser, você pode explorá-las por conta própria.


Considerações finais

Vivemos na era de ouro da inteligência artificial e do machine learning. Essas tecnologias evoluem mais rapidamente do que se imaginava e, em grande parte graças ao open source, hoje é possível criar modelos poderosos a partir de soluções existentes com apenas algumas linhas de código. É justamente isso que chamamos de Transfer Learning.

Na área de visão computacional e em tarefas de processamento de imagens, já existem modelos open source de grande escala, como ResNet50, MobileNet e outros. Esses modelos permitiram que os desenvolvedores alcançassem um novo patamar e criassem soluções de IA realmente eficientes. No entanto, a área financeira ainda é pouco explorada no que diz respeito a modelos abertos e soluções open source.

O objetivo deste artigo foi mostrar as possibilidades do Transfer Learning e demonstrar como essa abordagem pode ser aplicada ao setor financeiro, servindo como ponto de partida para a criação de modelos de maior escala capazes de auxiliar na análise dos mercados financeiros usando padrões comuns presentes em diferentes instrumentos.

Boa sorte!


Tabela de anexos


Nome do arquivo

Descrição/finalidade
Expert\Transfer Learning EA.mq5
EA principal para testar modelos de Transfer Learning no ambiente de trading.

Include\CNN.mqh
Biblioteca para carregar e implantar modelos CNN armazenados em arquivos .onnx no MQL5.


Include\pandas.mqh

Biblioteca no estilo do Pandas para processamento e armazenamento de dados.

Include\preprocessing.mqh

Contém classes para carregar, escalar e transformar dados representados no formato .onnx.

Include\ta.mqh

Biblioteca com uma abordagem simples e prática para trabalhar com indicadores no MQL5.

Scripts\CollectData.mqh

Script para coletar e salvar em arquivos CSV os dados OHLC obtidos de diferentes instrumentos.
Python\forex-transfer-learning.ipynb
Script Python em formato Jupyter Notebook para executar todo o código Python descrito neste artigo.


Common\Files*scaler.onnx

Os parâmetros de escalonamento usados no pré-processamento dos dados são salvos em arquivos no formato ONNX.

Common\Files*.onnx

Os modelos CNN são salvos em arquivos no formato ONNX.

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

Arquivos anexados |
Attachments.zip (1971.42 KB)
Últimos Comentários | Ir para discussão (1)
BSA
BSA | 22 fev. 2026 em 19:48
O resultado é previsível. Basear uma previsão em um indicador defasado, tentando prever o quê? O próprio valor desse indicador defasado? Para mim, isso é estranho. Na minha opinião, o modelo deve aprender por si mesmo a prever o impulso e sinalizar, independentemente do timeframe, em tempo real; só depois ele aprenderá a distinguir o ruído do impulso real. Não é possível prever com certeza o movimento do mercado e a força do impulso, mas deve ser possível acompanhar a tendência e aproveitar a onda. O que, em princípio, é suficiente para uma operação lucrativa.
Criação de um painel administrativo de trading em MQL5 (Parte XI): Interface moderna de mensagens na plataforma (I) Criação de um painel administrativo de trading em MQL5 (Parte XI): Interface moderna de mensagens na plataforma (I)
Hoje vamos aprimorar a interface de troca de mensagens do painel de comunicação e alinhá-la aos padrões dos aplicativos modernos de comunicação de alto desempenho. Para isso, atualizaremos a classe CommunicationsDialog. Analisaremos todas essas alterações em detalhes e também definiremos os próximos passos para a evolução das interfaces dos nossos programas em MQL5.
Ferramentas personalizadas de depuração e profiling para desenvolvimento em MQL5 (Parte I): Logging avançado Ferramentas personalizadas de depuração e profiling para desenvolvimento em MQL5 (Parte I): Logging avançado
Aprenda a implementar um poderoso framework personalizado de logging em MQL5 que vai muito além de simples chamadas a Print(), oferecendo suporte a níveis de severidade, vários handlers de saída e rotação automática de arquivos, tudo isso com possibilidade de ajuste dinâmico durante a execução. Integre o singleton CLogger com ConsoleLogHandler e FileLogHandler para registrar logs contextuais com marcas de tempo tanto na guia "Experts" quanto em arquivos persistentes. Torne a depuração e o rastreamento de desempenho dos seus EAs mais eficientes com formatos de log claros e configuráveis e gerenciamento centralizado.
Automatização de estratégias de negociação em MQL5 (Parte 17): dominando a estratégia de scalping Grid-Mart com painel informativo dinâmico Automatização de estratégias de negociação em MQL5 (Parte 17): dominando a estratégia de scalping Grid-Mart com painel informativo dinâmico
Neste artigo, analisaremos a estratégia de scalping Grid-Mart, automatizando-a em MQL5 com o auxílio de um painel informativo dinâmico para obter informações sobre a negociação em tempo real. Descrevemos em detalhes a lógica de martingale baseada em grade, bem como as funções de gestão de risco. Também realizamos testes em dados históricos e implantamos a solução para garantir uma operação confiável.
Operando com o Calendário Econômico do MQL5 (Parte 7): Preparação para Testes de Estratégia com Análise de Eventos de Notícias por Recurso Incorporado Operando com o Calendário Econômico do MQL5 (Parte 7): Preparação para Testes de Estratégia com Análise de Eventos de Notícias por Recurso Incorporado
Neste artigo, preparamos nosso sistema de trading MQL5 para testes de estratégia incorporando dados do calendário econômico como um recurso para análise fora do ambiente em tempo real. Implementamos o carregamento e a filtragem de eventos por horário, moeda e impacto e, em seguida, validamos o sistema no Strategy Tester. Isso permite realizar backtests eficazes de estratégias orientadas por notícias.