English Русский Español 日本語
preview
Integração do MQL5 com pacotes de processamento de dados (Parte 5): Aprendizado adaptativo e flexibilidade

Integração do MQL5 com pacotes de processamento de dados (Parte 5): Aprendizado adaptativo e flexibilidade

MetaTrader 5 — Exemplos |
14 0
Hlomohang John Borotho
Hlomohang John Borotho

Introdução

Um dos principais problemas enfrentados por muitos traders algorítmicos é a falta de adaptabilidade dos sistemas de negociação tradicionais. Como discutimos no artigo anterior, a maioria dos EAs é programada com regras rígidas, baseadas em condições e limiares estáticos que muitas vezes não conseguem acompanhar as mudanças na dinâmica do mercado, as variações de volatilidade ou o surgimento de novos padrões ainda não observados, em tempo real. Por isso, esses sistemas tendem a apresentar bons resultados apenas em determinadas condições de mercado, mas perdem eficiência quando o comportamento do mercado muda. Isso pode resultar em oportunidades perdidas, sinais falsos frequentes ou períodos prolongados de drawdown.

O aprendizado adaptativo e a flexibilidade oferecem uma solução para esse problema. Com o Python, podemos construir um modelo de aprendizado por reforço capaz de aprender continuamente com dados históricos do XAUUSD, permitindo que o sistema ajuste sua estratégia conforme as condições de mercado mudam. A flexibilidade das bibliotecas Python, como PyTorch, Gym, Pandas e outras, permite realizar um pré-processamento avançado dos dados, simular o ambiente de negociação e otimizar os modelos. Após o treinamento, o modelo pode ser exportado para o formato ONNX, permitindo sua implantação no ambiente MQL5.


Obtenção de dados históricos

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import pytz

# Display data on the MetaTrader 5 package
print("MetaTrader5 package author: ", mt5.__author__)
print("MetaTrader5 package version: ", mt5.__version__)

# Configure pandas display options
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1500)

# Establish connection to MetaTrader 5 terminal
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

# Set time zone to UTC
timezone = pytz.timezone("Etc/UTC")
# Create 'datetime' objects in UTC time zone to avoid the implementation of a local time zone offset
utc_from = datetime(2025, 05, 15, tzinfo=timezone.utc)
utc_to = datetime.(2025, 07,08, tzinfo=timezone.utc) 

# Get bars from XAU H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("XAUUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()

# Check if data was retrieved
if rates is None or len(rates) == 0:
    print("No data retrieved. Please check the symbol or date range.")
else:
    # Display each element of obtained data in a new line (for the first 10 entries)
    print("Display obtained data 'as is'")
    for rate in rates[:10]:
        print(rate)

    # Create DataFrame out of the obtained data
    rates_frame = pd.DataFrame(rates)
    # Convert time in seconds into the 'datetime' format
    rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

    # Save the data to a CSV file
    filename = "XAU_H1.csv"
    rates_frame.to_csv(filename, index=False)
    print(f"\nData saved to file: {filename}")

Para obter os dados históricos, começamos inicializando a conexão com o terminal MetaTrader 5 por meio da função mt5.initialize(), que permite a comunicação entre o Python e a plataforma MetaTrader 5. Em seguida, definimos o intervalo de datas para consultar os dados, especificando as datas inicial e final. Essas datas são representadas por objetos datetime em UTC, para levar em conta os fusos horários. Neste caso, o script está configurado para obter dados históricos de barras de uma hora do XAUUSD, referentes ao período de 15 de maio de 2025 a 8 de julho de 2025, utilizando a função mt5.copy_rates_range().

filename = "XAUUSD_H1.csv"
rates_frame.to_csv(filename, index=False)
print(f"\nData saved to file: {filename}")

Como você talvez já saiba, eu utilizo Linux. Se você utiliza Windows, pode obter os dados históricos com o seguinte script Python:

from datetime import datetime
import MetaTrader5 as mt5
import pandas as pd
import pytz

# Display data on the MetaTrader 5 package
print("MetaTrader5 package author: ", mt5.__author__)
print("MetaTrader5 package version: ", mt5.__version__)

# Configure pandas display options
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1500)

# Establish connection to MetaTrader 5 terminal
if not mt5.initialize():
    print("initialize() failed, error code =", mt5.last_error())
    quit()

# Set time zone to UTC
timezone = pytz.timezone("Etc/UTC")
# Create 'datetime' objects in UTC time zone to avoid the implementation of a local time zone offset
utc_from = datetime(2025, 05, 15, tzinfo=timezone.utc)
utc_to = datetime(2025, 07, 08, tzinfo=timezome.utc) 

# Get bars from XAUUSD H1 (hourly timeframe) within the specified interval
rates = mt5.copy_rates_range("XAUUSD", mt5.TIMEFRAME_H1, utc_from, utc_to)

