Практическое применение нейросетей в трейдинге (Часть 2). Компьютерное зрение

Andrey Dibrov | 27 января, 2021

Введение

Существенную проблему подготовки данных при обучении нейронных сетей для их использования в трейдинге представляет собой подготовка необходимой входной информации. Например, если мы будем применять десяток индикаторов. Если эти индикаторы, к тому же, будут представлять собой набор нескольких информативных графиков. Если будем рассчитывать эти индикаторы на определенную глубину, то в результате получим до сотни входов, а в некоторых случаях и более. Можем ли мы упростить обучение нейронной сети, используя компьютерное зрение? Для решения этой задачи воспользуемся сверточными нейронными сетями, которые сейчас применяются для решения задач классификации и распознавания.


Архитектура сверточной нейронной сети

В статье будет использована сверточная нейронная сеть, архитектура которой представлена на рисунке ниже. Впрочем, она характеризует общий принцип построения Convolutional Neural Network (CNN)

В данном варианте мы имеем:

  1. Вход CNN, который представляет изображение размером 449x449 пикселей.
  2. Первый сверточный слой из 96 карт признаков. Каждая карта представляет изображение размером 447x447. Ядро свертки 3x3.
  3. Слой подвыборки из 96 карт признаков размером 223x223 с ядром 2x2.
  4. Второй сверточный слой из 32 карты признаков. Каждая карта представляет изображение размером 221x221. Ядро свертки 3x3.
  5. Слой подвыборки из 32 карт признаков размером 110x110 с ядром 2x2.
  6. Третий сверточный слой из 16 карты признаков. Каждая карта представляет изображение размером 108x108. Ядро свертки 3x3.
  7. Слой подвыборки из 16 карт признаков размером 54x54 с ядром 2x2. На рисунке не показан.
  8. Полносвязный слой из 64 нейронов.
  9. Выходной слой из одного нейрона. Эти два слоя представляют собой блок классификации.

CNN

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

Подготовка массива изображений для обучения и тестирования нейронной сети

Перед подготовкой массива изображений нам необходимо определиться — чего же мы хотим от нашей нейронной сети. В идеале было бы обучить сеть на точках разворота. И исходя из этого посыла выхватить изображения, на которых будет отрисован крайний экстремальный бар. Поскольку, как оказалось, прикладного значения данный эксперимент не имеет, мы остановимся на другом наборе изображений. Хотя, ознакомившись с материалом этой статьи, в дальнейшем вы можете поэкспериментировать и с таким массивом. Это будет даже полезно для того, чтобы удостовериться, в том, что нейросети отлично выполняют задачи классификации на законченных образах. Но отклики сети, полученные, на непрерывном временном ряде, требуют дополнительной оптимизации. 

Наш эксперимент усложнять не будем и остановимся на двух категориях изображений:

Buy   Buy1  Buy2  Buy3

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

Предварительно подготовим график для получения нужного образа. Для этого воспользуемся шаблоном CNN.tpl. Разместите его в папке \AppData\Roaming\MetaQuotes\Terminal\D0E8209F77C8CF37AD8BF550E51FF075\MQL5\Profiles\Templates.

CNN.tpl

В свойствах графика текст определим как "White".

CNN.tpl


Экспериментируя, вы можете накидывать другие индикаторы. Эти индикаторы мною были взяты произвольно. Размер графика выставляйте произвольно, но надо найти оптимальный в зависимости от возможностей "железа".

Воспользуемся скриптом для создания массива изображений.

//+------------------------------------------------------------------+
//|                                                        CNNet.mq5 |
//|                                   Copyright 2021, Andrey Dibrov. |
//|                           https://www.mql5.com/ru/users/tomcat66 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2021, Andrey Dibrov."
#property link      "https://www.mql5.com/ru/users/tomcat66"
#property version   "1.00"
#property strict
#property script_show_inputs

input string Date="2017.01.02 00:00";
input string DateOut="2018.12.13 23:00";
input string DateTest="2019.01.02 00:00";
input string Dataset="Train";

