![Perceptrón Multicapa y Algoritmo de Retropropagación (Parte II): Implementación en Python e integración en MQL5](https://c.mql5.com/2/49/Multilayer-perceptron-and-backpropagation-algorithm_002_600x314.jpg)
Perceptrón Multicapa y Algoritmo de Retropropagación (Parte II): Implementación en Python e integración en MQL5
Introducción
En el artículo anterior, vimos cómo crear una neurona simple (perceptrón) y exploramos cómo se realizan los cálculos de descenso de gradiente, cómo se construye una red (MLP) compuesta por perceptrones interconectados y cómo se lleva a cabo el entrenamiento de este tipo de red.
En este artículo quiero demostrar lo simple que es implementar este tipo de algoritmo con la ayuda del lenguaje Python.
Python se introdujo en la gama de herramientas MQL5 y abre la puerta a numerosas posibilidades, como la exploración de datos, la creación y el uso de modelos de aprendizaje automático.
Esta integración nativa de MQL5 en Python abre las puertas a muchas posibilidades de uso que nos permiten construir desde una simple regresión lineal a un modelo de aprendizaje profundo. Como es un lenguaje para uso profesional, existe una gran variedad de bibliotecas que hacen todo el trabajo pesado que llevan consigo los cálculos.
En nuestro ejemplo, construiremos una red manualmente, pero como se dijo en el artículo anterior, es solo un paso para comprender lo que realmente sucede durante el proceso de entrenamiento y predicción, luego mostraré un ejemplo más sofisticado usando TensorFlow y Keras.
¿Qué es TensorFlow?
TensorFlow es una biblioteca de código abierto para procesamiento numérico rápido.
Fue creado, mantenido y lanzado por Google bajo la licencia de código abierto Apache 2.0. La API es virtualmente para el lenguaje de programación Python, aunque hay acceso a la API C++ subyacente.
A diferencia de otras bibliotecas numéricas destinadas a ser utilizadas en aprendizaje profundo como Theano, TensorFlow está diseñado para su uso tanto en investigación como en sistemas de desarrollo y producción, especialmente RankBrain en el sistema de búsqueda de Google y el divertido proyecto DeepDream.
Puede ejecutarse en sistemas de CPU única, GPU, así como en dispositivos móviles y sistemas distribuidos a gran escala de cientos de máquinas.
¿Qué es Keras?
Keras es una biblioteca de Python, poderosa, de código abierto y de fácil uso al desarrollar y evaluar modelos de aprendizaje profundo.
Abarca las eficientes bibliotecas de cálculo Theano y TensorFlow, y permite definir y entrenar modelos de redes neuronales en solo unas pocas líneas de código.
Tutorial
Este tutorial se divide en 4 partes:
- Instalación y preparación del entorno Python en MetaEditor.
- Primeros pasos y reconstrucción del modelo (perceptrón y MLP).
- Creación de un modelo simple usando Keras y TensorFlow.
- Cómo integrar MQL5 en Python.
1. Instalación y preparación del entorno Python.
Comenzaremos descargando Python desde su sitio web oficial www.python.org/downloads/
Para poder trabajar con TensorFlow es necesario instalar una versión superior a 3.3 e inferior a 3.8, yo uso la versión 3.7.
Después de descargar e iniciar el proceso de instalación, activamos la opción "Add Python 3.7 to PATH", esto asegurará que algunas cosas funcionen sin la necesidad de configuración adicional en el futuro.
Para poder ejecutar un script Python directamente desde nuestro terminal MetaTrader 5, solo necesitamos realizar una configuración previa.
- Establecer la ruta del ejecutable de Python (entorno)
- Instalar las dependencias necesarias para el proyecto
Necesitamos abrir MetaEditor e ir a Herramientas>Opciones.
En esta sesión, necesitamos definir la ruta donde está nuestro ejecutable de Python, tenga en cuenta que después de la instalación probablemente contendrá la ruta de Python predeterminada, si, por alguna razón, no la contiene, coloque la ruta al ejecutable completa para que pueda ejecutar los scripts directamente desde su terminal MetaTrader 5.
En mi caso, utilizo un entorno de biblioteca completamente separado, llamado entorno virtual, es una forma de obtener una instalación "limpia" y poder reunir solo las bibliotecas necesarias para el proyecto.
Para obtener más información sobre el paquete venv, consulte el enlace.
Una vez hecho esto, podemos ejecutar scripts de Python directamente desde la Terminal. Para nuestro experimento, necesitaremos instalar las bibliotecas:
Si tiene dudas sobre cómo instalar las bibliotecas, consulte el manual de instalación del módulo.
- MetaTrader 5
- TensorFlow
- Matplotlib
- Pandas
- Sklearn
Ahora que hemos instalado y configurado el entorno, hagamos una breve prueba para entender cómo crear y ejecutar un script en nuestra terminal. Para iniciar un nuevo script directamente desde MetaEditor podemos seguir los pasos a continuación:
Nuevo > Python script
Defina un nombre para su script, el asistente de creación de MetaEditor nos sugiere importar algunas librerías automáticamente, esto es algo muy interesante y para nuestro experimento seleccionaremos la opción numpy.
Ahora creemos un script simple que genere un gráfico de seno.
# 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 ejecutar el script, basta con clicar en compilar (F7) o abrir el terminal MetaTrader 5 y adjuntar el script a un gráfico. Después de esto, los resultados se mostrarán en la pestaña expertos en caso de que haya prints, como sucede en el desarrollo de un algoritmo MQL5, en nuestro caso se abrirá una ventana con la gráfica de la función que creamos arriba.
2 - Primeros pasos y reconstrucción del modelo (perceptrón y MLP).
Usaremos el mismo conjunto de datos que el ejemplo en MQL5, modo de simplificación.
A continuación veremos una función denominada predict() que anticipa un valor de salida para una fila, dado un conjunto de pesos, en este caso, el primer peso siempre será el sesgo. Y también la función de activación.
# 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 ya sabemos para entrenar una red necesitamos hacer el proceso de descenso de gradiente, que se discutió ampliamente en el artículo anterior, a continuación muestro la función de entrenamiento “ 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
Aplicación del modelo MLP:
Este tutorial se divide en 5 partes:
- Inicio de la red
- Propagación (FeedForward)
- BackPropagation
- Entrenamiento
- Previsión
Inicio de la red:
Comencemos con algo fácil, creemos una nueva red lista para el entrenamiento.
Cada neurona tiene un conjunto de pesos que deben mantenerse. Un peso para cada conexión de entrada y un peso adicional para el sesgo. Necesitaremos almacenar propiedades adicionales para una neurona durante el entrenamiento, por lo que usaremos un diccionario para representar cada neurona y para almacenar propiedades por nombres como 'pesos' para los 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)
Ahora que sabemos cómo crear e iniciar una red, veamos cómo podemos usarla para calcular una salida.
Propagación (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)
Al ejecutar el script de arriba tendremos el resultado:
[0.6629970129852887, 0.7253160725279748]
Los valores reales de salida son simplemente absurdos por ahora, pero luego comenzaremos a aprender cómo hacer que los pesos en las neuronas sean más útiles.
Retropropagación
# 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)
El ejemplo de ejecución imprime la red después de que se completa la verificación de errores. Puede ver que los valores de error se calculan y almacenan en neuronas para la capa de salida y la capa 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}]
Entrenamiento de la red
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)
Una vez entrenada, se imprime la red y se muestran los pesos aprendidos. También todavía en la red están los valores de salida y delta que se pueden ignorar. Podríamos actualizar nuestra función de entrenamiento para eliminar estos datos si quisiéramos.
>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 predecir podemos usar el conjunto de pesos ya ajustado en el ejemplo anterior
Prediciendo
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))
Al ejecutar el ejemplo, se imprime el resultado esperado para cada registro en el conjunto de datos de entrenamiento, seguido de la predicción clara realizada por la red.
Esto muestra que la red alcanza el 100% de precisión en este pequeño conjunto de datos.
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. Creación de un modelo simple usando Keras y TensorFlow.
Para recopilar los datos utilizaremos el paquete MetaTrader 5, iniciamos nuestro script importando las bibliotecas necesarias para la extracción, transformación y previsión de precios. Tenga en cuenta que no cubriremos la parte de preparación de datos en detalle, pero no se olvide de que es un paso muy importante para el modelo.
Comenzaremos con una breve vista previa de los datos. El conjunto de datos (dataset) se compone de las últimas 1000 barras del activo EURUSD. Para esto necesitamos algunos pasos, por ejemplo:
- Importación de bibliotecas
- Conexión con MetaTrader
- Recolección de datos
- Transformación de datos, ajuste de fecha
- Graficando datos
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()
Después de ejecutar el código, visualizaremos los datos de cierre en un gráfico de líneas como se muestra a continuación.
Usemos un enfoque de regresión en la que se predecirá el valor de cierre del próximo período.
Para este ejemplo usaremos un enfoque univariado y, como se indicó anteriormente, sin prestar atención a las mejores prácticas en la preparación de datos.
La serie de tiempo univariante es un conjunto de datos compuesto por una sola serie de observaciones con un orden temporal, y se necesita un modelo para aprender de la serie de observaciones pasadas para predecir el siguiente valor en la secuencia.
El primer paso es dividir la serie cargada en un conjunto de entrenamiento y prueba. Para ello, crearemos una función que dividirá tal serie en dos partes, usaremos un valor porcentual del tamaño total de la serie para el corte, por ejemplo, 70% para entrenamiento y 30% para pruebas. Para la validación (backtest) tenemos otros enfoques, como dividir la serie en entrenamiento, prueba y validación. Como hablamos de la serie financiera, todos los cuidados para evitar el sobreajuste no son suficientes.
En nuestra función esperamos recibir una matriz numpy y un valor de corte, y devolveremos dos series ajustadas.
El primer retorno es el conjunto completo desde la primera posición 0 hasta el tamaño que representa el tamaño del factor, y la segunda serie es todo lo demás.
def train_test_split(values, fator): train_size = int(len(values) * fator) return values[0:train_size], values[train_size:len(values)]
Los modelos en Keras se pueden definir como una secuencia de capas.
Creamos un modelo secuencial y agregamos capas -de una en una- hasta que estemos satisfechos con nuestra arquitectura de red.
Lo primero que debe hacer bien es asegurarse de que la capa de entrada tenga el número correcto de recursos de entrada. Esto se puede especificar al crear la primera capa con el argumento input_dim.
¿Cómo sabemos el número de capas y sus tipos?
Esta es una pregunta muy difícil. Hay heurísticas que podemos usar y, a menudo, la mejor estructura de red se encuentra a través de un proceso de experimentación de prueba y error. Generalmente, se necesita una red lo suficientemente grande para capturar la estructura del problema.
En este ejemplo, usaremos una estructura de red completamente conectada con una capa.
Las capas completamente conectadas se definen utilizando la clase Dense. Podemos especificar el número de neuronas o nodos en la capa como primer argumento y especificar la función de activación usando el argumento de activación.
Usaremos la función de activación de la unidad lineal rectificada referida como ReLU en la primera capa.
Antes de poder predecir una serie univariante, ésta debe prepararse.
El modelo MLP aprenderá con una función que asigna una secuencia de observaciones pasadas como entrada a una observación de salida. Como tal, la secuencia de observaciones debe transformarse en múltiples ejemplos de los que el modelo pueda aprender.
Considere una secuencia univariante:
[10, 20, 30, 40, 50, 60, 70, 80, 90]
Podemos dividir la secuencia en múltiples patrones de entrada/salida llamados muestras.
Para nuestro ejemplo, usaremos tres etapas de tiempo que se usan como entrada y un paso de tiempo que se usa como salida para la predicción que se está aprendiendo.
X, y
10, 20, 30 40
20, 30, 40 50
30, 40, 50 60
...
A continuación, creamos la función split_sequence() que implementa este comportamiento y dividirá un conjunto univariante en varias muestras donde cada una tiene un número específico de pasos de tiempo, y la salidaes un solo paso de tiempo.
Podemos probar nuestra función en un pequeño conjunto de datos, como los datos inventados anteriormente en el ejemplo.
# 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])
La ejecución del ejemplo divide la serie univariante en seis muestras, cada muestra tiene tres pasos de tiempo de entrada y un paso de tiempo de salida.
[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 continuar, necesitamos separar la muestra en X (feature) e y (target) para que podamos entrenar la red, para eso usaremos la función split_sequence(), que creamos anteriormente
X_train, y_train = split_sequence(train, 3) X_test, y_test = split_sequence(test, 3)
Ahora que hemos preparado nuestras muestras de datos, podemos crear la red MLP.
Un modelo MLP simple tiene una sola capa oculta de nodos (neuronas) y una capa de salida que se usa para hacer una predicción.
Podemos definir un MLP para predecir series de tiempo univariadas de la siguiente manera.
# define model model = Sequential() model.add(Dense(100, activation='relu', input_dim=n_steps)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse')
Un punto importante en la definición es la forma de la entrada, esto es lo que el modelo espera como entrada para cada muestra en términos del número de pasos de tiempo.
El número de pasos de tiempo como entrada es el número que elegimos al preparar nuestro conjunto de datos como argumento para la función split_sequence().
La dimensión de entrada de cada muestra se especifica en el argumento input_dim en la definición de la primera capa oculta. Técnicamente, el modelo mostrará cada paso de tiempo como un recurso separado en lugar de pasos de tiempo separados.
Casi siempre tenemos varias muestras, por lo que el modelo espera que el componente de entrada de datos de entrenamiento tenga las dimensiones o la forma:
[samples, features]
La función split_sequence() genera la X con la forma [muestras, características] lista para usar.
El modelo se entrena mediante un algoritmo eficiente llamado Adam para descenso de gradiente estocástico y optimizado utilizando la función de pérdida de error cuadrático medio, o 'mse'.
Una vez definido el modelo, podemos entrenarlo con el conjunto de datos.
model.fit(X_train, y_train, epochs=100, verbose=2)
Una vez que se entrena el modelo, podemos predecir un valor futuro, el modelo espera que la forma de entrada sea bidimensional con [muestras, características], por lo que debemos remodelar la muestra de entrada única antes de hacer la predicción, por ejemplo, con la forma [ 1, 3] para 1 muestra y 3 pasos de tiempo utilizados como características de entrada.
Por ejemplo, seleccionaremos el último registro de la muestra de prueba “X_test” y luego de la predicción lo compararemos con el valor real contenido en la última muestra “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. Cómo integrar MQL5 en Python.
Para poder utilizar el modelo en una cuenta de tráding tenemos algunas opciones, una de ellas es utilizar las funciones nativas en Python que abren y cierran posiciones, pero para este escenario no tendremos las numerosas posibilidades que nos ofrece MQL, por esta razón opté por una integración entre Python y el entorno MQL, esto nos dará más autonomía en la gestión de nuestras posiciones/pedidos.
Basado en el artículo sobre MQL5 Integración de MetaTrader 5 y Python: recibiendo y enviando datos escrito por Maxim Dmitrievsky implementé esta clase usando un patrón de desarrollo llamado Singleton que se encargará de crear un cliente Socket para la comunicación. Este patrón asegura que solo haya una copia de un cierto tipo de objeto, porque si el programa usa dos punteros haciendo referencia al mismo objeto, éstos apuntarán al mismo 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); };
La clase CClienteSocke almacena un puntero estático como miembro privado, la clase solo tiene un constructor que es privado y no se puede llamar, en lugar de llamar al constructor, se puede usar el método Socket, por lo que nos aseguramos de que solo se use un solo objeto .
static CClientSocket *CClientSocket::Socket(void) { if(CheckPointer(m_socket)==POINTER_INVALID) m_socket=new CClientSocket(); return m_socket; }
Este método comprueba si el puntero estático apunta al objeto CClienteSocket y si es verdadero devuelve una referencia, de lo contrario se crea un nuevo objeto y se asocia al puntero, asegurando así la exclusividad de este objeto en nuestro sistema.
Para establecer una conexión con el servidor es necesario iniciar la conexión, por eso creamos el método IsConnected para establecer la conexión y podemos empezar a transmitir/recibir datos.
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; }
Luego de una conexión y transmisión de mensajes exitosas es necesario cerrar esta comunicación, para eso usaremos el método Close para cerrar la conexión previamente abierta.
bool CClientSocket::Close(void) { bool res=false; if(SocketClose(m_handler_socket)) { res=true; m_handler_socket=INVALID_HANDLE; } return res; }
Ahora necesitamos escribir nuestro servidor que recibirá nuevas conexiones de MQL y enviará nuestras predicciones del 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()
Nuestro objeto es simple, en su constructor recibimos la dirección y el puerto con los que crearemos nuestro servidor. El método socket_received se encarga de aceptar una nueva conexión y comprobar la existencia de mensajes enviados, si hay mensajes por recibir iniciamos un bucle hasta recibir todas las partes del mensaje, luego cerramos la conexión con el cliente y salimos del círculo. El método socket_send, por otro lado, se encarga de enviar mensajes a nuestro cliente, tenga en cuenta que este modelo propuesto permite no solo enviar predicciones de nuestro modelo, sino que abre posibilidades para varias cosas más, todo dependerá de su creatividad y necesidad.
Ahora que tenemos la comunicación lista, debemos pensar en dos cosas:
- Entrenar un modelo y guárdalo.
- Utilizar el modelo entrenado para realizar predicciones.
Digo esto porque no sería práctico ni correcto recibir datos y hacer el entrenamiento cada vez que quisiéramos hacer una predicción. Por esta razón, siempre entreno, encuentro los mejores hiperparámetros y guardo mi modelo para su uso posterior.
Crearé un archivo llamado model_train que contendrá el código de entrenamiento de la red, en él usaremos la diferencia porcentual entre los precios de cierre e intentaremos predecir esta diferencia. Tenga en cuenta que no me preocupa el modelo, quiero demostrar cómo usar un modelo integrándolo con el entorno 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)
Ahora que tenemos un modelo entrenado y guardado en nuestra carpeta con la extensión .h5 ya podemos usarlo para predicciones, así que construiré un Objeto que instanciará este modelo para ser usado en comunicación.
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])
Nuestro objeto es simple, su constructor instancia un atributo llamado modelo que contendrá el modelo guardado, y el método de predicción es responsable de hacer la predicción.
Ahora necesitamos el método principal que se ejecutará y se comunicará con los clientes, recibiendo funciones y enviando predicciones.
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]))
En nuestro lado del cliente MQL, necesitamos crear el robot que recopilará los datos y los enviará a nuestro servidor para luego recibir las predicciones. Como nuestro modelo fue entrenado con datos de velas cerradas, crearemos una validación para recopilar y enviar estos datos solo después de que se cierre la vela, por lo que tendremos los datos completos para predecir la diferencia porcentual del cierre de la barra que acaba de comenzar, para esto usaremos una función que verifica la apariencia de una nueva 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]); }
La variable m_last_time está declarada en el ámbito global y almacenará la fecha y hora de la apertura de la barra, por lo que hacemos la prueba comprobando si la variable time es diferente de m_last_time, porque si es true significa que ha comenzado una nueva barra, por lo que necesitamos reemplazar el valor de m_last_time por el valor de tiempo.
Como no queremos que el EA abra varias posiciones sin cerrar la anterior, validaremos la existencia de posiciones abiertas, para eso usaremos el método CheckPosition que configurará las variables de compra y venta declaradas en el ámbito global como true o false.
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; } } }
Luego de la aparición de una nueva barra, validamos la existencia de posiciones abiertas, si hay posiciones abiertas esperaremos el cierre. Si no hay posición abierta, iniciaremos el proceso de comunicación llamando al método IsConnected de la clase CClienteSocket
if(NewBar()) { if(!Socket.IsConnected()) Print("Error : ", GetLastError(), " Line: ", __LINE__); ... }
si la devolución es true es porque podemos establecer una conexión con nuestro servidor, recogeremos los datos y los enviaremos.
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))+"]}"; }
Decidí enviar los datos en el formato {'rates':[1,2,3,4]} porque así lo transformaremos en un dataframe de pandas de una manera sencilla y no perderemos el tiempo haciendo conversiones.
Una vez recolectados los datos, los enviamos y esperamos recibir la predicción para que podamos tomar alguna decisión, en este caso usaré una media móvil para verificar la dirección del precio; y dependiendo del lado y valor de la predicción, compraremos o venderemos.
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(); } }
el último paso es cerrar la conexión previamente establecida y esperar a que aparezca una nueva barra, y con ello iniciar el proceso de envío y recepción de la predicción.
Esta arquitectura demuestra ser muy útil y de baja latencia ya que se puede probar, en algunos proyectos personales elegí usar esta arquitectura en cuentas comerciales porque me permite disfrutar de toda la potencia de MQL y todos los recursos disponibles para aprendizaje de máquina disponibles en Pitón
¿Qué viene ahora?
En el próximo artículo, quiero desarrollar una arquitectura un poco más flexible que permita el uso de modelos en el probador de estrategias, por lo que romperemos la única barrera que podría impedirnos usar la arquitectura propuesta.
Conclusión:
Espero haber dado una pequeña orientación sobre cómo podemos usar y desarrollar varios modelos de Python y comunicarlos con el entorno MQL.
Ud. has aprendido a:
- Configurar un entorno de desarrollo de Python.
- Recordamos e implementamos una neurona perceptrón y una red MLP en Python.
- Preparamos datos univariados para el aprendizaje de una red simple.
- Configuramos una arquitectura de comunicación entre Python y MQL.
Extensiones:
Esta sección enumera algunas ideas para ampliar el tutorial que quizás Ud. desee explorar.
- Tamaño de entrada. Explore de manera aproximada la cantidad de días utilizados como entrada para el modelo, como tres días, 21 días, 30 días y más.
- Ajuste de modelo. Explore diferentes estructuras e hiperparámetros para un modelo y obtenga el rendimiento promedio del modelo.
- Escalado de datos. Explore si el tamaño de los datos, como la estandarización y la normalización, se puede utilizar para mejorar el rendimiento del modelo.
- Diagnóstico de aprendizaje. Utilice diagnósticos como las curvas de aprendizaje para la pérdida de entrenamiento y la validación y el error cuadrático medio, para ayudar a ajustar la estructura y los hiperparámetros del modelo.
Si explora alguna de estas extensiones, me encantaría saberlo.
Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/9514
![Gráficos en la biblioteca DoEasy (Parte 83): Clase de objeto gráfico abstracto estándar](https://c.mql5.com/2/43/MQL5-avatar-doeasy-library3-2__2.png)
![Explorando opciones para crear velas multicolores](https://c.mql5.com/2/43/multicolored-candlesticks.png)
![Cómo ser un mejor programador (parte 05): Aprendiendo a desarrollar más rápido](https://c.mql5.com/2/43/speed__1.png)
![Análisis de spread Bid/Ask en MetaTrader 5](https://c.mql5.com/2/43/bid-ask-spread.png)
![MQL5 - Lenguaje de estrategias comerciales para el terminal de cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso