English Русский 中文 Español Deutsch 日本語
preview
Perceptron Multicamadas e o Algoritmo Backpropagation (Parte II): Implementação em Python e Integração com MQL5

Perceptron Multicamadas e o Algoritmo Backpropagation (Parte II): Implementação em Python e Integração com MQL5

MetaTrader 5Exemplos | 22 setembro 2021, 17:10
1 406 0
Jonathan Pereira
Jonathan Pereira

Introdução

No artigo anterior vimos como criar um neurônio simples (Perceptron) e exploramos como é realizado os cálculos de decida de gradiente, também foi abordado como é construído uma rede (MLP) composta por Neurônios Perceptrons interligados entre si e como é feito o treinamento desse tipo de rede.

Para esse artigo quero demonstrar como é simples a implementação desse tipo de algoritmo com o auxílio da linguagem Python.

Python foi introduzido no leque de ferramentas de MQL5 e abre as portas para enumeras possibilidades como, exploração de dados, criação e uso de modelos de machine learning.

Com essa integração nativa entre MQL5 e Python, abriu-se as portas para muitas possibilidades de uso, podemos construir de uma simples regressão linear a um modelo de aprendizado profundo. Como é uma linguagem de uso profissional existe uma gama enorme de bibliotecas que fazem todo o trabalho pesado por traz de muitos cálculos.

Em nosso exemplo, construiremos uma rede de forma manual, porém como dito no artigo anterior, é um passo apenas para entendimento do que de fato ocorre durante o processo de treinamento e predição, após isso irei mostrar um exemplo mais sofisticado usando TensorFlow e Keras.


O que é TensorFlow?

TensorFlow é uma biblioteca de código aberto para computação numérica rápida.

Ele foi criado e mantido pelo Google e lançado sob a licença apache 2.0 de código aberto. A API é nominalmente para a linguagem de programação Python, embora haja acesso à API C++ subjacente.

Ao contrário de outras bibliotecas numéricas destinadas a ser usada no Deep Learning como Theano, o TensorFlow foi projetado para uso tanto em pesquisa e desenvolvimento quanto em sistemas de produção, especialmente o RankBrain na pesquisa do Google e o divertido projeto DeepDream.

Ele pode ser executado em sistemas de CPU única, GPUs, bem como dispositivos móveis e sistemas distribuídos em larga escala de centenas de máquinas.


O que é Keras?

Keras é uma biblioteca Python poderosa e fácil de usar de código aberto para desenvolver e avaliar modelos de aprendizagem profunda.

Ele envolve as eficientes bibliotecas numéricas de computação Theano e TensorFlow e permite definir e treinar modelos de rede neural em apenas algumas linhas de código.

Tutorial

Este tutorial está dividido em 4 partes:

  1. Instalando e preparando o ambiente python no MetaEditor.
  2. Primeiros passos e reconstrução do modelo (Perceptron e MLP).
  3. Criação de um modelo simples usando Keras e TensorFlow.
  4. Criando integração entre Python e MQL5.


1.     Instalando e preparando o ambiente Python.


Iniciaremos baixando o Python em seu site oficial www.python.org/downloads/

Para conseguirmos trabalhar com o TensorFlow é necessário a instalação de uma versão superior a 3.3 e inferior a 3.8, eu utilizo a versão 3.7.

Após baixar e iniciar o processo de instalação marque a opção “Add Python 3.7 to PATH”, isso garantirá que algumas coisas funcionem sem a necessidade de configurações adicionais futuramente.

Para conseguirmos rodar um script Python diretamente de nosso terminal MetaTrader 5 é muito simples, precisamos realizar uma previa configuração.

  • Definir o caminho do executável Python (ambiente)
  • Instalar as dependências necessárias para o projeto 

Precisamos abrir o MetaEditor e ir em Ferramentas>Opções.

Nessa sessão precisamos definir o caminho onde fica nosso executável Python, note que, após a instalação provavelmente conterá o caminho default do Python, se eventualmente não estiver coloque o caminho completo ate o executável para que consiga executar scripts diretamente de seu terminal MetaTrader 5.

1 - Configuração de compiladores

No meu caso uso um ambiente de bibliotecas totalmente separado, chamado de ambiente virtual, é uma forma de conseguir ter uma instalação "limpa" e conseguir concentrar apenas as bibliotecas necessárias no projeto.

Para maiores informações sobre o pacote venv por favor verifique no link.


Feito isso estamos aptos a rodar scripts Python diretamente do Terminal. Para nosso experimento vamos precisar instalar as bibliotecas:

Caso tenha duvidas como instalar as bibliotecas consulte o manual de instalação de módulos.

  • MetaTrader 5
  • TensorFlow
  • Matplotlib
  • Pandas
  • Sklearn

Agora que já instalamos e configuramos o ambinete, vamos fazer um breve teste para entender como criar e rodar um script em nosso terminal. Para iniciar um novo script diretamente pelo MetaEditor podemos seguir os passo a seguir:

Novo > Python script

1-Novo Script

Defina um nome para seu script o Assistente de criação do MetaEditor nos sugere a importação de algumas bibliotecas de forma automática, isso é algo bem interessante e para nosso experimento vamos selecionar a opção numpy.

3 - Novo Script II

Agora vamos criar um simples script que gera um gráfico de seno e plota-lo.

# Copyright 2021, Lethan Corp.
# https://www.mql5.com/pt/users/14134597

import numpy as np
import matplotlib.pyplot as plt

data = np.linspace(-np.pi, np.pi, 201)
plt.plot(data, np.sin(data))
plt.xlabel('Angle [rad]')
plt.ylabel('sin(data)')
plt.axis('tight')
plt.show()