string Date1;
int count,countB,countS;
int day;
double DibMin;
double DibMax;
int HandleDate;
long WIDTH;
long HEIGHT;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   MqlDateTime stm;
   ChartSetInteger(0,CHART_SHIFT,false);
   ChartSetInteger(0,CHART_AUTOSCROLL,false);
   ChartSetInteger(0,CHART_SHOW_OBJECT_DESCR,false);
   WIDTH=ChartGetInteger(0,CHART_WIDTH_IN_PIXELS);
   ChartSetInteger(0,CHART_SHOW_PRICE_SCALE,false);

   if(Dataset=="Test")
     {
      HandleDate=FileOpen(Symbol()+"Date.csv",FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI,";");
      ChartNavigate(0,CHART_END,-(iBarShift(NULL,PERIOD_H1,StringToTime(DateTest))));
      Sleep(1000);

      for(int i=iBarShift(NULL,PERIOD_H1,StringToTime(DateTest)); i>0; i--)
        {
         Date1=TimeToString(iTime(NULL,PERIOD_H1,i));
         if(DateTest<=Date1)
           {
            if(ChartNavigate(0,CHART_END,-i))
              {
               Sleep(20);
               if(ChartScreenShot(0, (string)count + ".png", (int)WIDTH, (int)WIDTH, ALIGN_LEFT))
                 {
                  FileWrite(HandleDate,TimeToString(iTime(NULL,PERIOD_H1,i)));
                  count++;
                  Sleep(20);
                 }
              }
           }
        }
     }
   if(Dataset=="Train")
     {
      ChartNavigate(0,CHART_END,-iBarShift(NULL,PERIOD_H1,StringToTime(Date)));
      Sleep(1000);
      for(int i=iBarShift(NULL,PERIOD_H1,StringToTime(Date)); i>=iBarShift(NULL,PERIOD_H1,StringToTime(DateOut)); i--)
        {
         TimeToStruct(iTime(NULL,PERIOD_H1,i),stm);
         Date1=TimeToString(iTime(NULL,PERIOD_H1,i));
         if(DateOut>=Date1 && Date<=Date1)
           {
            if(ChartNavigate(0,CHART_END,-i))
              {
               Sleep(20);
               if(day != stm.day)
                 {
                  FileCopy("Sell" + (string)countS + ".png", 0, "Buy" + (string)(countB+1) + ".png", FILE_REWRITE);
                  FileDelete("Sell" + (string)countS + ".png", 0);
                  FileCopy("Buy" + (string)countB + ".png", 0, "Sell" + (string)(countS+1) + ".png", FILE_REWRITE);
                  FileDelete("Buy" + (string)countB + ".png", 0);
                  countB ++;
                  countS ++;
                 }
               day = stm.day;
               if(stm.hour == 0)
                 {
                  DibMin = iOpen(NULL, PERIOD_H1, i);
                  DibMax = iOpen(NULL, PERIOD_H1, i);
                 }
               if(iLow(NULL, PERIOD_H1, i+1) < DibMin)
                 {
                  DibMin = iLow(NULL, PERIOD_H1, i+1);
                  countS ++;
                  ChartScreenShot(0, "Sell" + (string)countS + ".png", (int)WIDTH, (int)WIDTH, ALIGN_LEFT);
                 }
               if(iHigh(NULL, PERIOD_H1, i+1) > DibMax)
                 {
                  DibMax = iHigh(NULL, PERIOD_H1, i+1);
                  countB ++;
                  ChartScreenShot(0, "Buy"  +(string)countB + ".png", (int)WIDTH, (int)WIDTH, ALIGN_LEFT);
                 }
               Sleep(20);
              }
           }
         else
            break;
        }
     }
  }

Скрипт работает в двух режимах "Train" — создаем массив картинок для обучения, и "Test" — создаем массив изображений для получения откликов NN, на основе которых будет получен индикатор. Этот индикатор в последующем будем использовать для оптимизации стратегии торговли. 