# Shut down connection to the MetaTrader 5 terminal
mt5.shutdown()

# Check if data was retrieved
if rates is None or len(rates) == 0:
    print("No data retrieved. Please check the symbol or date range.")
else:
    # Display each element of obtained data in a new line (for the first 10 entries)
    print("Display obtained data 'as is'")
    for rate in rates[:10]:
        print(rate)

    # Create DataFrame out of the obtained data
    rates_frame = pd.DataFrame(rates)
    # Convert time in seconds into the 'datetime' format
    rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')

    # Display data directly
    print("\nDisplay dataframe with data")
    print(rates_frame.head(10)

Se, por algum motivo, você não conseguir obter os dados históricos por meio de código, também poderá baixá-los manualmente pela própria plataforma MetaTrader 5. Para isso, abra o terminal, acesse Ferramentas > Opções no menu superior e selecione a aba Gráficos. Nessa aba, é necessário definir a quantidade de barras exibidas no gráfico. Recomenda-se selecionar a opção sem limite de barras (unlimited bars), especialmente porque estamos trabalhando com intervalos de datas e nem sempre é possível prever exatamente quantas barras haverá em determinado timeframe.

Para baixar os dados, acesse Exibir > Símbolos. A janela de símbolos será aberta na aba Especificação. Em seguida, selecione a aba Barras ou Ticks, dependendo do tipo de dado desejado. Defina as datas inicial e final do período e clique no botão "Solicitar". Após o download, você poderá exportar os dados e salvá-los em um arquivo .csv para uso posterior.


Primeiros passos

import pandas as pd

# Load the uploaded BTC 1H CSV file
file_path = '/home/int_j/Documents/Art Draft/Data Science/Adaptive Learning/XAUUSD_H1.csv'
xau_data = pd.read_csv(file_path)

# Display basic information about the dataset
xau_data_info = xau_data.info()
xau_data_head = xau_data.head()

xau_data_info, xau_data_head

Vamos começar analisando o conjunto de dados para entender sua estrutura. Para isso, usamos a função info() para verificar os tipos de dados, as dimensões e a presença de valores ausentes. Também utilizamos head() para exibir as primeiras linhas, que oferecem uma visão geral do conteúdo e da estrutura do conjunto de dados. Essa é uma etapa padrão da análise exploratória de dados (EDA), que permite confirmar se os dados foram carregados corretamente e obter uma visão inicial do formato dos dados.

# Reload the data with tab-separated values
xau_data = pd.read_csv(file_path, delimiter='\t')

# Display basic information and the first few rows after parsing
xau_data_info = xau_data.info()
xau_data_head = xau_data.head()

xau_data_info, xau_data_head

No próximo bloco de código, os dados históricos do XAUUSD são carregados novamente a partir do arquivo especificado, utilizando o caractere de tabulação (\t) como delimitador, em vez da vírgula padrão. Isso é importante para interpretar corretamente arquivos no formato TSV (Tab-Separated Values), nos quais os valores são separados por tabulações. Após o carregamento no DataFrame xau_data, a função info() exibe informações sobre o conjunto de dados, como os tipos das colunas, a quantidade de valores não nulos e o uso de memória. A função head() também é utilizada para exibir as primeiras linhas e permitir uma inspeção rápida dos dados.

import pandas as pd
import numpy as np
import ta
from sklearn.preprocessing import StandardScaler

# Split the single column into proper columns
if len(xau_data.columns) == 1:
    # Extract column headers from the first row
    headers = xau_data.columns[0].split('\t')
    
    # Split data into separate columns
    xau_data = xau_data[xau_data.columns[0]].str.split('\t', expand=True)
    xau_data.columns = headers

# Convert columns to proper data types
numeric_cols = ['<OPEN>', '<HIGH>', '<LOW>', '<CLOSE>', '<TICKVOL>', '<VOL>', '<SPREAD>']
xau_data[numeric_cols] = xau_data[numeric_cols].apply(pd.to_numeric, errors='coerce')

# Clean and create features
xau_data = xau_data.dropna()
xau_data['return'] = xau_data['<CLOSE>'].pct_change()

# Add technical indicators
xau_data['rsi'] = ta.momentum.RSIIndicator(xau_data['<CLOSE>'], window=14).rsi()
xau_data['macd'] = ta.trend.MACD(xau_data['<CLOSE>']).macd_diff()
xau_data['sma_20'] = ta.trend.SMAIndicator(xau_data['<CLOSE>'], window=20).sma_indicator()
xau_data['sma_50'] = ta.trend.SMAIndicator(xau_data['<CLOSE>'], window=50).sma_indicator()
xau_data = xau_data.dropna()

# Normalize features
scaler = StandardScaler()
features = ['rsi', 'macd', 'sma_20', 'sma_50', 'return']
xau_data[features] = scaler.fit_transform(xau_data[features])

Esse bloco de código realiza a limpeza e a formatação dos dados históricos do XAUUSD. Caso os dados tenham sido importados incorretamente para uma única coluna, o que pode acontecer com arquivos TSV, o script separa o conteúdo dessa coluna pelos caracteres de tabulação, recuperando os nomes das colunas e os respectivos valores. Em seguida, colunas importantes, como open, high, low, close, volume e spread, são convertidas explicitamente para tipos numéricos. O parâmetro errors='coerce' permite tratar eventuais erros de conversão, substituindo os valores inválidos por NaN. Depois, as linhas com valores ausentes são removidas e uma nova coluna é adicionada para registrar o retorno diário, calculada a partir da variação percentual do preço de fechamento.

Na etapa seguinte, o conjunto de dados é enriquecido com indicadores técnicos calculados por meio de uma biblioteca de análise técnica. Entre eles estão o RSI (Índice de Força Relativa), o MACD (Convergência e Divergência de Médias Móveis) e as médias móveis simples de 20 e 50 períodos, calculadas com base no preço de fechamento. Esses atributos são amplamente utilizados no trading algorítmico e ajudam os modelos a identificar tendências e momentum. Por fim, todos os atributos selecionados são padronizados com o StandardScaler da biblioteca scikit-learn, de modo que cada um passe a ter média zero e variância unitária. Essa é uma etapa importante antes de alimentar os modelos de machine learning ou de aprendizado por reforço com os dados.

import gym
from gym import spaces

class TradingEnv(gym.Env):
    def __init__(self, df, window_size=30, initial_balance=10000):
        super(TradingEnv, self).__init__()
        self.df = df.reset_index(drop=True)
        self.window_size = window_size
        self.initial_balance = initial_balance
        self.action_space = spaces.Discrete(3)  # 0: hold, 1: buy, 2: sell

        # Use correct shape (window_size, number of features)
        self.observation_space = spaces.Box(
            low=-np.inf, high=np.inf,
            shape=(self.window_size, len(features)), dtype=np.float32
        )

    def reset(self):
        self.current_step = self.window_size
        self.balance = self.initial_balance
        self.position = 0  # 1 = long, -1 = short, 0 = neutral
        self.entry_price = 0
        self.trades = []
        return self._next_observation()

    def _next_observation(self):
        # Use iloc to prevent overshooting shape
        obs = self.df.iloc[self.current_step - self.window_size : self.current_step]
        obs = obs[features].values
        return obs

    def step(self, action):
        current_price = self.df.loc[self.current_step, '<CLOSE>']
        reward = 0

        if action == 1 and self.position == 0:  # Buy
            self.position = 1
            self.entry_price = current_price
        elif action == 2 and self.position == 0:  # Sell
            self.position = -1
            self.entry_price = current_price
        elif action == 0 and self.position != 0:  # Close position
            if self.position == 1:
                reward = current_price - self.entry_price
            elif self.position == -1:
                reward = self.entry_price - current_price
            self.position = 0

        self.current_step += 1
        done = self.current_step >= len(self.df) - 1
        obs = self._next_observation()
        return obs, reward, done, {}

Como podemos observar no código, é definida uma classe de ambiente personalizada do OpenAI Gym, chamada TradingEnv, destinada a treinar agentes de aprendizado por reforço para tomar decisões de negociação com base em dados financeiros históricos, neste caso, do XAUUSD. O ambiente simula a negociação com três ações discretas: manter a posição (0), comprar (1) e vender (2). Ele é inicializado com uma janela de tamanho fixo de observações anteriores (window_size) e utiliza os atributos dos dados para simular o comportamento de negociação. O espaço de observações consiste em uma janela de valores históricos dos atributos, como RSI e MACD, enquanto o ambiente acompanha parâmetros essenciais, como saldo, estado da posição e preço de entrada.

A função reset() prepara o ambiente para um novo episódio, redefinindo o contador de passos, o saldo, a posição atual e todas as operações abertas. Já a função step() implementa a lógica das ações do agente. Quando o agente escolhe comprar ou vender sem ter uma posição aberta, uma operação é aberta. Se escolher manter uma posição já aberta, ela é encerrada, e o lucro ou prejuízo resultante é calculado como recompensa. O episódio avança pelo conjunto de dados, passo a passo, até chegar ao final (done=True). As observações retornadas correspondem a recortes dos atributos históricos, que o agente utiliza para tomar suas próximas decisões.

import torch.nn as nn
import torch.nn.functional as F

class DuelingDQN(nn.Module):
    def __init__(self, state_shape, action_dim):
        super(DuelingDQN, self).__init__()
        # Calculate flattened dimension
        flattened_dim = np.prod(state_shape)
        
        # Network layers
        self.fc1 = nn.Linear(flattened_dim, 128)
        
        # Value stream
        self.value_stream = nn.Sequential(
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, 1)
        )
        
        # Advantage stream
        self.advantage_stream = nn.Sequential(
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, action_dim)
        )

    def forward(self, state):
        # Flatten state while keeping batch dimension
        x = state.view(state.size(0), -1)
        x = F.relu(self.fc1(x))
        value = self.value_stream(x)
        advantages = self.advantage_stream(x)
        return value + (advantages - advantages.mean(dim=1, keepdim=True))

Em seguida, definimos a arquitetura Dueling Deep Q-Network (Dueling DQN) utilizando PyTorch. Trata-se de uma variação da DQN tradicional, na qual a função de valor do estado e a função de vantagem de cada ação são estimadas separadamente. A classe DuelingDQN herda de nn. Module e recebe como parâmetros o formato do estado de entrada e o número de ações possíveis (action_dim). Primeiro, os dados de entrada são convertidos em um vetor unidimensional e passam por uma camada totalmente conectada compartilhada (fc1). Depois, a rede se divide em dois fluxos: um estima o valor do estado, enquanto o outro calcula a vantagem de cada ação.

No método forward(), os dois fluxos são combinados pela seguinte fórmula:


Isso permite que o modelo diferencie o valor do próprio estado (V(s)) da vantagem relativa de cada ação (A(s, a)), melhorando a estabilidade e a eficiência do treinamento em tarefas de aprendizado por reforço, como a negociação no mercado financeiro.

# Training loop parameters
env = TradingEnv(xau_data)

# Use positional arguments instead of keyword arguments
model = DuelingDQN(150, 3)  # input_dim=150 (flattened state), action_dim=3
target_model = DuelingDQN(150, 3)  # Same dimensions
target_model.load_state_dict(model.state_dict())

Resultado:

