English 中文 Español Deutsch 日本語 Português
preview
Многослойный перцептрон и алгоритм обратного распространения ошибки (Часть II): Реализация на Python и интеграция с MQL5

Многослойный перцептрон и алгоритм обратного распространения ошибки (Часть II): Реализация на Python и интеграция с MQL5

MetaTrader 5Примеры | 15 октября 2021, 08:41
5 154 9
Jonathan Pereira
Jonathan Pereira

Введение

В предыдущей статье мы увидели, как создать простой нейрон (перцептрон), и изучили, как выполняются вычисления градиентного спуска, как строится сеть (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 части:

  1. Установка и подготовка среды Python в MetaEditor.
  2. Первые шаги и реконструкция модели (перцептрон и MLP).
  3. Создание простой модели с использованием Keras и TensorFlow.
  4. Как интегрировать 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.

1 - Настройка компиляторов

В моем случае я использую полностью отдельную библиотечную среду, называемую виртуальной средой, это способ получить «чистую» установку и собрать только библиотеки, необходимые для проекта.

Для получения дополнительной информации о пакете venv перейдите по ссылке.


Как только это будет готово, мы сможем запускать скрипты Python прямо из терминала. Для нашего эксперимента нам потребуется установить библиотеки:

Если у вас есть вопросы о том, как установить библиотеки, смотрите руководство по установке модуля.

  • MetaTrader 5
  • TensorFlow
  • Matplotlib
  • Pandas
  • Sklearn

Теперь, когда мы установили и настроили среду, давайте проведем небольшой тест, чтобы понять, как создать и запустить скрипт в нашем терминале. Чтобы запустить новый скрипт прямо из MetaEditor, мы можем выполнить следующие шаги:

Новый > Python script

1 - Новый скрипт

Определите имя для вашего скрипта, мастер создания MetaEditor подсказывает нам автоматически импортировать некоторые библиотеки, это очень интересно и для нашего эксперимента мы выберем вариант numpy.

3 - Новый скрипт II

Теперь давайте создадим простой скрипт, который генерирует синусоидальный график.

# 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, в нашем случае откроется окно с графиком функции, которую мы создали выше.

3 - Синусоидальный график


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()
    

    После запуска кода мы визуализируем данные закрытия на графике с линиями, как показано ниже.

    plot_1


    Давайте использовать регрессионный подход, в котором будет предсказано значение закрытия следующего периода.

    В этом примере мы будем использовать одномерный подход и, как указано выше, не обращая внимания на передовые методы подготовки данных.

    Одномерный временной ряд - это набор данных, состоящий из одной серии наблюдений с временным порядком, и ему необходима модель, чтобы учиться на серии прошлых наблюдений, чтобы предсказать следующее значение в последовательности.

    Первый шаг - разделить загруженную серию на набор для обучения и тестирования. Для этого создадим функцию, которая разделит такую ​​серию на две части; мы будем использовать процентное значение от общего размера серии для отсечения, например, 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 отвечает за отправку сообщений нашему клиенту, но имейте в виду, что эта предлагаемая модель не только позволяет нам отправлять прогнозы нашей модели, а также открывает возможности для нескольких других вещей - всё будет зависеть от вашей креативности и потребности.

    Теперь, когда у нас есть готовая коммуникация, мы должны подумать о двух вещах:

    1. Обучить модель и сохранить ее.
    2. Использовать обученную модель, чтобы делать прогнозы.

    Я говорю так, потому что было бы непрактично и неправильно получать данные и проводить обучение каждый раз, когда мы хотим сделать прогноз. По этой причине я всегда провожу обучение, нахожу лучшие гиперпараметры и сохраняю свою модель для её дальнейшего использования.

    Я создам файл с именем 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.

    Вы научились:

    1. Настраивать среду разработки Python.
    2. Мы вспоминали и реализовали нейрон перцептрона и сеть MLP в Python.
    3. Мы приготовили одномерные данные для изучения простой сети. 
    4. Мы настраивали архитектуру связи между Python и MQL.


    Расширения:

    В этом разделе перечислены некоторые идеи, которые, возможно, помогут вам расширить руководство, если у вас есть желание их изучать.

    • Размер входных данных. Примерно изучите количество дней, используемых в качестве входных данных для модели, например три дня, 21 день, 30 дней и т.д.
    • Настройка модели. Изучите различные структуры и гиперпараметры модели и получите среднюю производительность модели.
    • Масштабирование данных. Узнайте, можно ли использовать размер данных, например стандартизацию и нормализацию, для повышения производительности модели.
    • Диагностика обучения. Используйте такую диагностику, как кривые обучения для потери обучения и валидации, и среднеквадратичную ошибку, чтобы помочь настроить структурy и гиперпараметры модели.

    Если вы изучите какое-либо из этих расширений, я был бы рад узнать об этом.


    Перевод с португальского произведен MetaQuotes Ltd.
    Оригинальная статья: https://www.mql5.com/pt/articles/9514

    Прикрепленные файлы |
    MQL5.zip (179.04 KB)
    Последние комментарии | Перейти к обсуждению на форуме трейдеров (9)
    Rashid Umarov
    Rashid Umarov | 26 окт. 2021 в 10:57
    Aliaksandr Hryshyn #:
    Artigo sobre Python e MQL e nem uma única linha coda.

    Статья обновлена. При переводе примеры были потеряны, теперь они восстановлены.

    Vladimir Perervenko
    Vladimir Perervenko | 8 нояб. 2021 в 10:28

    Хотел уточнить вот эту мысль автора -" использовать встроенные функции в Python, которые открывают и закрывают позиции, но для этого сценария у нас не будет многих возможностей, которые предлагает нам MQL"

    О каких возможностях MQL идет речь?

    И второй вопрос- Вы работаете под Линуксом?

    Jonathan Pereira
    Jonathan Pereira | 11 нояб. 2021 в 19:34
    Vladimir Perervenko # :

    Хотел уточнить вот эту мысль автора -" использовать встроенные функции в Python, которые открывают и закрывают позиции , но для этого сценария у нас не будет многих возможностей, которые предлагает нам MQL"

    О каких возможностях MQL идет речь?

    И второй вопрос- Вы работаете под Линуксом?

    1 - Например, в Python у нас нет событий Tick и Book, что заставляет нас использовать бесконечный цикл в коде, что меня особенно ужасно. Другие функции, такие как OnTradeTransaction, также недоступны, что заставляет вас прилагать больше усилий, чтобы узнать, была ли позиция полностью или частично заполнена, на нашей фондовой бирже (B3) существует много ситуаций частичного заполнения при выходе.

    2- Я не работаю в Linux, использую Windows. Я использую только Linux в своей работе.

    Bulanov_1
    Bulanov_1 | 31 янв. 2023 в 18:19
    Прекрасная статья! Код шикарен, я не сильно программист) Но все завелось сразу из распакованного архива) Никаких танцев с бубном) при том, что мой уровень в программировании  - "чайник") Вы хотели продолжить данную тематику в следующе статье, будет ли она?)
    Bulanov_1
    Bulanov_1 | 31 янв. 2023 в 19:10

    Еще, при открытии ордера получаю ошибку:

    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]

    Как возможно ее исправить? Спасибо!
    Графика в библиотеке DoEasy (Часть 85): Коллекция графических объектов - добавляем вновь создаваемые Графика в библиотеке DoEasy (Часть 85): Коллекция графических объектов - добавляем вновь создаваемые
    В статье закончим создание классов-наследников класса абстрактного графического объекта и начнём реализацию хранения этих объектов в классе-коллекции. В частности — создадим функционал для добавления в класс-коллекцию вновь создаваемых стандартных графических объектов.
    Стать хорошим программистом (Часть 4): повышаем скорость программирования Стать хорошим программистом (Часть 4): повышаем скорость программирования
    Я полагаю, каждый разработчик хочет писать код быстрее. При этом возможность быстро и эффективно писать код — это не какая-то особая врожденная способность, доступная только избранным. Это навык, которым может овладеть любой программист, независимо от предыдущего опыта и объема набранных на клавиатуре текстов.
    Анализ спреда по ценам Bid и Ask в MetaTrader 5 Анализ спреда по ценам Bid и Ask в MetaTrader 5
    В статье рассказываю об инструменте, который позволит увидеть уровни спреда, т.е. разницу между ценами бид и аск у вашего брокера. Тиковые данные в MetaTrader 5 позволяют проанализировать, какие же исторические значения спредов по ценам бид и аск были на самом деле. При этом не нужно искать текущее значение спреда, потому что его можно получить через отображение линий цен бид и аск.
    Стать хорошим программистом (Часть 3): 5 советов, чтобы лучше программировать на MQL5 Стать хорошим программистом (Часть 3): 5 советов, чтобы лучше программировать на MQL5
    Статья обязательна к прочтению для всех, кто хочет улучшить свою карьеру программиста. Цель этой серии статей — помочь любому читателю, даже опытному, улучшить навыки программирования. Описанные в статье идеи работают как для начинающих MQL5-программистов, так и для профессионалов.