Запустим скрипт в режиме "Train".

Train

Переменная "Date" — начальная дата выборки изображений для обучения. "DateOut' — конечная дата выборки изображений для обучения. "DateTest" — начальная дата выборки изображения для получения почасовых откликов нейронной сети. Конечной датой откликов будет час запуска скрипта. 

В папке ...\MQL5\Files каталога данных мы получим набор изображений Buy... и Sell.... Всего 6125.

Tren

Далее мы подготовим каталоги для обучающего множества, валидационного и тестового. Для удобства создадим на "Рабочем столе" папку "CNN" и в ней три папки "Train", "Val", "Test".

CNN

В каталогах "Train" и "Val" создадим подкаталоги "Buy" и "Sell". В каталоге "Test" — подкаталог "Resp".

Train

Из папки ...\MQL5\Files вырежем все файлы "Buy..." и вставим в папку ...\Train\Buy. Получим 3139 изображений. Такую же процедуру проведем с файлами "Sell..." и поместим их в каталог ...\Train\Sell. У нас получилось 2986 изображений. Из папки "Buy" и "Sell" вырежем по 30% последних (с наибольшими номерами) картинок и разместим их в соответствующих папках из каталога "Val".

Получилось 

Таким образом мы подготовили набор изображений для обучения нашей сети. Изображения в данном случае у меня получились размером 449х449 пикселей.

Подготовим набор изображений для тестирования. Запустим скрипт в режиме "Test".

Test

В папке ...\MQL5\Files каталога данных мы получим набор последовательных почасовых снимков. На данный момент — 12558. Мы их никак не разделяем, поскольку нейросеть должна сама проводить это разделение. Вернее, давать нам рекомендацию с какой вероятностью тот или иной снимок соответствует тем условиям на которых мы обучали сеть. Движение вверх и разворот внизу. Движение вниз и разворот вверху. 

Вырежем эти файлы и вставим в папку ...CNN\Test\Resp.

Test

Итак, набор изображений для дальнейшего тестирования откликов и оптимизации стратегии мы подготовили. Оставшийся в папке ...\MQL5\Files файл с датами и временем EURUSDDate перенесем в папку CNN.

В заключение хочется отметить одну особенности MetaTrader 5. Подготавливать набор изображений было бы удобнее и надежнее с помощью эксперта в тестере стратегий, однако, почему-то MetaTrader 5 в тестере стратегий графики не "скриншотит". Но в любом случае при реализации торгового робота эта особенность нам не помешает.


Обучение нейронной сети

Для работы со сверточными нейронными сетями мы воспользуемся средой Anaconda. Данную среду нам необходимо настроить для работы с CPU и GPU (если у вас есть видеокарта NVIDIA). Данная видеокарта нам необходима, чтобы ускорить процесс обучения. Хотя она налагает и некоторые ограничения при создании архитектуры NN. Это зависимость от объема оперативной памяти видеокарты. Но в скорости обучения мы выигрываем значительно. Например, в моем случае, если на CPU одна эпоха обучения длится 20 минут, то на GPU 1-2 минуты. Если надо обучать NN на 40 эпохах, то получится 13 часов и 1,5. А если все это на стадии исследований, то понятно, что использование GPU значительно ускоряет процесс поиска NN.

  1. Загрузим и установим последнюю версию Anaconda Navigator. Все делаем по умолчанию.
  2. Из меню "Пуск\Anaconda3" запустим "Anaconda Promt".
  3. Введем команду "pip install tensorflow". Установим программную библиотеку для машинного обучения, разработанную компанией Google.
    tensorflow

  4. Введем команду "pip install keras". Установим нейросетевую библиотеку Keras.
    keras

  5. Создадим новую среду conda под GPU.  Наберем команду conda create --name PythonGPU. И активируем среду - activate PythonGPU.
     
    GPU

  6. Для установки tensorflow gpu наберем команду - conda create -n PythonGPU python=3.6 tensorflow-gpu. Хочу обратить внимание, что tensorflow gpu необходимо устанавливать для python 3.6.
      tensorflow gpu

  7. Для установки keras gpu наберем команду - conda install -c anaconda keras-gpu.
    keras gpu

  8. Установим интерфейс Jupyter для программирования в среде Python GPU. Для CPU Jupyter у нас был установлен при установке Anaconda. Наберем команду - conda install jupyterJupyter

  9. Установим еще две библиотеки Pandas и Pillow - conda install -c anaconda pandas. И потом - conda install pillow. Эти библиотеки также необходимо установить для CPU если вы не будете использовать видеокарту.
    pandas 

  10. pillow

  11. Теперь мы можем приступить к обучению нашей сверточной нейронной сети. В ранее созданную папку CNN поместим два файла "Train.ipynb" и "Test.ipynb". Это файлы в формате Jupyter Notebook, с которыми мы и будем работать в дальнейшем. Запустим Jupyter Notebook (PythonGPU) и откроем файл Train.  CNN  