<All keys matched successfully>

Nesta etapa, inicializamos o ambiente de treinamento e os modelos do agente Dueling DQN. O ambiente TradingEnv é criado a partir do conjunto de dados preparado, xau_data, que fornece os atributos de mercado utilizados no treinamento. São criadas duas instâncias do modelo DuelingDQN: model (rede principal) e target_model (rede-alvo). Ambas possuem uma dimensão de entrada de 150 e um espaço de ações com três possibilidades: comprar, vender e manter a posição. A rede-alvo é inicializada com uma cópia dos pesos da rede principal. Essa é uma prática comum no treinamento de modelos DQN, pois ajuda a estabilizar o aprendizado ao utilizar uma rede-alvo cujos pesos são atualizados lentamente durante o cálculo das diferenças temporais.

class ReplayBuffer:
    def __init__(self, capacity=10000):
        self.buffer = deque(maxlen=capacity)

    def push(self, state, action, reward, next_state, done):
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):
        batch = random.sample(self.buffer, batch_size)
        state, action, reward, next_state, done = map(np.array, zip(*batch))
        return (
            torch.tensor(state, dtype=torch.float32),          # Shape: [batch, state_dim]
            torch.tensor(action, dtype=torch.int64),           # Should be integer (for indexing)
            torch.tensor(reward, dtype=torch.float32),         
            torch.tensor(next_state, dtype=torch.float32),
            torch.tensor(done, dtype=torch.float32)
        )

    def __len__(self):
        return len(self.buffer)

A classe ReplayBuffer implementa um buffer de memória utilizado no aprendizado por reforço para armazenar e selecionar experiências anteriores. Ela utiliza a estrutura deque, com capacidade máxima fixa de 10.000 elementos por padrão, permitindo armazenar de forma eficiente tuplas no formato (state, action, reward, next_state, done). O método push() adiciona uma nova experiência ao buffer e remove automaticamente a mais antiga quando a capacidade máxima é excedida.

O método sample() seleciona aleatoriamente um lote de amostras do buffer e as converte em tensores PyTorch adequados ao treinamento do modelo. Durante essa conversão, são utilizados os tipos de dados apropriados para cada elemento, como int64 para as ações e float32 para os estados e as recompensas. Esse mecanismo contribui para a estabilidade do treinamento ao reduzir a correlação entre as amostras utilizadas. Assim, o agente pode aprender com um conjunto diversificado de interações anteriores, em vez de depender apenas de experiências obtidas em passos consecutivos.

env = TradingEnv(xau_data)
obs_shape = env.observation_space.shape
n_actions = env.action_space.n

# Calculate flattened dimension
flattened_dim = np.prod(obs_shape)  # 30*5 = 150

model = DuelingDQN(flattened_dim, n_actions)
target_model = DuelingDQN(flattened_dim, n_actions)
target_model.load_state_dict(model.state_dict())

optimizer = optim.Adam(model.parameters(), lr=0.0005)
buffer = ReplayBuffer()
gamma = 0.99
epsilon = 1.0
batch_size = 64
target_update_interval = 10

all_rewards = []
all_actions = []  # We'll collect actions for the entire dataset

# Training loop
for episode in range(200):
    state = env.reset()
    total_reward = 0
    done = False
    episode_actions = []  # Store actions for this episode

    while not done:
        if random.random() < epsilon:
            action = env.action_space.sample()
        else:
            with torch.no_grad():
                # Flatten state and pass to model
                state_tensor = torch.tensor(state, dtype=torch.float32).flatten().unsqueeze(0)
                q_values = model(state_tensor)
                action = q_values.argmax().item()
        
        next_state, reward, done, _ = env.step(action)
        buffer.push(state, action, reward, next_state, done)
        state = next_state
        total_reward += reward
        episode_actions.append(action)  # Record action

        if len(buffer) >= batch_size:
            s, a, r, s2, d = buffer.sample(batch_size)

            q_val = model(s).gather(1, a.unsqueeze(1)).squeeze()
            next_q_val = target_model(s2).max(1)[0]
            target = r + (1 - d) * gamma * next_q_val

            loss = nn.MSELoss()(q_val, target)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

    epsilon = max(0.01, epsilon * 0.995)
    if episode % target_update_interval == 0:
        target_model.load_state_dict(model.state_dict())
        print(f"Episode {episode}, Reward: {total_reward}")

    # After episode completes
    all_rewards.append(total_reward)
    all_actions.extend(episode_actions)  # Add episode actions to master list

Resultado:

Esse laço de treinamento configura um ambiente de aprendizado por reforço para negociação utilizando a arquitetura Dueling Deep Q-Network (Dueling DQN). Primeiro, o ambiente é inicializado com dados históricos do XAUUSD e, em seguida, os principais parâmetros são extraídos dos espaços de observações e ações. São criadas a rede principal Dueling DQN e a rede-alvo, ambas com dimensão de entrada de 150 (correspondente a 30 passos de tempo × 5 atributos). Também são configurados o otimizador Adam e o buffer de reprodução de experiências (replay buffer), além dos hiperparâmetros: fator de desconto (gamma), taxa de exploração (epsilon), tamanho do lote e frequência de atualização da rede-alvo.

A cada episódio, o ambiente é reinicializado e o agente interage com ele passo a passo, selecionando ações de acordo com a estratégia epsilon-greedy (ε-gulosa). Se um número aleatório for menor que epsilon, o agente escolhe uma ação aleatória. Caso contrário, o modelo seleciona a ação com o maior valor Q previsto. Após executar a ação, o agente recebe uma resposta do ambiente, que é armazenada no buffer de reprodução de experiências. Quando há experiências suficientes no buffer, um lote é selecionado para treinar o modelo. Em seguida, são calculados os valores Q e os valores-alvo, e o erro quadrático médio é utilizado na retropropagação do erro (backpropagation) para atualizar os parâmetros do modelo.

Para estabilizar o treinamento, a rede-alvo recebe periodicamente uma cópia dos pesos da rede principal. O valor de epsilon é reduzido gradualmente, diminuindo a proporção de ações aleatórias e permitindo que o agente se baseie cada vez mais no que aprendeu. Durante o treinamento, são registradas as recompensas acumuladas e as ações de cada episódio para avaliar o desempenho do agente. Ao longo de 200 episódios, esse laço ajuda o agente a desenvolver uma estratégia de negociação otimizada, equilibrando a exploração de novas possibilidades com o aproveitamento dos padrões já aprendidos.

import matplotlib.pyplot as plt

# Plotting performance metrics like cumulative reward
plt.plot([r for r in range(len(buffer.buffer))], label="Reward Trend")
plt.title("Training Rewards")
plt.show()

Resultado:

Para visualizar os resultados do treinamento, utilizamos a biblioteca matplotlib e criamos um gráfico da evolução das recompensas com base nos dados armazenados no buffer de reprodução de experiências. Isso permite acompanhar como a recompensa acumulada do agente varia durante o treinamento.

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.plot(all_rewards)
plt.xlabel("Episode")
plt.ylabel("Total Reward")
plt.title("Training Performance")
plt.grid(True)
plt.show()

Resultado:

Em seguida, visualizamos o desempenho do agente ao longo dos episódios por meio de um gráfico gerado a partir de all_rewards, que apresenta a recompensa total obtida em cada episódio. Esse gráfico permite acompanhar o progresso do treinamento e a estabilidade do comportamento do agente de negociação ao longo do tempo. Para facilitar a leitura, foram adicionados uma grade e rótulos aos eixos.

# Run a clean evaluation with the trained model (no exploration)
eval_env = TradingEnv(xau_data)
state = eval_env.reset()
eval_actions = []  # Store actions for this single episode

with torch.no_grad():
    while True:
        # Flatten state and predict
        state_tensor = torch.tensor(state, dtype=torch.float32).flatten().unsqueeze(0)
        q_values = model(state_tensor)
        action = q_values.argmax().item()
        
        next_state, _, done, _ = eval_env.step(action)
        eval_actions.append(action)
        state = next_state
        
        if done:
            break

# Now plot using eval_actions
close_prices = xau_data['<CLOSE>'].values
window_size = eval_env.observation_space.shape[0]

# Create action array with same length as price data
action_array = np.full(len(close_prices), np.nan)
action_array[window_size:window_size + len(eval_actions)] = eval_actions

# Create plot
plt.figure(figsize=(14, 8))
plt.plot(close_prices, label='XAUUSD Price', alpha=0.7)

# Plot buy signals (action=1)
buy_mask = (action_array == 1)
buy_indices = np.where(buy_mask)[0]
plt.scatter(buy_indices, close_prices[buy_mask], 
            color='green', label='Buy', marker='^', s=100)

# Plot sell signals (action=2)
sell_mask = (action_array == 2)
sell_indices = np.where(sell_mask)[0]
plt.scatter(sell_indices, close_prices[sell_mask], 
            color='red', label='Sell', marker='v', s=100)

plt.legend()
plt.title("Trading Actions on XAUUSD (Trained Policy)")
plt.xlabel("Time Step")
plt.ylabel("Price")
plt.grid(True)
plt.show()

Resultado:

Na etapa de avaliação (evaluation), o modelo treinado é utilizado para tomar decisões em um ambiente preparado para avaliação, sem exploração aleatória (eval_env). O agente observa o estado atual do mercado, seleciona a ação com o maior valor Q (estratégia gulosa) e registra cada decisão tomada. A execução continua até o final do episódio, permitindo avaliar o comportamento do agente sem a influência da aleatoriedade na escolha das ações.

dummy_input = torch.randn(1, *obs_shape)
torch.onnx.export(model, dummy_input, "dueling_dqn_xauusd.onnx",
                  input_names=["input"], output_names=["output"],
                  dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}

Por fim, exportamos o modelo Dueling DQN treinado para o formato ONNX, utilizando um vetor de entrada fictício (dummy input) para garantir a compatibilidade com outras plataformas. Isso permite utilizar o modelo fora do PyTorch, por exemplo, em sistemas que operam em tempo real ou diretamente no ambiente MQL5.


Integrando tudo no MQL5

//+------------------------------------------------------------------+
//|                                  ONNX_DQN_Trading_Script.mq5     |
//|                        Copyright 2023, MetaQuotes Ltd.           |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property script_show_inputs

//--- input parameters
input string   ModelPath = "dueling_dqn_xauusd.onnx";  // File in MQL5\Files\
input int      WindowSize = 30;                        // Observation window size
input int      FeatureCount = 5;                       // Number of features

//--- ONNX model handle
long onnxHandle;

