![Многослойный перцептрон и алгоритм обратного распространения ошибки (Часть II): Реализация на Python и интеграция с MQL5](https://c.mql5.com/2/49/Multilayer-perceptron-and-backpropagation-algorithm_002_600x314.jpg)
Многослойный перцептрон и алгоритм обратного распространения ошибки (Часть II): Реализация на Python и интеграция с MQL5
Введение
В предыдущей статье мы увидели, как создать простой нейрон (перцептрон), и изучили, как выполняются вычисления градиентного спуска, как строится сеть (MLP), состоящая из взаимосвязанных перцептронов, и как выполняется обучение этого типа сети.
В этой статье я хочу продемонстрировать, насколько просто реализовать этот тип алгоритма с помощью языка Python.
Python был внедрен в набор инструментов MQL5 и открывает двери для многих возможностей, таких как изучение данных, создание и использование моделей машинного обучения.
Эта встроенная интеграция MQL5 в Python открывает для нас много возможностей, которые позволяют построить от простой линейной регрессии до моделей глубокого обучения. Поскольку это язык для профессионального использования, существует множество библиотек, которые берут на себя всю тяжелую работу, связанную с вычислениями.
В нашем примере мы построим сеть вручную, но, как было сказано в предыдущей статье, это всего лишь шаг, чтобы понять, что на самом деле происходит в процессе обучения и прогнозирования, а затем я покажу более сложный пример с использованием TensorFlow и Keras.
Что такое TensorFlow?
TensorFlow - это библиотека с открытым кодом для быстрой числовой обработки.
Он был создан, поддержан и выпущен Google под лицензией Apache 2.0 с открытым кодом. API виртуально предназначен для языка программирования Python, хотя есть доступ к базовому C++ API.
В отличие от других числовых библиотек, предназначенных для использования в глубоком обучении, таких как Theano, TensorFlow предназначен для использования как в научно-исследовательских, так и в системах разработки и производства, особенно RankBrain в поисковой системе Google и в интересном проекте DeepDream.
Он может работать в системах на одном процессоре, на графическом процессоре, а также на мобильных устройствах и на крупномасштабных системах, распределенных на сотни машин.
Что такое Keras?
Keras - это мощная библиотека Python с открытым кодом, которую легко использовать при разработке и оценке моделей глубокого обучения.
Он охватывает эффективные библиотеки вычислений Theano и TensorFlow и позволяет определять и обучать модели нейронных сетей всего за несколько строк кода.
Руководство
Это руководство разделено на 4 части:
- Установка и подготовка среды Python в MetaEditor.
- Первые шаги и реконструкция модели (перцептрон и MLP).
- Создание простой модели с использованием Keras и TensorFlow.
- Как интегрировать MQL5 в Python.
1. Установка и подготовка среды Python.
Начнем с загрузки Python с официального сайта www.python.org/downloads/
Чтобы работать с TensorFlow необходимо установить версию выше 3.3 и ниже 3.8; я использую версию 3.7.
После загрузки и начала процесса установки мы активируем опцию «Добавить Python 3.7 в PATH», это гарантирует, что некоторые вещи будут работать без необходимости дополнительной настройки в будущем.
Чтобы запустить скрипт Python прямо из нашего терминала MetaTrader 5, нам нужно только выполнить предыдущую настройку.
- Установить путь исполняемого файла Python (среда)
- Установите необходимые зависимости для проекта
Нам нужно открыть MetaEditor и перейти в Инструменты>Параметры.
В данном этапе нам нужно определить путь, на котором находится наш исполняемый файл Python, и иметь в виду, что после установки он, вероятно, будет содержать путь Python по умолчанию, если по какой-то причине это не так, то введите полный путь к исполняемому файлу, чтобы вы могли запустить скрипты прямо из вашего терминала MetaTrader 5.
В моем случае я использую полностью отдельную библиотечную среду, называемую виртуальной средой, это способ получить «чистую» установку и собрать только библиотеки, необходимые для проекта.
Для получения дополнительной информации о пакете venv перейдите по ссылке.
Как только это будет готово, мы сможем запускать скрипты Python прямо из терминала. Для нашего эксперимента нам потребуется установить библиотеки:
Если у вас есть вопросы о том, как установить библиотеки, смотрите руководство по установке модуля.
- MetaTrader 5
- TensorFlow
- Matplotlib
- Pandas
- Sklearn
Теперь, когда мы установили и настроили среду, давайте проведем небольшой тест, чтобы понять, как создать и запустить скрипт в нашем терминале. Чтобы запустить новый скрипт прямо из MetaEditor, мы можем выполнить следующие шаги:
Новый > Python script
Определите имя для вашего скрипта, мастер создания MetaEditor подсказывает нам автоматически импортировать некоторые библиотеки, это очень интересно и для нашего эксперимента мы выберем вариант numpy.
Теперь давайте создадим простой скрипт, который генерирует синусоидальный график.
# 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()
Чтобы запустить скрипт, просто нажмите на компилировать (F7) или откройте терминал MetaTrader 5 и прикрепите скрипт к графику. После этого результаты будут отображаться во вкладке экспертов в случае, если есть распечатки, как при разработке алгоритма MQL5, в нашем случае откроется окно с графиком функции, которую мы создали выше.
2 - Первые шаги и реконструкция модели (перцептрон и MLP).
Мы будем использовать тот же набор данных, что и пример в MQL5, чтобы было проще.
Затем мы увидим функцию под названием predict(), которая прогнозирует выходное значение для строки с заданным набором весов, в этом случае первым весом всегда будет смещение. А также функция активации.
# 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)
Как мы уже знаем, для обучения сети нам необходимо выполнить процесс градиентного спуска, который подробно обсуждался в предыдущей статье, а в продолжение я покажу обучающую функцию "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
Применение модели MLP:
Это руководство разделено на 5 частей:
- Запуск сети
- Распространение (FeedForward)
- Обратное распространение (BackPropagation)
- Обучение
- Прогноз
Запуск сети:
Начнем с чего-нибудь простого, для этого создадим новую сеть, готовую к обучению.
У каждого нейрона есть набор весов, которые необходимо поддерживать. Один вес для каждого входного соединения и один дополнительный вес для смещения. Нам нужно будет сохранить дополнительные свойства нейрона во время обучения, поэтому мы будем использовать словарь для отображения каждого нейрона и чтобы хранить свойства по именам, например, как «веса» для весов.
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)
Теперь, когда мы знаем, как создать и запустить сеть, давайте посмотрим, как мы можем использовать ее для расчета выходных данных.
Распространение (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)
При выполнении приведенного выше скрипта мы получим результат:
[0.6629970129852887, 0.7253160725279748]
Фактические выходные значения пока просто абсурдны, но скоро мы узнаем, как сделать так, чтобы веса в нейронах были более полезными.
Обратное распространение
# 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)
Образец выполнения распечатывает сеть после завершения проверки ошибок. Как мы видим, значения ошибок вычисляются и сохраняются в нейронах для выходного слоя и скрытого слоя.
[{'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}]
Обучение сети
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)
После обучения сеть распечатывается и отображаются выученные веса. Также в сети еще остаются выходные значения и значения дельты, которые можно игнорировать. Мы могли бы обновить нашу функцию обучения, чтобы удалить эти данные, если бы захотели.
>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}]
Чтобы сделать прогноз, мы можем использовать уже настроенный в предыдущем примере набор весов.
Прогнозирование
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))
При выполнении примера распечатывается ожидаемый результат для каждой записи в наборе обучающих данных, после чего следует четкий прогноз, сделанный сетью.
Это показывает, что сеть достигает 100% точности на этом небольшом наборе данных.
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. Создание простой модели с использованием Keras и TensorFlow.
Для сбора данных мы будем использовать пакет MetaTrader 5. Запускаем наш скрипт путем импорта библиотек, необходимых для извлечения, преобразования и прогнозирования цен. Обратите внимание, что мы не будем детально рассматривать часть подготовки данных, но не забывайте, что это очень важный шаг для модели.
Начнем с краткого предварительного просмотра данных. Набор данных (dataset) состоит из последних 1000 баров актива EURUSD. Для этого нам понадобятся несколько шагов, например:
- Импорт библиотек
- Подключение к MetaTrader
- Сбор данных
- Преобразование данных, установка даты
- Графические данные
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()
После запуска кода мы визуализируем данные закрытия на графике с линиями, как показано ниже.
Давайте использовать регрессионный подход, в котором будет предсказано значение закрытия следующего периода.
В этом примере мы будем использовать одномерный подход и, как указано выше, не обращая внимания на передовые методы подготовки данных.
Одномерный временной ряд - это набор данных, состоящий из одной серии наблюдений с временным порядком, и ему необходима модель, чтобы учиться на серии прошлых наблюдений, чтобы предсказать следующее значение в последовательности.
Первый шаг - разделить загруженную серию на набор для обучения и тестирования. Для этого создадим функцию, которая разделит такую серию на две части; мы будем использовать процентное значение от общего размера серии для отсечения, например, 70% для обучения и 30% для тестов. Для валидации (backtest) у нас есть другие подходы, такие как разделение серии на обучение, тестирование и валидацию. Поскольку мы говорим о финансовых сериях, нам надо будет особо стараться избегать переобучения.
В нашей функции мы ожидаем получить массив numpy и значение отсечения, и мы вернем две подобранные серии.
Первый возврат - это весь набор от первой позиции 0 до размера, который представляет размер фактора, а второй ряд - все остальное.
def train_test_split(values, fator): train_size = int(len(values) * fator) return values[0:train_size], values[train_size:len(values)]
Модели на Keras можно определить как последовательность слоев.
Мы создаем последовательную модель и добавляем слои - по одному за раз - до тех пор, пока мы не будем довольны нашей сетевой архитектурой.
Первое, что нужно сделать правильно, это убедиться, что входной слой имеет правильное количество входных ресурсов. Это можно указать при создании первого слоя с аргументом input_dim.
Как узнать количество слоев и их типы?
Это очень сложный вопрос. Есть эвристики, которые мы можем использовать, и зачастую лучшая сетевая структура находится в процессе экспериментов методом проб и ошибок. Как правило, требуется достаточно большая сеть, чтобы охватить структуру проблемы.
В этом примере мы будем использовать полносвязную однослойную сетевую структуру.
Полносвязные слои определяются с помощью класса Dense. Мы можем указать количество нейронов или узлов в слое в качестве первого аргумента и указать функцию активации с помощью аргумента активации.
В первом слое, мы будем использовать функцию активации выпрямленного линейного блока, называемого ReLU.
Прежде чем можно будет предсказать одномерный ряд, его необходимо подготовить.
Модель MLP будет обучаться с помощью функции, которая назначает последовательность прошлых наблюдений в качестве входных данных для выходных наблюдений. Таким образом, последовательность наблюдений должна быть преобразована в множество примеров, на которых модель смогла бы учиться.
Рассмотрим одномерную последовательность:
[10, 20, 30, 40, 50, 60, 70, 80, 90]
Мы можем разделить последовательность на несколько шаблонов входа/выхода, называемых выборкой.
В нашем примере мы будем использовать три временных этапа, которые используются в качестве входных данных, и один временной шаг, который используется для изучаемого прогноза.
X, y
10, 20, 30 40
20, 30, 40 50
30, 40, 50 60
...
Далее, мы создаем функцию split_sequence(), которая реализует это поведение и разделит одномерный набор на несколько выборок, каждая из которых имеет определенное количество временных шагов, и выход - это единичный временной шаг.
Мы можем протестировать нашу функцию на небольшом наборе данных, таких как уже придуманные в вышеуказанном примере.
# 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])
При выполнении примера одномерная серия делится на шесть выборок, каждая из которых имеет три временных шага входа и один временной шаг выхода.
[10 20 30] 40
[20 30 40] 50
[30 40 50] 60
[40 50 60] 70
[50 60 70] 80
[60 70 80] 90
Чтобы продолжить, нам нужно разделить выборку на X (feature) и y (target), чтобы мы могли обучить сеть. Для этого мы будем использовать функцию split_sequence(), которую мы создали ранее.
X_train, y_train = split_sequence(train, 3) X_test, y_test = split_sequence(test, 3)
Теперь, когда мы подготовили наши образцы данных, мы можем создать сеть MLP.
Простая модель MLP имеет единственный скрытый слой узлов (нейронов) и выходной слой, который используется для прогнозирования.
Мы можем определить MLP для прогнозирования одномерных временных рядов следующим образом.
# define model model = Sequential() model.add(Dense(100, activation='relu', input_dim=n_steps)) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse')
Важным моментом в определении является форма входных данных, это то, что модель ожидает в качестве входных данных для каждой выборки с точки зрения количества временных шагов.
Количество временных шагов в качестве входных данных - это число, которое мы выбираем при подготовке нашего набора данных как аргумент функции split_sequence().
Размер входных данных для каждой выборки указывается в аргументе input_dim в определении первого скрытого слоя. Технически модель будет показывать каждый временной шаг как отдельный ресурс, а не как отдельные временные шаги.
У нас почти всегда есть несколько образцов, поэтому модель ожидает, что входной обучающий компонент данных будет иметь размеры или форму:
[samples, features]
Функция split_sequence() генерирует X в форме [образцы, характеристики], готовой к использованию.
Модель обучается с использованием эффективного алгоритма под названием Adam для стохастического градиентного спуска и он оптимизируется с использованием функции потерь среднеквадратичной ошибки, или 'mse'.
Как только модель определена, мы можем обучить ее с помощью набора данных.
model.fit(X_train, y_train, epochs=100, verbose=2)
После обучения модели мы можем предсказать будущее значение; модель ожидает, что входная форма будет двумерной с [образцы, характеристики], поэтому нам нужно изменить форму единственной входной выборки до того, как сделать прогноз, например, с помощью формы [1, 3] для 1 выборки и 3 временных шагов, используемых в качестве входных характеристик.
Например, мы выберем последнюю запись тестовой выборки «X_test» и после предсказания сравним ее с реальным значением, содержащимся в последней выборке «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. Как интегрировать MQL5 в Python.
Чтобы использовать модель в торговом счете, у нас есть несколько вариантов, один из них - использовать встроенные функции в Python, которые открывают и закрывают позиции, но для этого сценария у нас не будет многих возможностей, которые предлагает нам MQL; по этой причине я выбрал одну интеграцию между Python и средой MQL, что даст нам больше автономии в управлении нашими позициями/ордерами.
Основываясь на статье MQL5 Соединение MetaTrader 5 и Python: получение и отправка данных, написанной Максимом Дмитриевским, я реализовал этот класс, используя шаблон разработки под названием Singleton, который будет отвечать за создание Socket-клиента для соединения. Этот шаблон гарантирует, что существует только одна копия объекта определенного типа, потому что если программа использует два указателя, относящиеся к одному и тому же объекту, они будут указывать на один и тот же объект.
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); };
Класс CClienteSocke хранит статический указатель как частный член; у класса есть только конструктор, который является частным и не может быть вызван, вместо вызова конструктора можно использовать метод Socket, этим мы гарантируем, что используется только один объект.
static CClientSocket *CClientSocket::Socket(void) { if(CheckPointer(m_socket)==POINTER_INVALID) m_socket=new CClientSocket(); return m_socket; }
Этот метод проверяет, указывает ли статический указатель на объект CClienteSocket, и если это правда, то он возвращает ссылку; в противном случае создается новый объект и связывается с указателем, тем самым гарантируя исключительность этого объекта в нашей системе.
Чтобы установить соединение с сервером, необходимо запустить соединение, поэтому мы создали метод IsConnected, чтобы установить соединение и мы можем начать передачу/получение данных.
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; }
После успешного подключения и успешной передачи сообщений, необходимо закрыть это соединение, для этого мы воспользуемся методом Close, чтобы закрыть ранее открытое соединение.
bool CClientSocket::Close(void) { bool res=false; if(SocketClose(m_handler_socket)) { res=true; m_handler_socket=INVALID_HANDLE; } return res; }
Теперь нам нужно прописать наш сервер, который будет получать новые MQL-соединения и отправлять прогнозы нашей модели.
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()
Наш объект простой, в его конструкторе мы получаем адрес и порт, с которым будем создавать свой сервер. Метод socket_received отвечает за принятие нового соединения и проверку наличия отправленных сообщений; если есть сообщения, которые нужно получить, мы запускаем цикл, пока не получим все части сообщения, затем мы закрываем соединение с клиентом и выходим из цикла. С другой стороны, метод socket_send отвечает за отправку сообщений нашему клиенту, но имейте в виду, что эта предлагаемая модель не только позволяет нам отправлять прогнозы нашей модели, а также открывает возможности для нескольких других вещей - всё будет зависеть от вашей креативности и потребности.
Теперь, когда у нас есть готовая коммуникация, мы должны подумать о двух вещах:
- Обучить модель и сохранить ее.
- Использовать обученную модель, чтобы делать прогнозы.
Я говорю так, потому что было бы непрактично и неправильно получать данные и проводить обучение каждый раз, когда мы хотим сделать прогноз. По этой причине я всегда провожу обучение, нахожу лучшие гиперпараметры и сохраняю свою модель для её дальнейшего использования.
Я создам файл с именем model_train, который будет содержать обучающий код сети, в нем мы будем использовать процентную разницу между ценами закрытия и попытаемся предсказать эту разницу. Обратите внимание, что меня не интересует модель, я хочу продемонстрировать, как использовать модель, интегрируя ее со средой 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)
Теперь, когда у нас есть обученная и сохраненная в нашей папке модель с расширением .h5, мы можем использовать ее для прогнозов, поэтому я создам объект, который создаст экземпляр этой модели для использования в соединении.
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])
Наш объект прост, его конструктор создает экземпляр атрибута, называемого моделью, который будет содержать сохраненную модель, а метод прогнозирования отвечает за осуществление прогнозирования.
Теперь нам нужен основной метод, который будет работать и взаимодействовать с клиентами, получая функции и отправляя прогнозы.
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]))
На стороне нашего MQL-клиента нам нужно создать робота, который будет собирать данные и отправлять их на наш сервер, а затем получать прогнозы. Поскольку наша модель была обучена с данными от закрытых свечей, мы создадим проверку для сбора и отправки этих данных только после закрытия свечи, поэтому у нас будут полные данные для прогнозирования процентной разницы от закрытия только что начавшегося бара, для этого воспользуемся функцией, которая проверяет появление нового бара.
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]); }
Переменная m_last_time объявлена в глобальной области видимости и будет хранить дату и время открытия бара, поэтому мы проводим тест, проверяя, отличается ли переменная time от m_last_time, потому что если это true, это означает, что начал формироваться новый бар. Тут надо заменить значение m_last_time значением time.
Советник не должен открывать позиций, не закрыв предыдущую, поэтому проверяем наличие открытых позиций — для этого используем метод CheckPosition, который задаст значения true или 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; } } }
После появления нового бара мы проверяем наличие открытых позиций, и если есть открытые, то будем ждать закрытия. Если открытой позиции нет, мы запустим процесс соединения, вызвав метод IsConnected класса CClienteSocket.
if(NewBar()) { if(!Socket.IsConnected()) Print("Error : ", GetLastError(), " Line: ", __LINE__); ... }
если возвращается true, это потому, что мы можем установить соединение с нашим сервером, мы соберем данные и отправим их.
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))+"]}"; }
Я решил отправить данные в формате {'rates':[1,2,3,4]}, потому что таким образом мы просто преобразуем их в pandas-датафрейм, и мы не будем тратить время на конверсии.
Как только данные собраны, мы отправляем их и ждем получения прогноза, чтобы мы могли потом принять решение. В этом случае, я буду использовать скользящую среднюю, чтобы проверить направление цены; и в зависимости от стороны и ценности прогноза мы будем покупать или продавать.
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(); } }
Последний шаг - закрыть ранее установленное соединение и дождаться появления нового бара, и таким образом запустить процесс отправки и получения прогноза.
Эта архитектура оказалась очень практичная и с низкой задержкой, поскольку ее можно протестировать. В некоторых личных проектах я решил использовать эту архитектуру в коммерческих учетных записях, потому что она позволяет мне пользоваться всей мощью MQL и всеми ресурсами, доступными для машинного обучения, имеющимся в Python.
Что будет дальше?
В следующей статье я хочу разработать немного более гибкую архитектуру, которая позволяет использовать модели в тестере стратегий, поэтому мы преодолеем единственный барьер, который может помешать нам использовать предложенную архитектуру.
Заключение:
Надеюсь, я дал небольшое руководство о том, как мы можем использовать и разрабатывать различные модели Python и передавать их в среду MQL.
Вы научились:
- Настраивать среду разработки Python.
- Мы вспоминали и реализовали нейрон перцептрона и сеть MLP в Python.
- Мы приготовили одномерные данные для изучения простой сети.
- Мы настраивали архитектуру связи между Python и MQL.
Расширения:
В этом разделе перечислены некоторые идеи, которые, возможно, помогут вам расширить руководство, если у вас есть желание их изучать.
- Размер входных данных. Примерно изучите количество дней, используемых в качестве входных данных для модели, например три дня, 21 день, 30 дней и т.д.
- Настройка модели. Изучите различные структуры и гиперпараметры модели и получите среднюю производительность модели.
- Масштабирование данных. Узнайте, можно ли использовать размер данных, например стандартизацию и нормализацию, для повышения производительности модели.
- Диагностика обучения. Используйте такую диагностику, как кривые обучения для потери обучения и валидации, и среднеквадратичную ошибку, чтобы помочь настроить структурy и гиперпараметры модели.
Если вы изучите какое-либо из этих расширений, я был бы рад узнать об этом.
Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/9514
![Графика в библиотеке DoEasy (Часть 85): Коллекция графических объектов - добавляем вновь создаваемые](https://c.mql5.com/2/43/MQL5-avatar-doeasy-library3-2__4.png)
![Стать хорошим программистом (Часть 4): повышаем скорость программирования](https://c.mql5.com/2/43/speed.png)
![Анализ спреда по ценам Bid и Ask в MetaTrader 5](https://c.mql5.com/2/43/bid-ask-spread.png)
![Стать хорошим программистом (Часть 3): 5 советов, чтобы лучше программировать на MQL5](https://c.mql5.com/2/43/Article_image__1.png)
![MQL5 - Язык торговых стратегий для клиентского терминала MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Artigo sobre Python e MQL e nem uma única linha coda.
Статья обновлена. При переводе примеры были потеряны, теперь они восстановлены.
Хотел уточнить вот эту мысль автора -" использовать встроенные функции в Python, которые открывают и закрывают позиции, но для этого сценария у нас не будет многих возможностей, которые предлагает нам MQL"
О каких возможностях MQL идет речь?
И второй вопрос- Вы работаете под Линуксом?
Хотел уточнить вот эту мысль автора -" использовать встроенные функции в Python, которые открывают и закрывают позиции , но для этого сценария у нас не будет многих возможностей, которые предлагает нам MQL"
О каких возможностях MQL идет речь?
И второй вопрос- Вы работаете под Линуксом?
1 - Например, в Python у нас нет событий Tick и Book, что заставляет нас использовать бесконечный цикл в коде, что меня особенно ужасно. Другие функции, такие как OnTradeTransaction, также недоступны, что заставляет вас прилагать больше усилий, чтобы узнать, была ли позиция полностью или частично заполнена, на нашей фондовой бирже (B3) существует много ситуаций частичного заполнения при выходе.
2- Я не работаю в Linux, использую Windows. Я использую только Linux в своей работе.
Еще, при открытии ордера получаю ошибку:
2023.01.31 20:12:00.305 Demo (EURUSD,M1) CTrade::OrderSend: market buy 0.01 EURUSD sl: -59.99999 tp: 60.00001 [invalid stops]