Разберем поблочно программный код.

Загружаем необходимые модули нейросетевых библиотек.

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Conv2D, MaxPooling2D
from tensorflow.python.keras.layers import Activation, Dropout, Flatten, Dense
from tensorflow.keras.callbacks import ModelCheckpoint
from tensorflow.keras.models import load_model

Задаем необходимые гиперпараметры.

# Каталог с данными для обучения
train_dir = 'train'
# Каталог с данными для проверки
val_dir = 'val'
# Размеры изображения
img_width, img_height = 449, 449
# Размерность тензора на основе изображения для входных данных в нейронную сеть
# backend Tensorflow, channels_last
input_shape = (img_width, img_height, 3)
# Количество эпох
epochs = 20
# Размер мини-выборки
batch_size = 7
# Количество изображений для обучения
nb_train_samples = 4289
# Количество изображений для проверки
nb_validation_samples = 1836
# Количество изображений для тестирования
#nb_test_samples = 3736

Создаем архитектуру сети.

  • Задаем последовательную архитектуру CNN (convolutional neural network)
  • Входное изображение 449х449 пикселей, трех канальное (красный, зеленый и синий). Мы используем цветное изображение. Можно будет поэкспериментировать с двухцветным
  • Создаем первый слой свертки для работы с двухмерными данными - 96 карт признаков каждая с собственным ядром свертки размером 3х3. Каждый нейрон сверточного слоя подключен к квадратному участку изображения размером 3х3 
  • Слой активации, использующий функцию "relu" - поскольку она менее требовательна к вычислительным ресурсам
  • Для уменьшения размерности добавляем слой подвыборки с ядром 2х2 с выбором максимального значения из этого квадрата
  • Далее добавляем еще два слоя свертки с 32 и 16 ядрами и по два слоя активации и подвыборки
  • Преобразуем данные из двумерного формата в одномерный
  • Передаем преобразованные данные на полносвязный слой, у которого 64 нейрона
  • С помощью функции слоя регуляризации Dropout(0.5) пытаемся избежать переобучения
  • Добавляем полносвязный выходной слой с одним нейроном. Поскольку мы используем два класса изображений и будем получать отклик от NN в бинарном виде. Мы можем экспериментировать и с несколькими классами. Допустим - два тренда и флэт. Тогда бы в выходном слое было бы три нейрона. И естественно изображения нам надо было бы разделить на три класса.
  • Функция активации "sigmoid". Подходит для классификации и лучше других зарекомендовала в наших экспериментах

Этот пример архитектуры мы можем легко модернизировать — увеличивать количество слоев, их размерность, менять местами в зависимости от последовательности размерности, размерность ядер свертки, функции активации, поработать с полно связными слоями. Однако здесь возникает дилемма — при использовании GPU необходимо увеличивать оперативную память видеокарты — если мы хотим наращивать архитектуру нейросети. Либо уменьшать размер изображений. Либо жертвовать своим временем и использовать CPU.