Para rodar o script é simples, basta clicar em compilar(F7) ou abrir seu terminal MetaTrader 5 e anexar o script a um gráfico, e os resultados serão exibidos na aba experts caso haja prints, igual ocorre no desenvolvimento de um algoritmo MQL5, no nosso caso abrirá uma janela com o gráfico da função que criamos acima.

3 - Plot Seno


2.     Primeiros passos e reconstrução do modelo (Perceptron e MLP)

 

Iremos usar o mesmo conjunto de dados do exemplo em MQL5 para simplificar.

Abaixo a função nomeada como predict() que prevê um valor de saída para uma linha dado um conjunto de  pesos, nesse caso o primeiro peso sempre será o bias. E também a função de ativação.

# Transfer neuron activation
def activation(activation):
    return 1.0 if activation >= 0.0 else 0.0

# Make a prediction with weights
def predict(row, weights):
    z = weights[0]
    for i in range(len(row) - 1):
        z += weights[i + 1] * row[i]
    return activation(z)

Como já sabemos para fazer o treinamento de uma rede precisamos fazer o processo de descida de gradiente, que foi amplamente discutido no artigo anterior, abaixo mostro a função de treinamento “ train_weights() “.

# Estimate Perceptron weights using stochastic gradient descent
def train_weights(train, l_rate, n_epoch):
    weights = [0.0 for i in range(len(train[0]))]  #random.random()
    for epoch in range(n_epoch):
        sum_error = 0.0
        for row in train:
            y = predict(row, weights)
            error = row[-1] - y
            sum_error += error**2
            weights[0] = weights[0] + l_rate * error

            for i in range(len(row) - 1):
                weights[i + 1] = weights[i + 1] + l_rate * error * row[i]
        print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))
    return weights


Aplicação do modelo MLP:

Este tutorial é dividido em 5 partes:

  • Inicialização da rede
  • Propagação (FeedForward)
  • BackPropagation
  • Treinamento
  • Previsão

Inicialização da rede:

Vamos começar com algo fácil, a criação de uma nova rede pronta para treinamento.

Cada neurônio tem um conjunto de pesos que precisam ser mantidos. Um peso para cada conexão de entrada e um peso adicional para o bias. Precisaremos armazenar propriedades adicionais para um neurônio durante o treinamento, portanto usaremos um dicionário para representar cada neurônio e armazenar propriedades por nomes como 'pesos' para os pesos.

from random import seed
from random import random

# Initialize a network
def initialize_network(n_inputs, n_hidden, n_outputs):
    network = list()
    hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
    network.append(hidden_layer)
    output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
    network.append(output_layer)
    return network

seed(1)
network = initialize_network(2, 1, 2)
for layer in network:
    print(layer)