//--- Normalization parameters (REPLACE WITH YOUR ACTUAL VALUES)
const double   RSI_MEAN = 55.0,       RSI_STD = 15.0;
const double   MACD_MEAN = 0.05,      MACD_STD = 0.5;
const double   SMA20_MEAN = 1800.0,   SMA20_STD = 100.0;
const double   SMA50_MEAN = 1800.0,   SMA50_STD = 100.0;
const double   RETURN_MEAN = 0.0002,  RETURN_STD = 0.01;

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
   //--- Load ONNX model
   onnxHandle = OnnxCreate(ModelPath, ONNX_DEFAULT);
   if(onnxHandle == INVALID_HANDLE)
   {
      Print("Error loading model: ", GetLastError());
      return;
   }
   
   //--- Prepare input data buffer
   double inputData[];
   ArrayResize(inputData, WindowSize * FeatureCount);
   
   //--- Collect and prepare data
   if(!PrepareInputData(inputData))
   {
      Print("Data preparation failed");
      OnnxRelease(onnxHandle);
      return;
   }
   
   //--- Set input shape (no need to set shape for dynamic axes)
   //--- Run inference
   double outputData[3];
   if(!RunInference(inputData, outputData))
   {
      Print("Inference failed");
      OnnxRelease(onnxHandle);
      return;
   }
   
   //--- Interpret results
   InterpretResults(outputData);
   OnnxRelease(onnxHandle);
}
//+------------------------------------------------------------------+
//| Prepare input data for the model                                 |
//+------------------------------------------------------------------+
bool PrepareInputData(double &inputData[])
{
   //--- Get closing prices
   double closes[];
   int closeCount = WindowSize + 1;
   if(CopyClose(_Symbol, _Period, 0, closeCount, closes) != closeCount)
   {
      Print("Not enough historical data. Requested: ", closeCount, ", Received: ", ArraySize(closes));
      return false;
   }
   
   //--- Calculate returns (percentage changes)
   double returns[];
   ArrayResize(returns, WindowSize);
   for(int i = 0; i < WindowSize; i++)
      returns[i] = (closes[i] - closes[i+1]) / closes[i+1];
   
   //--- Calculate technical indicators
   double rsi[], macd[], sma20[], sma50[];
   if(!CalculateIndicators(rsi, macd, sma20, sma50))
      return false;
   
   //--- Verify indicator array sizes
   if(ArraySize(rsi) < WindowSize || ArraySize(macd) < WindowSize || 
      ArraySize(sma20) < WindowSize || ArraySize(sma50) < WindowSize)
   {
      Print("Indicator data mismatch");
      return false;
   }
   
   //--- Normalize features and fill input data
   int dataIndex = 0;
   for(int i = WindowSize - 1; i >= 0; i--)
   {
      inputData[dataIndex++] = (rsi[i] - RSI_MEAN) / RSI_STD;
      inputData[dataIndex++] = (macd[i] - MACD_MEAN) / MACD_STD;
      inputData[dataIndex++] = (sma20[i] - SMA20_MEAN) / SMA20_STD;
      inputData[dataIndex++] = (sma50[i] - SMA50_MEAN) / SMA50_STD;
      inputData[dataIndex++] = (returns[i] - RETURN_MEAN) / RETURN_STD;
   }
   
   return true;
}
//+------------------------------------------------------------------+
//| Calculate technical indicators                                   |
//+------------------------------------------------------------------+
bool CalculateIndicators(double &rsi[], double &macd[], double &sma20[], double &sma50[])
{
   //--- RSI (14 period)
   int rsiHandle = iRSI(_Symbol, _Period, 14, PRICE_CLOSE);
   if(rsiHandle == INVALID_HANDLE) return false;
   if(CopyBuffer(rsiHandle, 0, 0, WindowSize, rsi) != WindowSize) return false;
   IndicatorRelease(rsiHandle);
   
   //--- MACD (12,26,9)
   int macdHandle = iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE);
   if(macdHandle == INVALID_HANDLE) return false;
   double macdSignal[];
   if(CopyBuffer(macdHandle, 0, 0, WindowSize, macd) != WindowSize) return false;
   if(CopyBuffer(macdHandle, 1, 0, WindowSize, macdSignal) != WindowSize) return false;
   
   // Calculate MACD difference (histogram)
   for(int i = 0; i < WindowSize; i++)
      macd[i] = macd[i] - macdSignal[i];
   
   IndicatorRelease(macdHandle);
   
   //--- SMA20
   int sma20Handle = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE);
   if(sma20Handle == INVALID_HANDLE) return false;
   if(CopyBuffer(sma20Handle, 0, 0, WindowSize, sma20) != WindowSize) return false;
   IndicatorRelease(sma20Handle);
   
   //--- SMA50
   int sma50Handle = iMA(_Symbol, _Period, 50, 0, MODE_SMA, PRICE_CLOSE);
   if(sma50Handle == INVALID_HANDLE) return false;
   if(CopyBuffer(sma50Handle, 0, 0, WindowSize, sma50) != WindowSize) return false;
   IndicatorRelease(sma50Handle);
   
   return true;
}
//+------------------------------------------------------------------+
//| Run model inference                                              |
//+------------------------------------------------------------------+
bool RunInference(const double &inputData[], double &outputData[])
{
   //--- Run model directly without setting shape (for dynamic axes)
   if(!OnnxRun(onnxHandle, ONNX_DEBUG_LOGS, inputData, outputData))
   {
      Print("Model inference failed: ", GetLastError());
      return false;
   }
   return true;
}
//+------------------------------------------------------------------+
//| Interpret model results                                          |
//+------------------------------------------------------------------+
void InterpretResults(const double &outputData[])
{
   //--- Find best action
   int bestAction = ArrayMaximum(outputData);
   string actionText = "";
   
   switch(bestAction)
   {
      case 0: actionText = "HOLD"; break;
      case 1: actionText = "BUY"; break;
      case 2: actionText = "SELL"; break;
   }
   
   //--- Print results
   Print("Model Output: [HOLD: ", outputData[0], ", BUY: ", outputData[1], ", SELL: ", outputData[2], "]");
   Print("Recommended Action: ", actionText);
}

O script MQL5 ONNX_DQN_Trading_Script.mq5 foi desenvolvido para executar o modelo Dueling DQN treinado e exportado no formato ONNX, com o objetivo de gerar sinais de negociação no MetaTrader 5. Primeiro, o modelo ONNX é carregado do diretório Files e os dados de entrada são preparados com base em uma janela de observações de tamanho fixo. O script coleta os dados de preços mais recentes e calcula os indicadores técnicos RSI, histograma do MACD, SMA20 e SMA50, além dos retornos. Em seguida, esses valores são normalizados com base em médias e desvios-padrão predefinidos. Os atributos processados são então convertidos em um array unidimensional compatível com o formato de entrada esperado pelo modelo.