model = Sequential()
model.add(Conv2D(96, (3, 3), input_shape=input_shape))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(16, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))

Компилируем сеть. Используем функцию ошибки binary-crossentropy. В данном варианте мы будем использовать отклик, состоящий из двух классов, которые должен принимать значения 0 или 1 в идеале. Реально же наши значения будут распределяться от 0 до 1. Оптимизатор выбираем градиентного спуска. В данном случае исходя из экспериментов, он наиболее подходит для обучения NN.  Выбираем метрику аккуратность (accuracy) — процентное соотношение правильных ответов. 

model.compile(loss='binary_crossentropy',
              optimizer='sgd',
              metrics=['accuracy'])

Нормализуем данные о интенсивности пикселов изображений.

datagen = ImageDataGenerator(rescale=1. / 255)

Используем генераторы Keras для чтения данных с диска и создания обучающего и валидационного массивов для NN из наших изображений. Опять-таки, class_mode выбираем binary. Shuffle выбираем False. Перемешивать наши изображения не будем.

train_generator = datagen.flow_from_directory(
    train_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary',
    shuffle=False)
val_generator = datagen.flow_from_directory(
    val_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary',
    shuffle=False)

Функция callbacks сохраняет обученную NN после каждой эпохи. Это дает нам возможность выбрать наиболее подходящую сеть по значениям ошибки и процентам "попадания".

callbacks = [ModelCheckpoint('cnn_Open{epoch:1d}.hdf5')]

Приступим непосредственно к обучению сети.

model.fit(
    train_generator,
    steps_per_epoch=nb_train_samples // batch_size,
    epochs=epochs,
    validation_data=val_generator,
    validation_steps=nb_validation_samples // batch_size,
    callbacks=callbacks)

12. Запустим программу на выполнение, как показано на рисунке.

Run


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

Fit

После окончания процесса обучения в папке CNN будет находится 20 обученных NN.

CNN

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


NN

На первый взгляд на 18 й эпохе нейросеть обучилась с результатом ошибки 30% и 85% верных результатов. Однако при проверке NN на валидационном множестве мы видим, что ошибка растет, а процент положительных ответов падает. Значит, нам необходимо выбрать нейросеть обученную на 11 й эпохе. Как мы видим, здесь наиболее подходящие результаты проверки при валидации val_loss: 0.6607 и val_accuracy: 0.6129. В идеале, конечно, необходимо стремиться к получению значений ошибки, стремящейся к 0 (или хотя бы ниже примерно 35-40%), а аккуратность стремящейся к 1 (или хотя бы выше примерно 55-60%). Тогда оптимизацию можно было бы и не делать, либо проводить по минимальным параметрам для повышения качества торгов. Но и на данных результатах обучения можно создать положительную торговую систему.


Интерпретация отклика нейронной сети

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

Запустим Jupyter Notebook без поддержки GPU и откроем файл Test.jpynb из каталога CNN.

Test

Рассмотрим поблочно.

Загружаем необходимые модули нейросетевых библиотек.

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Conv2D, MaxPooling2D
from tensorflow.python.keras.layers import Activation, Dropout, Flatten, Dense
from tensorflow.keras.callbacks import ModelCheckpoint
from tensorflow.keras.models import load_model
import pandas as pd

Задаем необходимые параметры.

predict_dir = 'Test'
img_width, img_height = 449, 449
nb_predict_samples = 12558

Считываем из файла дату и время тестируемых изображений.

Date=pd.read_csv('EURUSDDate.csv', delimiter=';',header=None)

Загружаем сохраненную после 11 й эпохи NN.

model=load_model('cnn_Open11.hdf5')

Нормализуем изображения.

datagen = ImageDataGenerator(rescale=1. / 255)

С помощью генератора читаем данные с диска.

predict_generator = datagen.flow_from_directory(
    predict_dir,
    target_size=(img_width, img_height),
    shuffle=False)

Получаем отклики от NN.

indicator=model.predict(predict_generator, nb_predict_samples )

Покажем результат. Процесс получения откликов длительный и это нас оповестит, что он окончен.

print(indicator)

Полученный результат сохраним в файл.

Date=pd.DataFrame(Date)
Date['0'] =indicator
Date.to_csv('Indicator.csv',index=False, header=False,sep=';')

Запустим программу и после окончания ее работы в папке CNN получим файл Indicator.csv.

Indicator

И перенесем его в папку C:\Users\...\AppData\Roaming\MetaQuotes\Terminal\Common\Files.

На график EURUSD H1 поместим индикатор NWI.

NWI

 

//+------------------------------------------------------------------+
//|                                                          NWI.mq5 |
//|                                 Copyright © 2019, Andrey Dibrov. |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2019, Andrey Dibrov."
#property link      "https://www.mql5.com/ru/users/tomcat66"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
#property indicator_type1   DRAW_LINE
#property indicator_type2   DRAW_LINE
#property indicator_color1  Red
#property indicator_color2  DodgerBlue


int Handle;
int i;
int h;
input int Период=5;
double    ExtBuffer[];
double    SignBuffer[];
datetime Date1;
datetime Date0;
string File_Name="Indicator.csv";

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
void OnInit()
  {
   SetIndexBuffer(0,ExtBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,SignBuffer,INDICATOR_DATA);
   IndicatorSetInteger(INDICATOR_DIGITS,5);
   Handle=FileOpen(File_Name,FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";");
   //FileClose(Handle);
  }
//+------------------------------------------------------------------+
//| Relative Strength Index                                          |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   MqlDateTime stm;
   Date0=StringToTime(FileReadString(Handle));
   i=iBarShift(NULL,PERIOD_H1,Date0,false);
   Handle=FileOpen(File_Name,FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";");
   ArraySetAsSeries(ExtBuffer,true);
   ArraySetAsSeries(SignBuffer,true);
   while(!FileIsEnding(Handle) && !IsStopped())
     {
      Date1=StringToTime(FileReadString(Handle));
      ExtBuffer[i]=StringToDouble(FileReadString(Handle));
      h=Период-1;
      if(i>=0)
        {
         while(h>=0)
           {
            SignBuffer[i]=SignBuffer[i]+ExtBuffer[i+h];
            h--;
           }
        }
      SignBuffer[i]=SignBuffer[i]/Период;
      TimeToStruct(Date1,stm);
      i--;
     }
   FileClose(Handle);
   return(rates_total);
  }
//+------------------------------------------------------------------+

Для упрощения интерпретировать отклики от CNN будем с помощью пересечения основной лини индикатора простой средней. Воспользуемся советником TestCNN.

//+------------------------------------------------------------------+
//|                                                      TestCNN.mq5 |
//|                                 Copyright © 2019, Andrey Dibrov. |
//+------------------------------------------------------------------+
#property copyright " Copyright © 2019, Andrey Dibrov."
#property link      "https://www.mql5.com/ru/users/tomcat66"
#property version   "1.00"
#property strict

#include<Trade\Trade.mqh>

CTrade  trade;

input int Период=5;
input int H1;
input int H2;
input int H3;
input int H4;
input int LossBuy;
input int ProfitBuy;
input int LossSell;
input int ProfitSell;

ulong TicketBuy1;
ulong TicketSell0;

datetime Count;

double Per;
double Buf_0[];
double Buf_1[];
bool send1;
bool send0;