Agora que sabemos como criar e inicializar uma rede, vamos ver como podemos usá-la para calcular uma saída.

    Propagação (FeedForward)

    from math import exp
    
    # Calculate neuron activation for an input
    def activate(weights, inputs):
        activation = weights[-1]
        for i in range(len(weights)-1):
            activation += weights[i] * inputs[i]
        return activation
    
    # Transfer neuron activation
    def transfer(activation):
        return 1.0 / (1.0 + exp(-activation))
    
    # Forward propagate input to a network output
    def forward_propagate(network, row):
        inputs = row
        for layer in network:
            new_inputs = []
            for neuron in layer:
                activation = activate(neuron['weights'], inputs)
                neuron['output'] = transfer(activation)
                new_inputs.append(neuron['output'])
            inputs = new_inputs
        return inputs
    
    # test forward propagation
    network = [[{'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
            [{'weights': [0.2550690257394217, 0.49543508709194095]}, {'weights': [0.4494910647887381, 0.651592972722763]}]]
    row = [1, 0, None]
    output = forward_propagate(network, row)
    print(output)

    rodando o script acima teremos a saída:

    [0.6629970129852887, 0.7253160725279748]

    Os valores reais de saída são apenas absurdos por enquanto, mas em seguida, vamos começar a aprender a tornar os pesos nos neurônios mais úteis.

    Backpropagation

    # Calculate the derivative of an neuron output
    def transfer_derivative(output):
        return output * (1.0 - output)
    
    # Backpropagate error and store in neurons
    def backward_propagate_error(network, expected):
        for i in reversed(range(len(network))):
            layer = network[i]
            errors = list()
            if i != len(network)-1:
                for j in range(len(layer)):
                    error = 0.0
                    for neuron in network[i + 1]:
                        error += (neuron['weights'][j] * neuron['delta'])
                    errors.append(error)
            else:
                for j in range(len(layer)):
                    neuron = layer[j]
                    errors.append(expected[j] - neuron['output'])
            for j in range(len(layer)):
                neuron = layer[j]
                neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])
    
    # test backpropagation of error
    network = [[{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614]}],
              [{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095]}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763]}]]
    expected = [0, 1]
    backward_propagate_error(network, expected)
    for layer in network:
        print(layer)
    

    O exemplo de execução imprime a rede após a verificação de erro ser concluída. Você pode ver que os valores de erro são calculados e armazenados nos neurônios para a camada de saída e a camada oculta.

    [{'output': 0.7105668883115941, 'weights': [0.13436424411240122, 0.8474337369372327, 0.763774618976614], 'delta': -0.0005348048046610517}]

    [{'output': 0.6213859615555266, 'weights': [0.2550690257394217, 0.49543508709194095], 'delta': -0.14619064683582808}, {'output': 0.6573693455986976, 'weights': [0.4494910647887381, 0.651592972722763], 'delta': 0.0771723774346327}]

    Treinamento da rede

    from math import exp
    from random import seed
    from random import random
    
    # Initialize a network
    def initialize_network(n_inputs, n_hidden, n_outputs):
        network = list()
        hidden_layer = [{'weights':[random() for i in range(n_inputs + 1)]} for i in range(n_hidden)]
        network.append(hidden_layer)
        output_layer = [{'weights':[random() for i in range(n_hidden + 1)]} for i in range(n_outputs)]
        network.append(output_layer)
        return network
    
    # Calculate neuron activation for an input
    def activate(weights, inputs):
        activation = weights[-1]
        for i in range(len(weights)-1):
            activation += weights[i] * inputs[i]
        return activation
    
    # Transfer neuron activation
    def transfer(activation):
        return 1.0 / (1.0 + exp(-activation))
    
    # Forward propagate input to a network output
    def forward_propagate(network, row):
        inputs = row
        for layer in network:
            new_inputs = []
            for neuron in layer:
                activation = activate(neuron['weights'], inputs)
                neuron['output'] = transfer(activation)
                new_inputs.append(neuron['output'])
            inputs = new_inputs
        return inputs
    
    # Calculate the derivative of an neuron output
    def transfer_derivative(output):
        return output * (1.0 - output)
    
    # Backpropagate error and store in neurons
    def backward_propagate_error(network, expected):
        for i in reversed(range(len(network))):
            layer = network[i]
            errors = list()
            if i != len(network)-1:
                for j in range(len(layer)):
                    error = 0.0
                    for neuron in network[i + 1]:
                        error += (neuron['weights'][j] * neuron['delta'])
                    errors.append(error)
            else:
                for j in range(len(layer)):
                    neuron = layer[j]
                    errors.append(expected[j] - neuron['output'])
            for j in range(len(layer)):
                neuron = layer[j]
                neuron['delta'] = errors[j] * transfer_derivative(neuron['output'])
    
    # Update network weights with error
    def update_weights(network, row, l_rate):
        for i in range(len(network)):
            inputs = row[:-1]
            if i != 0:
                inputs = [neuron['output'] for neuron in network[i - 1]]
            for neuron in network[i]:
                for j in range(len(inputs)):
                    neuron['weights'][j] += l_rate * neuron['delta'] * inputs[j]
                neuron['weights'][-1] += l_rate * neuron['delta']
    
    # Train a network for a fixed number of epochs
    def train_network(network, train, l_rate, n_epoch, n_outputs):
        for epoch in range(n_epoch):
            sum_error = 0
            for row in train:
                outputs = forward_propagate(network, row)
                expected = [0 for i in range(n_outputs)]
                expected[row[-1]] = 1
                sum_error += sum([(expected[i]-outputs[i])**2 for i in range(len(expected))])
                backward_propagate_error(network, expected)
                update_weights(network, row, l_rate)
            print('>epoch=%d, lrate=%.3f, error=%.3f' % (epoch, l_rate, sum_error))
    
    # Test training backprop algorithm
    seed(1)
    dataset = [[2.7810836,2.550537003,0],
        [1.465489372,2.362125076,0],
        [3.396561688,4.400293529,0],
        [1.38807019,1.850220317,0],
        [3.06407232,3.005305973,0],
        [7.627531214,2.759262235,1],
        [5.332441248,2.088626775,1],
        [6.922596716,1.77106367,1],
        [8.675418651,-0.242068655,1],
        [7.673756466,3.508563011,1]]
    
    n_inputs = len(dataset[0]) - 1
    n_outputs = len(set([row[-1] for row in dataset]))
    network = initialize_network(n_inputs, 2, n_outputs)
    train_network(network, dataset, 0.5, 20, n_outputs)
    for layer in network:
        print(layer)
    

    Uma vez treinada, a rede é impressa, mostrando os pesos aprendidos. Também ainda na rede estão os valores de saída e delta que podem ser ignorados. Poderíamos atualizar nossa função de treinamento para excluir esses dados se quisermos.

    >epoch=13, lrate=0.500, error=1.953

    >epoch=14, lrate=0.500, error=1.774

    >epoch=15, lrate=0.500, error=1.614

    >epoch=16, lrate=0.500, error=1.472

    >epoch=17, lrate=0.500, error=1.346

    >epoch=18, lrate=0.500, error=1.233

    >epoch=19, lrate=0.500, error=1.132

    [{'weights': [-1.4688375095432327, 1.850887325439514, 1.0858178629550297], 'output': 0.029980305604426185, 'delta': -0.0059546604162323625}, {'weights': [0.37711098142462157, -0.0625909894552989, 0.2765123702642716], 'output': 0.9456229000211323, 'delta': 0.0026279652850863837}]

    [{'weights': [2.515394649397849, -0.3391927502445985, -0.9671565426390275], 'output': 0.23648794202357587, 'delta': -0.04270059278364587}, {'weights': [-2.5584149848484263, 1.0036422106209202, 0.42383086467582715], 'output': 0.7790535202438367, 'delta': 0.03803132596437354}]

    Para prever podemos usar o conjunto de pesos já ajustados no exemplo anterior

    Prever

    from math import exp
    
    # Calculate neuron activation for an input
    def activate(weights, inputs):
        activation = weights[-1]
        for i in range(len(weights)-1):
            activation += weights[i] * inputs[i]
        return activation
    
    # Transfer neuron activation
    def transfer(activation):
        return 1.0 / (1.0 + exp(-activation))
    
    # Forward propagate input to a network output
    def forward_propagate(network, row):
        inputs = row
        for layer in network:
            new_inputs = []
            for neuron in layer:
                activation = activate(neuron['weights'], inputs)
                neuron['output'] = transfer(activation)
                new_inputs.append(neuron['output'])
            inputs = new_inputs
        return inputs
    
    # Make a prediction with a network
    def predict(network, row):
        outputs = forward_propagate(network, row)
        return outputs.index(max(outputs))
    
    # Test making predictions with the network
    dataset = [[2.7810836,2.550537003,0],
        [1.465489372,2.362125076,0],
        [3.396561688,4.400293529,0],
        [1.38807019,1.850220317,0],
        [3.06407232,3.005305973,0],
        [7.627531214,2.759262235,1],
        [5.332441248,2.088626775,1],
        [6.922596716,1.77106367,1],
        [8.675418651,-0.242068655,1],
        [7.673756466,3.508563011,1]]
    network = [[{'weights': [-1.482313569067226, 1.8308790073202204, 1.078381922048799]}, {'weights': [0.23244990332399884, 0.3621998343835864, 0.40289821191094327]}],
        [{'weights': [2.5001872433501404, 0.7887233511355132, -1.1026649757805829]}, {'weights': [-2.429350576245497, 0.8357651039198697, 1.0699217181280656]}]]
    for row in dataset:
        prediction = predict(network, row)
        print('Expected=%d, Got=%d' % (row[-1], prediction))

    A execução do exemplo imprime a saída esperada para cada registro no conjunto de dados de treinamento, seguida pela previsão nítida feita pela rede.

    Isso mostra que a rede alcança 100% de precisão neste pequeno conjunto de dados.

    Expected=0, Got=0

    Expected=0, Got=0

    Expected=0, Got=0

    Expected=0, Got=0

    Expected=0, Got=0

    Expected=1, Got=1

    Expected=1, Got=1

    Expected=1, Got=1

    Expected=1, Got=1

    Expected=1, Got=1


    3.      Criação de um modelo simples usando Keras e TensorFlow.

    Para coletar os dados iremos usar o pacote MetaTrader 5, iniciaremos nosso script importando as bibliotecas necessárias para extração, transformação e previsão dos preços. Note que não abordaremos de forma detalhada a parte de preparação de dados, mas tenha em mente que é um passo muito importante para o modelo.

    Iniciaremos com uma breve visualização dos dados. O dataset (conjunto de dados) é composta com as últimas 1000 barras do ativo EURUSD. Para isso precisamos de alguns passos, como:

    • Importação das bibliotecas
    • Conexão com o MetaTrader
    • Coleta dos dados
    • Transformação de dados, ajuste na data
    • Plotar dados


    import MetaTrader5 as mt5
    from pandas import to_datetime, DataFrame
    import matplotlib.pyplot as plt
    
    symbol = "EURUSD"
    
    if not mt5.initialize():
        print("initialize() failed")
        mt5.shutdown()
    
    rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_D1, 0, 1000)
    mt5.shutdown()
    
    rates = DataFrame(rates)
    rates['time'] = to_datetime(rates['time'], unit='s')
    rates = rates.set_index(['time'])
    
    plt.figure(figsize = (15,10))
    plt.plot(rates.close)
    plt.show()
    

    Após executar o código iremos visualizar os dados de fechamentos em um gráfico de linha como mostrado abaixo.

    plot_1


    Vamos usar uma abordagem simples de regressão onde será previsto o valor de fechamento do próximo período.

    Para esse exemplo usaremos uma abordagem univariada, e como dito anteriormente, sem atenção as melhores práticas em preparação de dados.

    Séries temporais univariadas são um conjunto de dados composto por uma única série de observações com uma ordenação temporal e um modelo é necessário aprender com a série de observações passadas para prever o próximo valor na sequência.

    O primeiro passo é dividir a serie carregada em conjunto de treinamento e teste. Para isso vamos criar uma função que dividirá essa serie em duas partes, usaremos um valor percentual do tamanho total da serie para o corte, exemplo: 70% para treinamento e 30% para teste. Para a validação (backtest) temos outras abordagens, como dividir a serie em treino, test e validação. Como estamos falando de serie financeira todo o cuidado para evitar overfitting é pouco.

    Em nossa função esperamos receber uma matriz numpy, e um valor de corte, e retornaremos duas series quebradas.

    O primeiro retorno é todo o conjunto da primeira posição 0 até o tamanho que representa o tamanho do fator, e a segunda serie é todo o resto.

    def train_test_split(values, fator):
        train_size = int(len(values) * fator)
        return values[0:train_size], values[train_size:len(values)]
    


    Os modelos em Keras podem ser definidos como uma sequência de camadas.

    Criamos um modelo Sequencial e adicionamos camadas uma de cada vez até que estejamos felizes com nossa arquitetura de rede.

    A primeira coisa a acertar e garantir que a camada de entrada tenha o número certo de recursos de entrada. Isso pode ser especificado ao criar a primeira camada com o argumento input_dim.

    Como sabemos o número de camadas e seus tipos?

    Esta é uma pergunta muito difícil. Existem heurísticas que podemos usar e muitas vezes a melhor estrutura de rede é encontrada através de um processo de experimentação de tentativa e erro. Geralmente, você precisa de uma rede grande o suficiente para capturar a estrutura do problema.

    Neste exemplo, usaremos uma estrutura de rede totalmente conectada com uma camada.

    Camadas totalmente conectadas são definidas usando a classe Dense. Podemos especificar o número de neurônios ou nós na camada como o primeiro argumento, e especificar a função de ativação usando o argumento de ativação.

    Usaremos a função de ativação da unidade linear retificada referida como ReLU na primeira camada.

    Antes que uma série univariada possa ser prevista, ela deve ser preparada.

    O modelo MLP aprenderá com uma função que mapeia uma sequência de observações passadas como entrada para uma observação de saída. Como tal, a sequência de observações deve ser transformada em múltiplos exemplos dos quais o modelo pode aprender.

    Considere uma sequência univariada:

    [10, 20, 30, 40, 50, 60, 70, 80, 90]

    Podemos dividir a sequência em vários padrões de entrada/saída chamados amostras.

    Para nosso exemplo usaremos três etapas de tempo que são usadas como entrada e um passo de tempo que é usado como saída para a previsão que está sendo aprendida.

    X,                                        y

    10, 20, 30                          40

    20, 30, 40                          50

    30, 40, 50                          60

    ...

    Abaixo criamos a função split_sequence() que implementa esse comportamento e dividirá um conjunto univariado em varias amostras onde cada uma tem um número especificado de etapas de tempo e a saída é uma única etapa de tempo.

    Podemos testar nossa função em um pequeno conjunto de dados, como os dados inventados acima no exemplo.

    # univariate data preparation
    from numpy import array
     
    # split a univariate sequence into samples
    def split_sequence(sequence, n_steps):
        X, y = list(), list()
        for i in range(len(sequence)):
            # find the end of this pattern
            end_ix = i + n_steps
            # check if we are beyond the sequence
            if end_ix > len(sequence)-1:
                break
            # gather input and output parts of the pattern
            seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
            X.append(seq_x)
            y.append(seq_y)
        return array(X), array(y)
     
    # define input sequence
    raw_seq = [10, 20, 30, 40, 50, 60, 70, 80, 90]
    # choose a number of time steps
    n_steps = 3
    # split into samples
    X, y = split_sequence(raw_seq, n_steps)
    # summarize the data
    for i in range(len(X)):
        print(X[i], y[i])
    

    A execução do exemplo divide a série univariada em seis amostras onde cada amostra tem três etapas de tempo de entrada e um passo de tempo de saída.

    [10 20 30] 40

    [20 30 40] 50

    [30 40 50] 60

    [40 50 60] 70

    [50 60 70] 80

    [60 70 80] 90

    Para prosseguir precisamos separar a amostra em X (feature)  e y (target) para que possamos fazer o treinamento da rede, para isso usaremos a função que criamos logo acima split_sequence()

    X_train, y_train = split_sequence(train, 3)
    X_test, y_test = split_sequence(test, 3)
    

    Agora que já preparamos nossas amostras de dados podemos criar a rede MLP.

    Um modelo MLP simples tem uma única camada oculta de nós(neurônios), e uma camada de saída usada para fazer uma previsão.

    Podemos definir um MLP para a previsão de séries temporais univariadas da seguinte forma.

    # define model
    model = Sequential()
    model.add(Dense(100, activation='relu', input_dim=n_steps))
    model.add(Dense(1))
    model.compile(optimizer='adam', loss='mse')
    

    Um ponto importante na definição é a forma da entrada, é isso que o modelo espera como entrada para cada amostra em termos de número de etapas de tempo.

    O número de etapas de tempo como entrada é o número que escolhemos ao preparar nosso conjunto de dados como argumento para a função split_sequence().

    A dimensão de entrada de cada amostra é especificada no argumento input_dim  sobre a definição da primeira camada oculta. Tecnicamente, o modelo exibirá cada passo de tempo como um recurso separado em vez de etapas de tempo separadas.

    Quase sempre temos várias amostras, portanto, o modelo espera que o componente de entrada dos dados de treinamento tenha as dimensões ou forma:

    [samples, features]

    A função split_sequence() produz o X com a forma [amostras, características] prontas para uso.

    O modelo é treinado usando um algoritmo eficiente chamado Adam para a descida de gradiente estocástico e otimizado usando o erro quadrado médio, ou 'mse', função de perda.

    Uma vez que o modelo está definido podemos treinar o modelo com o conjunto de dados.

    model.fit(X_train, y_train, epochs=100, verbose=2)

    Depois que o modelo estiver treinado podemos fazer a previsão de um valor futuro, o modelo espera que a forma de entrada seja bidimensional com [amostras, características], portanto, devemos remodelar a amostra de entrada única antes de fazer a previsão, por exemplo, com a forma [1, 3] para 1 amostra e 3 etapas de tempo usadas como características de entrada.

    Para exemplificar vamos selecionar o último registro da amostra de test “X_test” e após a predição vamos comparar com o valor real contido na última amostra “y_test”.

    # demonstrate prediction
    x_input = X_test[-1]
    x_input = x_input.reshape((1, n_steps))
    yhat = model.predict(x_input, verbose=0)
    
    print("Valor previsto: ", yhat)
    print("Valor real: ", y_test[-1])
    


    4.      Criando integração entre Python e MQL5.

    Para conseguir usar o modelo em uma conta de negociação temos algumas opções, uma delas é usar as funções nativas em Python que fazem abertura e encerramento de posições, mas para esse cenário não teremos a enumeras possibilidades que MQL nos oferece, por esse motivo optei por uma integração entre o ambiente Python e MQL, isso nos dará mais autonomia no gerenciamento de nossas posições/ordens.

    Baseado no artigo Integração da MetaTrader 5 e Python: recebendo e enviando dados - Artigos MQL5 escrito por Maxim Dmitrievsky fiz a implementação dessa classe usando um padrão de desenvolvimento chamado Singleton que será responsável por criar um cliente Sockte para a comunicação. Este padrão assegura que exista apenas uma cópia de um certo tipo de objeto pois caso o programa utilize dois ponteiros, ambos se referindo a um mesmo objeto, os ponteiros irão apontar para o mesmo objeto.

    class CClientSocket
      {
    private:
       static CClientSocket*  m_socket;
       int                    m_handler_socket;
       int                    m_port;
       string                 m_host;
       int                    m_time_out;
                         CClientSocket(void);
                        ~CClientSocket(void);
    public:
       static bool           DeleteSocket(void);
       bool                  SocketSend(string payload);
       string                SocketReceive(void);
       bool                  IsConnected(void);
       static CClientSocket *Socket(void);
       bool                  Config(string host, int port);
       bool                  Close(void);
      };

    A classe CClienteSocke armazena um ponteiro estático como um membro privado, a classe tem apenas um construtor que é privado e não pode ser chamado, em vez de chamar o construtor, o método Socket pode ser usado, assim asseguramos que apenas um único objeto será utilizado.

    static CClientSocket *CClientSocket::Socket(void)
      {
       if(CheckPointer(m_socket)==POINTER_INVALID)
          m_socket=new CClientSocket();
       return m_socket;
      }

    Esse método verifica se o ponteiro estático aponta para o objeto CClienteSocket e caso seja verdade ele retorna uma referencia, caso contrário um novo objeto é criado e associado ao ponteiro, assegurando assim a exclusividade desse objeto em nosso sistema.

    Para estabelecer conexão com o servidor é necessário iniciar a conexão, portanto criamos o método IsConnected para estabelecer a conexão e podermos iniciar a transmissão/recepção dos dados.

    bool CClientSocket::IsConnected(void)
      {
       ResetLastError();
       bool res=true;
    
       m_handler_socket=SocketCreate();
       if(m_handler_socket==INVALID_HANDLE)
          res=false;
    
       if(!::SocketConnect(m_handler_socket,m_host,m_port,m_time_out))
          res=false;
    
       return res;
      }

    Após uma conexão e transmissão de mensagens bem sucedida é necessário encerrar essa comunicação, para isso usaremos o método de Close para fechar a conexão aberta anteriormente.

    bool CClientSocket::Close(void)
      {
       bool res=false;
       if(SocketClose(m_handler_socket))
         {
          res=true;
          m_handler_socket=INVALID_HANDLE;
         }
       return res;
      }

    Agora precisamos escrever nosso servidor que irá receber novas conexões provenientes do MQL e fará o envio das previsões de nosso modelo.

    import socket
    
    class socketserver(object):
        def __init__(self, address, port):
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.address = address
            self.port = port
            self.sock.bind((self.address, self.port))
            
        def socket_receive(self):
            self.sock.listen(1)
            self.conn, self.addr = self.sock.accept()
            self.cummdata = ''
    
            while True:
                data = self.conn.recv(10000)
                self.cummdata+=data.decode("utf-8")
                if not data:
                    self.conn.close()
                    break
                return self.cummdata
        
        def socket_send(self, message):
            self.sock.listen(1)
            self.conn, self.addr = self.sock.accept()
            self.conn.send(bytes(message, "utf-8"))
        
                
        def __del__(self):
            self.conn.close()

    Nosso objeto é simples, em seu construtor recebemos o endereço e a porta que iremos criar nossos servidor. O método socket_received é responsável por aceitar uma nova conexão e verificar a existência de mensagens enviadas, caso haja mensagens a serem recebidas iniciamos um loop ate que todas as partes da mensagem seja recebida, após isso encerramos a conexão com o cliente e saímos do loop. Já o método socket_send é responsável por enviar mensagens ao nosso cliente, note que esse modelos proposto permite não so o envio de predições de nosso modelo mas abre possibilidades para vários outras coisas, tudo vai depender da sua criatividade e necessidade.

    Agora que temos a comunicação pronta precisamos pensar em duas coisas:

    1. Treinar um modelo e salva-lo.
    2. Usar o modelo treinado para previsões.

    Digo isso pois não seria prático e nem correto receber dados e fazer o treinamento a toda vez que quiséssemos fazer uma previsão. Por esse motivo eu sempre treino, acho os melhores hiperparametros e salvo meu modelo para uso posterior.

    Irei criar um arquivo com o nome model_train que conterá o código de treinamento da rede, nele usaremos a diferença percentual entre os preços de fechamento, e iremos tentar prever essa diferença. Note que não estou preocupado com o modelo quero demonstrar como usar um modelo integrando com o ambiente MQL.

    import MetaTrader5 as mt5
    from numpy.lib.financial import rate
    from pandas import to_datetime, DataFrame
    from datetime import datetime, timezone
    from matplotlib import pyplot
    from sklearn.metrics import mean_squared_error
    from math import sqrt
    import numpy as np
    
    from tensorflow.keras import Sequential
    from tensorflow.keras.layers import Dense
    from tensorflow.keras.callbacks import *
    
    
    symbol = "EURUSD"
    date_ini = datetime(2020, 1, 1, tzinfo=timezone.utc)
    date_end = datetime(2021, 7, 1, tzinfo=timezone.utc)
    period   = mt5.TIMEFRAME_D1
    
    def train_test_split(values, fator):
        train_size = int(len(values) * fator)
        return np.array(values[0:train_size]), np.array(values[train_size:len(values)])
    
    # split a univariate sequence into samples
    def split_sequence(sequence, n_steps):
            X, y = list(), list()
            for i in range(len(sequence)):
                    end_ix = i + n_steps
                    if end_ix > len(sequence)-1:
                            break
                    seq_x, seq_y = sequence[i:end_ix], sequence[end_ix]
                    X.append(seq_x)
                    y.append(seq_y)
            return np.array(X), np.array(y)
    
    if not mt5.initialize():
        print("initialize() failed")
        mt5.shutdown()
        raise Exception("Error Getting Data")
    
    rates = mt5.copy_rates_range(symbol, period, date_ini, date_end)
    mt5.shutdown()
    rates = DataFrame(rates)
    
    if rates.empty:
        raise Exception("Error Getting Data")
    
    rates['time'] = to_datetime(rates['time'], unit='s')
    rates.set_index(['time'], inplace=True)
    
    rates = rates.close.pct_change(1)
    rates = rates.dropna()
    
    X, y = train_test_split(rates, 0.70)
    X = X.reshape(X.shape[0])
    y = y.reshape(y.shape[0])
    
    train, test = train_test_split(X, 0.7)
    
    n_steps = 60
    verbose = 1
    epochs  = 50
    
    X_train, y_train = split_sequence(train, n_steps)
    X_test, y_test   = split_sequence(test, n_steps)
    X_val, y_val     = split_sequence(y, n_steps)
    
    # define model
    model = Sequential()
    model.add(Dense(200, activation='relu', input_dim=n_steps))
    model.add(Dense(1))
    model.compile(optimizer='adam', loss='mse')
    
    history = model.fit(X_train
                       ,y_train  
                       ,epochs=epochs
                       ,verbose=verbose
                       ,validation_data=(X_test, y_test))
    
    model.save(r'C:\YOUR_PATH\MQL5\Experts\YOUR_PATH\model_train_'+symbol+'.h5')
    
    pyplot.title('Loss')
    pyplot.plot(history.history['loss'], label='train')
    pyplot.plot(history.history['val_loss'], label='test')
    pyplot.legend()
    pyplot.show()
    
    history = list()
    yhat    = list()
    
    for i in range(0, len(X_val)):
            pred = X_val[i]
            pred = pred.reshape((1, n_steps))
            history.append(y_val[i])
            yhat.append(model.predict(pred).flatten()[0])
    
    pyplot.figure(figsize=(10, 5))
    pyplot.plot(history,"*")
    pyplot.plot(yhat,"+")
    pyplot.plot(history, label='real')
    pyplot.plot(yhat, label='prediction')
    pyplot.ylabel('Price Close', size=10)
    pyplot.xlabel('time', size=10)
    pyplot.legend(fontsize=10)
    
    pyplot.show()
    rmse = sqrt(mean_squared_error(history, yhat))
    mse = mean_squared_error(history, yhat)
    
    print('Test RMSE: %.3f' % rmse)
    print('Test MSE: %.3f' % mse)

    Agora que temos um modelo treinado e salvo em nossa pasta com a extensão .h5 ja podemos utiliza-lo para as previsões, por isso irei construir um Objeto que irá instanciar esse modelo para ser usado na comunicação.

    from tensorflow.keras.models import *
    
    class Model(object):
        def __init__(self, n_steps:int, symbol:str, period:int) -> None:
            super().__init__()
            self.n_steps = n_steps
            self.model = load_model(r'C:\YOUR_PATH\MQL5\Experts\YOUR_PATH\model_train_'+symbol+'.h5')
    
        def predict(self, data):
            return(self.model.predict(data.reshape((1, self.n_steps))).flatten()[0])

    Nosso objeto é simples, seu construtor instancia um atributo intitulado model que conterá o modelo salvo e o método predict é responsável por fazer a previsão.

    Agora precisamos do método main que ira rodar e fazer a comunicação com os clientes, recebendo as features e enviando as previsões.

    import ast
    import pandas as pd
    from model import Model
    from server_socket import socketserver
    
    host = 'localhost'
    port = 9091 
    n_steps = 60
    TIMEFRAME = 24 | 0x4000
    model   = Model(n_steps, "EURUSD", TIMEFRAME)
    
    if __name__ == "__main__":
        serv = socketserver(host, port)
    
        while True:
            print("<<--Waiting Prices to Predict-->>")
            rates = pd.DataFrame(ast.literal_eval(serv.socket_receive()))
            rates = rates.rates.pct_change(1)
            rates.dropna(inplace=True)
            rates = rates.values.reshape((1, n_steps))
            serv.socket_send(str(model.predict(rates).flatten()[0]))

    Do lado do nosso cliente MQL precisamos criar o robô que irá coletar os dados e enviar ao nosso servidor para posteriormente receber as predições. Como nosso modelo foi treinado com dados de velas encerradas iremos criar uma validação para somente coletar e enviar esses dados após o encerramento da vela, assim teremos os dados completos para prever a diferença percentual do fechamento da barra que acabou de iniciar, para isso iremos usar uma função que checa o surgimento de uma nova barra.

    bool NewBar(void)
      {
       datetime time[];
       if(CopyTime(Symbol(), Period(), 0, 1, time) < 1)
          return false;
       if(time[0] == m_last_time)
          return false;
       return bool(m_last_time = time[0]);
      }

    a variável m_last_time é declarada no escopo global e armazenará data e hora da abertura da barra, por isso fazemos o teste checando se a variável time é diferente de m_last_time, pois caso verdadeiro significa que uma nova barra se iniciou, então precisamos substituir o valor de m_last_time pelo valor de time.

    Como não queremos que o EA abra diversas posições sem o encerramento da anterior iremos validar a existência de posições em aberto, para isso usaremos o método CheckPosition que irá atribuir como true ou false as variáveis buy e sell declaradas no escopo global.

    void CheckPosition(void)
      {
       buy = false;
       sell  = false;
    
       if(PositionSelect(Symbol()))
         {
          if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY&&PositionGetInteger(POSITION_MAGIC) == InpMagicEA)
            {
             buy = true;
             sell  = false;
            }
          if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL&&PositionGetInteger(POSITION_MAGIC) == InpMagicEA)
            {
             sell = true;
             buy = false;
            }
         }
      }

    Após o surgimento de uma nova barra, validamos a existência de posições em aberto, se tiver posições em aberto iremos aguardar o encerramento. Se não tiver nenhuma posição em aberto iremos inicar o processo de comunicação chamando o método IsConnected da classe CClienteSocket

    if(NewBar())
       {
          if(!Socket.IsConnected())
             Print("Error : ", GetLastError(), " Line: ", __LINE__);
        ...
       }

    se o retorno for verdadeiro ou seja, conseguimos estabelecer conexão com nosso servidor iremos coletar os dados e envia-lo.

    string payload = "{'rates':[";
    for(int i=InpSteps; i>=0; i--)
       {
          if(i>=1)
             payload += string(iClose(Symbol(), Period(), i))+",";
          else
             payload += string(iClose(Symbol(), Period(), i))+"]}";
       }

    Decidi enviar os dados no formato {'rates':[1,2,3,4]} pois dessa forma transformaremos em um dataframe pandas de forma simples e não perderemos tempo fazendo conversões.

    Feito a coleta de dados fazemos o envio e aguardamos o recebimento da predição para podermos tomar alguma decisão, no caso irei uma média móvel para verificar a direção do preço e dependendo do lado e valor da predição iremos comprar ou vender.

    void OnTick(void)
      {
          ....
    
          bool send = Socket.SocketSend(payload);
          if(send)
            {
             if(!Socket.IsConnected())
                Print("Error : ", GetLastError(), " Line: ", __LINE__);
    
             double yhat = StringToDouble(Socket.SocketReceive());
    
             Print("Value of Prediction: ", yhat);
    
             if(CopyBuffer(handle, 0, 0, 4, m_fast_ma)==-1)
                Print("Error in CopyBuffer");
    
             if(m_fast_ma[1]>m_fast_ma[2]&&m_fast_ma[2]>m_fast_ma[3])
               {
                if((iClose(Symbol(), Period(), 2)>iOpen(Symbol(), Period(), 2)&&iClose(Symbol(), Period(), 1)>iOpen(Symbol(), Period(), 1))&&yhat<0)
                  {
                   m_trade.Sell(mim_vol);
                  }
               }
    
             if(m_fast_ma[1]<m_fast_ma[2]&&m_fast_ma[2]<m_fast_ma[3])
               {
                if((iClose(Symbol(), Period(), 2)<iOpen(Symbol(), Period(), 2)&&iClose(Symbol(), Period(), 1)<iOpen(Symbol(), Period(), 1))&&yhat>0)
                  {
                   m_trade.Buy(mim_vol);
                  }
               }
            }
            
          Socket.Close();
         }
      }

    o ultimo passo é fechar a conexão estabelecida anteriormente e aguardar o surgimento de uma nova barra, e com isso iniciar o processo de envio e recebimento da predição.

    Essa arquitetura se mostra muito útil  e de baixa latência como pode ser testado, em alguns projetos pessoais optei por usar essa arquitetura em contas de negociação pois me permitem usufruir de todo o poder do MQL e de todos os recursos disponíveis para Machine Learinig disponíveis em Python


    O que vem agora?

    No próximo artigo, quero desenvolver uma arquiteta um pouco mais flexível permitindo o uso dos modelos no testador de estratégia, assim quebraremos o única barreira que pode nos impedir de usar a arquitetura proposta.

    Conclusão:

    Espero ter dado um pequeno direcionamento de como podemos usar e desenvolver diversos modelos em Python e comunica-los com o ambiente MQL.

    Você aprendeu:

    1. Montar um ambiente de desenvolvimento Python.
    2. Relembramos e implementamos um neurônio perceptron e uma rede MLP em python.
    3. Preparamos dados univariados para o aprendizado de uma rede simples. 
    4. Montamos uma arquitetura de comunicação entre Python e MQL.


    Extensões:

    Esta seção lista algumas ideias para estender o tutorial que você pode querer explorar.

    • Tamanho da entrada. Explore mais ou menos o número de dias usados como entrada para o modelo, como três dias, 21 dias, 30 dias e muito mais.
    • Tunning do modelo. Explore diferentes estruturas e hiperparmetros para um modelo e levante o desempenho do modelo em média.
    • Dimensionamento de dados. Explorar se o dimensionamento de dados, como padronização e normalização, pode ser usado para melhorar o desempenho do modelo.
    • Diagnóstico de Aprendizagem. Use diagnósticos como curvas de aprendizagem para a perda de treino e validação e erro médio ao quadrado para ajudar a ajustar a estrutura e os hiperparmetros do modelo.

    Se você explorar alguma dessas extensões, eu adoraria saber.


    Arquivos anexados |
    MQL5.zip (179.04 KB)
    Combinatória e teoria da probabilidade para negociação (Parte II): fractal universal Combinatória e teoria da probabilidade para negociação (Parte II): fractal universal
    Neste artigo, continuaremos a estudar fractais e prestaremos muita atenção a resumir todo o material. Tentarei apresentar todos os projetos da maneira mais compacta e compreensível para serem aplicados ao trading.
    Gráficos na biblioteca DoEasy (Parte 81): integrando gráficos nos objetos da biblioteca Gráficos na biblioteca DoEasy (Parte 81): integrando gráficos nos objetos da biblioteca
    Hoje começaremos a integrar os objetos gráficos já criados nos restantes, o que, em última análise, dotará cada objeto da biblioteca com seu próprio objeto gráfico, permitindo ao usuário interagir com o programa.
    Combinatória e teoria da probabilidade para negociação (Parte III): primeiro modelo matemático Combinatória e teoria da probabilidade para negociação (Parte III): primeiro modelo matemático
    Para dar continuação lógica ao tópico, hoje abordaremos o desenvolvimento de modelos matemáticos multifuncionais para tarefas de negociação. Assim sendo, descreverei todo o processo de desenvolvimento do primeiro modelo matemático para descrever fractais a partir do zero. Este modelo deve se tornar um importante alicerce, ser multifuncional e universal, inclusive para construir a base teórica para o futuro desenvolvimento do ramo.
    Como se tornar um bom programador (Parte 1): cinco hábitos que devem ser abandonados para programar melhor em MQL5 Como se tornar um bom programador (Parte 1): cinco hábitos que devem ser abandonados para programar melhor em MQL5
    Tanto iniciantes quanto programadores avançados têm alguns hábitos ruins que os impedem de melhorar. Neste artigo, vamos discuti-los e ver o que podemos fazer com eles. O artigo é destinado a todos que desejam se tornar um programador MQL5 de sucesso.