Com o vetor de entrada preparado, o script executa a inferência com OnnxRun e obtém, no array de saída, três valores Q previstos, correspondentes às ações HOLD, BUY e SELL. A ação com o maior valor Q é interpretada como a recomendação do modelo e exibida no terminal. Durante a inferência, são realizadas verificações de erros para aumentar a confiabilidade da execução, e os recursos utilizados são liberados ao final das operações.


Conclusão

Ao longo deste artigo, desenvolvemos um modelo de negociação adaptativo e flexível, baseado na arquitetura Dueling DQN e treinado com dados históricos do XAUUSD. O modelo utiliza uma janela deslizante de atributos técnicos, incluindo RSI, histograma do MACD, SMA20, SMA50 e retornos percentuais, normalizados com base em parâmetros estatísticos. Também visualizamos a evolução das recompensas acumuladas durante o treinamento para acompanhar a estabilidade do aprendizado. Após o treinamento, exportamos o modelo para o formato ONNX, permitindo sua integração com o MetaTrader 5. Na plataforma, um script MQL5 específico carrega o modelo, prepara dinamicamente os dados de entrada, executa a inferência e identifica a ação recomendada (HOLD, BUY ou SELL) com base no maior valor Q previsto.

Esse pipeline completo (end-to-end) oferece aos traders um sistema automatizado de apoio à tomada de decisões, combinando aprendizado por reforço profundo com dados de mercado em tempo real. Sua flexibilidade permite adaptá-lo a novos instrumentos financeiros ou conjuntos de indicadores por meio de alterações nos atributos de entrada e de um novo treinamento do modelo. Com a integração da inferência ONNX diretamente ao MQL5, os traders podem utilizar modelos inteligentes na própria plataforma de negociação, aumentando a eficiência na execução de suas estratégias e a capacidade de resposta às mudanças do mercado, sem depender de software externo para executar o modelo.

Nome do arquivo
Descrição
Ada_flex.mq5 Arquivo que contém o script MQL5 para utilizar o modelo de aprendizado por reforço treinado em Python.
Ada_L.ipynb
Arquivo que contém o notebook para treinamento e salvamento do modelo.
XAUUSD_H1.csv
Arquivo com dados históricos de preços do XAUUSD.


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

Arquivos anexados |
ADA_L.ipynb (344.08 KB)
Ada_Flex.mq5 (6.79 KB)
XAUUSD_H1.csv (54.43 KB)
Automatização de estratégias de negociação em MQL5 (Parte 25): EA para negociação com linhas de tendência ajustadas pelo método dos mínimos quadrados e geração dinâmica de sinais Automatização de estratégias de negociação em MQL5 (Parte 25): EA para negociação com linhas de tendência ajustadas pelo método dos mínimos quadrados e geração dinâmica de sinais
Neste artigo, desenvolveremos um programa de negociação baseado em linhas de tendência que utiliza o ajuste pelo método dos mínimos quadrados (least squares fit) para determinar as linhas de tendência que atuam como suporte e resistência. O programa gera sinais dinâmicos de compra e venda quando o preço toca essas linhas e abre posições com base nos sinais identificados.
Desenvolvimento de um EA dinâmico para múltiplos pares (Parte 4): Ajuste do risco com base na volatilidade Desenvolvimento de um EA dinâmico para múltiplos pares (Parte 4): Ajuste do risco com base na volatilidade
Nesta etapa, configuramos o EA multipar para ajustar o tamanho das posições e a exposição ao risco em tempo real com base em indicadores de volatilidade, como o ATR. Isso torna a gestão de risco mais consistente, reforça a proteção das posições e melhora o desempenho em diferentes condições de mercado.
Automação de estratégias de negociação em MQL5 (Parte 26): Criação de um sistema de preço médio baseado em pin bars para negociação com múltiplas posições Automação de estratégias de negociação em MQL5 (Parte 26): Criação de um sistema de preço médio baseado em pin bars para negociação com múltiplas posições
Neste artigo, desenvolvemos em MQL5 um sistema de preço médio baseado em pin bars, que identifica esses padrões para abrir operações e utiliza uma estratégia de preço médio para gerenciar múltiplas posições, com recursos de trailing stop e ajuste do stop para o ponto de equilíbrio (breakeven). Também combinamos parâmetros configuráveis com um dashboard que permite monitorar as posições e os lucros em tempo real.
Analizado  Como criar um sistema de trading lucrativo (Parte 2): As nuances do dimensionamento de posições Analizado Como criar um sistema de trading lucrativo (Parte 2): As nuances do dimensionamento de posições
Mesmo com um sistema de trading de expectativa matemática positiva, o tamanho das posições pode influenciar o sucesso ou o fracasso. Esse é um aspecto fundamental da gestão de risco, pois permite transformar uma vantagem estatística em resultados concretos sem comprometer a proteção do capital.