int h=4;
int k;
int K;
int bars;
int Handle;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---

   Handle=FileOpen("Indicator.csv",FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";");

   while(!FileIsEnding(Handle)&& !IsStopped())
     {
      StringToTime(FileReadString(Handle));
      bars++;
     }
   FileClose(Handle);
   ArrayResize(Buf_0,bars);
   ArrayResize(Buf_1,bars);
   Handle=FileOpen("Indicator.csv",FILE_CSV|FILE_SHARE_READ|FILE_ANSI|FILE_COMMON,";");

   while(!FileIsEnding(Handle)&& !IsStopped())
     {
      Count=StringToTime(FileReadString(Handle));
      Buf_0[k]=StringToDouble(FileReadString(Handle));
      h=Период-1;
      if(k>=h)
        {
         while(h>=0)
           {
            Buf_1[k]=Buf_1[k]+Buf_0[k-h];
            h--;
           }
         Buf_1[k]=Buf_1[k]/Период;
        }
      k++;
     }
   FileClose(Handle);

   int deviation=10;
   trade.SetDeviationInPoints(deviation);
   trade.SetTypeFilling(ORDER_FILLING_RETURN);
   trade.SetAsyncMode(true);

//---
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   MqlDateTime stm;
   TimeToStruct(TimeCurrent(),stm);

   int    digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
   double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
   double PriceAsk=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double PriceBid=SymbolInfoDouble(_Symbol,SYMBOL_BID);

   double SL1=NormalizeDouble(PriceBid-LossBuy*point,digits);
   double TP1=NormalizeDouble(PriceAsk+ProfitBuy*point,digits);
   double SL0=NormalizeDouble(PriceAsk+LossSell*point,digits);
   double TP0=NormalizeDouble(PriceBid-ProfitSell*point,digits);

   if(LossBuy==0)
      SL1=0;

   if(ProfitBuy==0)
      TP1=0;

   if(LossSell==0)
      SL0=0;

   if(ProfitSell==0)
      TP0=0;

//---------Buy1
   if(send1==false && K>0 && Buf_0[K-1]>Buf_1[K-1] && Buf_0[K]<Buf_1[K] && iLow(NULL,PERIOD_H1,1)<iLow(NULL,PERIOD_H1,2) && stm.hour>H1 && stm.hour<H2 && H1<H2)
     {
      send1=trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,1,PriceAsk,SL1,TP1);
      TicketBuy1 = trade.ResultDeal();
     }

   if(send1==true && K>0 && Buf_0[K-1]<Buf_1[K-1] && Buf_0[K]>Buf_1[K] && iHigh(NULL,PERIOD_H1,1)>iHigh(NULL,PERIOD_H1,2))
     {
      trade.PositionClose(TicketBuy1);
      send1=false;
     }

//---------Sell0

   if(send0==false && K>0 && Buf_0[K-1]<Buf_1[K-1] && Buf_0[K]>Buf_1[K] && iHigh(NULL,PERIOD_H1,1)>iHigh(NULL,PERIOD_H1,2) && stm.hour>H3 && stm.hour<H4 && H3<H4)
     {
      send0=trade.PositionOpen(_Symbol,ORDER_TYPE_SELL,1,PriceBid,SL0,TP0);
      TicketSell0 = trade.ResultDeal();
     }

   if(send0==true && K>0 && Buf_0[K-1]>Buf_1[K-1] && Buf_0[K]<Buf_1[K] && iLow(NULL,PERIOD_H1,1)<iLow(NULL,PERIOD_H1,2))
     {
      trade.PositionClose(TicketSell0);
      send0=false;
     }
   K++;
  }
//+------------------------------------------------------------------+


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


Optim

Optim1

Нейронная сеть обучалась на временном периоде, находящемся до линии графика, и оптимизация проводилась до вертикальной красной линии. После — тест по оптимизированным откликам от NN. На графиках выше показаны два случайных положительных результата оптимизации, которые оптимизатор показал как результат с наибольшим приоритетом.


Визуализация слоев нейронной сети и повышение качества CNN

Нейронная сеть представляется как некий черный ящик. Однако это не совсем так, поскольку мы можем посмотреть, какие признаки CNN выделяет в картах признаков в слоях. И это дает нам еще информацию для аналитики и дальнейшего повышения качества CNN. Давайте посмотрим.

Запустим из папки CNN программу Visual.ipynb.

Загружаем необходимые модули нейросетевых библиотек.

from tensorflow.python.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.models import Model, load_model
import pandas as pd
from tensorflow.python.keras.preprocessing import image 
import matplotlib.pyplot as plt
import numpy as np

Загрузим сохраненную модель.

model=load_model('cnn_Open11.hdf5')

Посмотрим на архитектуру сети.

model.summary()

Conv2d

Нас интересует, что же изучают вот эти сверточные слои.

Загрузим какое-либо изображение.

img_path='Train/Buy/Buy81.png'
img=image.load_img(img_path,target_size=(449,449))
plt.figure(figsize=(8, 8))
plt.imshow(img)
plt.show

Buy81

Преобразуем изображение в массив numpy и нормализуем.

x=image.img_to_array(img)
x=np.expand_dims(x,axis=0)
x/=255

Обрежем модель на каком-либо сверточном слое. Номера сверточных слоев 0,3,6. Начнем с нулевого. По сути, мы создаем новую, но уже обученную, модель и будем получать от нее промежуточный результат, до классификации.

model=Model(inputs=model.input, outputs=model.layers[0].output)

После мы можем посмотреть на 3 слой и 6 й.

#model=Model(inputs=model.input, outputs=model.layers[3].output)
#model=Model(inputs=model.input, outputs=model.layers[6].output)

Выведем информацию о нашей урезанной модели.

model.summary()

Conv2d-0

То есть посмотрим на первый сверточный слой с номером 0.

Получим отклик от нейросети.

model=model.predict(x)

И распечатаем одну из карт признаков под номером 18.

print(model.shape)
im=model[0,:,:,18]
plt.figure(figsize=(10, 10))
plt.imshow(im)
plt.show()


PLT

Как мы видим в этой карте нейросеть выделила бычьи свечи. И сразу бросается в глаза, что на данном этапе CNN сложно разделять медвежьи свечи и точки Параболика. Поскольку они заданы нами одного цвета. Теперь мы можем сделать вывод, что все отдельные элементы на нашем графике следует подавать в разных цветах. 

Посмотрим на все карты признаков

rows=12
filters=model.shape[-1]
size=model.shape[1]
cols=filters//rows
display_grid=np.zeros((cols*size,rows*size))
for col in range(cols):
    for row in range(rows):
        channel_image=model[0,:,:,col*rows+row]
        channel_image-=channel_image.mean()
        channel_image/=channel_image.std()
        channel_image*=64
        channel_image+=128
        channel_image=np.clip(channel_image,0,255).astype('uint8')
        display_grid[col*size:(col+1)*size,row*size:(row+1)*size]=channel_image
scale=1./size
plt.figure(figsize=(scale*display_grid.shape[1],scale*display_grid.shape[1]))
plt.grid(False)
plt.imshow(display_grid,aspect='auto',cmap='viridis')

Shape

Давайте посмотрим на третью карту признаков под номером 2.

Shape2

На этой карте CNN выделяет все свечи, но с разной тенью. И за счет этого картинка становится как бы объемной. Однако мы снова обращаем внимание на то, что тени у медвежьих свечей и у Параболика одного цвета.

Давайте посмотрим на карту номер 5 следующего сверточного слоя, который у нас имеет номер, как мы помним — 3.


Shape5

Здесь точки Параболика накладываются друг на друга и CNN выделяет их как признак флэтовой модели. И если мы посмотрим на предыдущий рисунок, то увидим, что нейросеть этот участок так же отрисовала иначе. То есть можно сделать вывод о расширении категорий изображений для обучения. Для обучения нейросети необходимо ввести еще одну категорию — флэтовую.

Таким образом, визуальное изучение карт признаков CNN позволит нам более четко ставить задачи на обучение. А также позволит расширить категории признаков, выделяемых CNN, и тем самым уменьшить шумовые эффекты.

Заключение

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

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