
Советник на базе универсального аппроксиматора MLP
Содержание
- Введение
- Погружение в проблематику обучения
- Универсальный аппроксиматор
- Реализация MLP в составе торгового советника
Введение
Когда речь заходит о нейронных сетях, многие представляют себе сложные алгоритмы и громоздкие технические детали. По своей сути, нейронная сеть представляет собой композицию функций, где каждый слой состоит из комбинации линейного преобразования и нелинейной функции активации. Если представить это в виде формулы, мы можем записать это как:
F(x) = f2(f1(x))
где f1 — функция первого слоя, а f2 — функция второго слоя.
Многие считают нейросети чем-то невообразимо сложным и трудным для понимания, однако, я хочу объяснить их простыми словами, чтобы каждый мог увидеть их с другой стороны. Существует множество архитектур нейронных сетей, каждая из которых предназначена для выполнения определенных задач. В этой статье мы сосредоточимся на простейшем многослойном персептроне (MLP), который выполняет преобразование входящей информации через нелинейные функции. Зная архитектуру сети, мы можем записать её в аналитическом виде, где каждая функция активации в нейронах служит нелинейным преобразователем.
Каждый слой сети содержит группу нейронов, которые обрабатывают информацию, проходящую через множество нелинейных преобразований. Многослойный персептрон способен выполнять такие задачи, как аппроксимация, классификация и экстраполяция. Общая формула, описывающая работу персептрона, регулируется с помощью весов, что позволяет адаптировать его к различным задачам.
Интересно, что мы можем интегрировать этот аппроксиматор в любую торговую систему. Если рассматривать нейронную сеть без упоминания оптимизаторов, таких как SGD или ADAM, MLP можно использовать как преобразователь информации. Например, он может анализировать состояния рынка — будь то флет, тренд или переходное состояние — и на основе этого применять различные торговые стратегии. Мы также можем использовать нейросеть для преобразования данных индикаторов в торговые сигналы.
В этой статье мы стремимся развеять миф о сложности применения нейронных сетей и показать, как, оставив за рамками сложные детали настройки весов и оптимизации, можно создать торговый советник на базе нейронной сети, не обладая глубокими знаниями в области машинного обучения. Мы шаг за шагом рассмотрим процесс создания советника, начиная от сбора и подготовки данных, заканчивая обучением модели и её интеграцией в торговую стратегию.
Погружение в проблематику обучения
Существует три основных вида обучения. Нас интересуют нюансы этих видов применительно к анализу рыночных данных. Подход, представленный в статье, призван учесть недостатки этих видов обучения.
Обучение с учителем. Модель обучается на размеченных данных, строя свои прогнозы на основе примеров. Целевая функция: минимизация ошибки соответствия прогноза целевому значению (например, ошибка MSE). Однако, у этого подхода есть ряд недостатков. Он требует значительного объема качественных размеченных данных, что представляет собой основную проблему в контексте временных рядов. Если у нас есть четкие и достоверные примеры для обучения, например, как в задачах распознавания рукописного текста или содержимого изображений, то процесс обучения проходит без затруднений. Нейронная сеть учится распознавать именно то, чему её обучали.
В случае временных рядов, ситуация обстоит иначе: разметить данные так, чтобы быть уверенным в их достоверности и релевантности, крайне сложно. На практике получается, что сеть обучается тому, что мы предполагаем, а не тому, что действительно имеет отношение к исследуемому процессу. Многие авторы подчеркивают, что для успешного обучения с учителем необходимо использовать "хорошие" метки, однако степень их качества в контексте временных рядов зачастую трудно определить заранее.
В результате, возникают и другие субъективные оценки качества обучения, такие как "переобучение", так же вводится искусственное понятие "шума", подразумевающее, что слишком "переобученная" сеть могла запомнить шумовые данные, а не основные закономерности. Вы нигде не найдете четкие определения и количественные характеристики "шума" и "переобучения" именно потому, что они субъективны, когда речь идет об анализе временных рядов. Поэтому, следует признать, что применение обучения с учителем к временным рядам требует учета множества трудно алгоритмизируемых нюансов, которые существенно влияют на устойчивость модели на новых данных.
Обучение без учителя. Модель сама ищет скрытые структуры в данных без меток. Целевая функция: разные — в зависимости от методов. Сложно оценить качество полученных результатов, так как нет четких меток для проверки. Модель может не найти полезные закономерности, если данные не имеют четкой структуры, и вообще не известно, действительно ли были найдены структуры в данных, имеющих непосредственное отношение к "несущему процессу".
Методы, которые традиционно относят к обучению без учителя: K-means, Самоорганизующиеся карты (SOM), и другие. Все эти методы обучаются, используя специфичные для них целевые функции.
Приведем соотвествующие примеры:
- K-средних (K-means). Минимизация внутрикластерной дисперсии, которая определяется как сумма квадратов расстояний между каждой точкой и центром её кластера.
- Метод главных компонент (PCA). Максимизация дисперсии проекций данных на новые оси (главные компоненты).
- Деревья решений (DT). Минимизация энтропии, индекса Джини, дисперсии и другие.
Обучение с подкреплением. Целевая функция: суммарная награда. Это метод машинного обучения, где агент (например, программа или робот) учится принимать решения, взаимодействуя с окружающей средой. Агент получает вознаграждение или наказание в зависимости от своих действий. Цель агента — максимизировать общее вознаграждение, обучаясь на основе опыта.
Результаты могут быть нестабильными из-за случайного характера обучения, что затрудняет предсказание поведения модели и не всегда подходит для задач, где нет четкой системы наград и штрафов, что может сделать обучение менее эффективным. Обучение с подкреплением, как правило, связано с множеством практических проблем: трудность представления целевой функции подкрепления при использовании таких алгоритмов обучения нейронных сетей, как ADAM и подобные, так как необходимо нормировать значения целевой функции в диапазон, близкий к [-1;1]. Это связано с вычислением производных функции активации в нейронах и обратным прохождением ошибки через сеть для корректировки весов, чтобы избежать "взрыва весов" и подобных эффектов, приводящих к ступору нейронной сети.
Выше мы рассмотрели классическую классификацию видов обучения. Как можно заметить, все они основаны на минимизации/максимизации некоторой целевой функции. Тогда становится очевидным, что основное отличие у них в только в одном — отсутствии или наличии "учителя", а в случае отсутствия такового, разделение видов обучения сводится к специфике целевой функции, которую необходимо оптимизировать.
Таким образом, на мой взгляд, классификацию видов обучения можно представить как обучение с учителем, когда есть целевые значения (минимизация ошибки прогноза относительно цели) и обучение без учителя, когда нет целевых значений. Подвиды обучения без учителя по типу целевой функции на основе свойств данных (расстояния, плотности и т.д.), результатов работы системы (интегральные метрики, например, прибыль, производительность и т.д.), распределений (для генеративных моделей) и других критериев оценки.
Универсальный аппроксиматор
Предложенный мной подход относится ко второму типу — обучению без учителя. В этом методе мы не пытаемся "обучить" нейронную сеть правильной торговле и не указываем ей, где открывать или закрывать позиции, поскольку сами не знаем ответа на эти вопросы. Вместо этого мы позволяем сети самостоятельно принимать торговые решения, а наша задача заключается в оценке её совокупных торговых результатов.
При этом нам нет необходимости нормализовать функцию оценки или беспокоиться о таких проблемах, как "взрывы весов" и "ступор сети", так как они отсутствуют в данном подходе. Мы логически отделяем нейронную сеть от алгоритма оптимизации и предоставляем ей только задачу преобразования входных данных в новый вид информации, который отражает навыки трейдера. По сути, мы просто преобразуем один тип информации в другой, не имея представления о закономерностях во временном ряде и о том, как нужно торговать для получения прибыли.
Для этой роли идеально подходит такой тип нейронных сетей, как MLP (multilayer perceptron), что подтверждается теоремой об универсальной аппроксимации. Эта теорема утверждает, что нейронные сети могут приближать любую непрерывную функцию. В нашем случае под "непрерывной функцией" мы понимаем процесс, происходящий в анализируемом временном ряде. В данном подходе нет необходимости прибегать к искусственным и субъективным понятиям, таким как "шум" и "переобучение", которые не имеют количественной оценки.
Чтобы иметь представление, как это работает, достаточно обратится к рисунку 1. Мы подаем в MLP некоторую информацию, имеющую отношение к рыночным данным на текущий момент времени (это могут быть цены OHLC баров, значения индикаторов и т.д.), а на выходе получаем готовые к применению торговые сигналы. После прогона по истории торгового символа, мы можем посчитать целевую функцию, представляющую собой интегральную оценку (или комплекс оценок) торговых результатов, и скорректировать веса сети внешним алгоритмом оптимизации, максимизируя целевую функцию, описывающую качество торговых результатов нейронной сети.
Рисунок 1. Преобразование одного вида информации в другой
Реализация MLP в составе торгового советника
Сначала напишем класс MLP, затем встроим класс в советника. В статьях представлено очень много различных реализаций сетей разных архитектур, но я покажу свой вариант MLP, который представляет собой именно нейронную сеть, без оптимизатора.
Объявим класс "C_MLP", который реализует многослойный персептрон (MLP). Основные функции:
1. Init () — инициализация, настраивает сеть в зависимости от требуемого количества слоев и количества нейронов в каждом слое и возвращает общее количество весов.
2. ANN () — прямой проход от первого входного слоя к последнему выходному, метод принимает входные данные и веса, вычисляет выходные значения сети (см. рисунок 1).
3. GetWcount () — метод возвращает общее количество весов в сети.
4. LayerCalc () — выполняет расчет слоя сети.
Внутренние элементы:
- слои — хранят значения нейронов
- weightsCNT — общее количество весов
- layersCNT — общее количество слоев
Класс позволяет создавать нейронную сеть MLP с любым количеством скрытых слоев и любым количеством нейронов в них.
//+----------------------------------------------------------------------------+ //| Класс многослойного персептрона (MLP) | //| Реализует прямой проход по полносвязной нейронной сети | //| Архитектура: Lin -> L1 -> L2 -> ... Ln -> Lout | //+----------------------------------------------------------------------------+ class C_MLP { public: //-------------------------------------------------------------------- // Инициализация сети заданной конфигурацией // Возвращает общее количество весов в сети или 0 в случае ошибки int Init (int &layerConfig []); // Последовательно вычисляет значения всех слоев от входа к выходу void ANN (double &inLayer [], // входные значения double &weights [], // веса сети (включая смещения) double &outLayer []); // значения выходного слоя // Получить общее количество весов в сети int GetWcount () { return weightsCNT; } int layerConf []; // Конфигурация сети - количество нейронов в каждом слое private: //------------------------------------------------------------------- // Структура для хранения слоя нейронной сети struct S_Layer { double l []; // Значения нейронов }; S_Layer layers []; // Массив всех слоев сети int weightsCNT; // Общее количество весов в сети (включая смещения) int layersCNT; // Общее количество слоев (включая входной и выходной) int cnt_W; // Текущий индекс в массиве весов при проходе по сети double temp; // Временная переменная для хранения суммы взвешенных входов // Вычисление значений одного слоя сети void LayerCalc (double &inLayer [], // значения нейронов предыдущего слоя double &weights [], // массив весов и смещений всей сети double &outLayer [], // массив для записи значений текущего слоя const int inSize, // количество нейронов во входном слое const int outSize); // outSize - количество нейронов в выходном слое };
Инициализируется многослойный персептрон (MLP) с заданной конфигурацией слоев. Основные шаги:
1. Проверка конфигурации:
- Проверка, что в сети минимум 2 слоя (входной и выходной).
- Проверка, что в каждом слое есть хотя бы 1 нейрон. Если условия не выполнены, выводится сообщение об ошибке, и функция возвращает 0.
2. Сохранение конфигурации каждого слоя для быстрого доступа в массив "layerConf".
3. Создание массивов слоев: выделяется память для хранения нейронов в каждом слое.
4. Подсчет весов: рассчитывается общее количество весов в сети, включая смещения для каждого нейрона.
Функция возвращает общее количество весов или 0 в случае ошибки.
//+----------------------------------------------------------------------------+ //| Инициализация сети | //| layerConfig - массив с количеством нейронов в каждом слое | //| Возвращает общее количество необходимых весов или 0 при ошибке | //+----------------------------------------------------------------------------+ int C_MLP::Init (int &layerConfig []) { // Проверяем, что сеть имеет минимум 2 слоя (входной и выходной) layersCNT = ArraySize (layerConfig); if (layersCNT < 2) { Print ("Error Net config! Layers less than 2!"); return 0; } // Проверяем, что в каждом слое есть хотя бы 1 нейрон for (int i = 0; i < layersCNT; i++) { if (layerConfig [i] <= 0) { Print ("Error Net config! Layer No." + string (i + 1) + " contains 0 neurons!"); return 0; } } // Сохраняем конфигурацию сети ArrayCopy (layerConf, layerConfig, 0, 0, WHOLE_ARRAY); // Создаем массив слоев ArrayResize (layers, layersCNT); // Выделяем память под нейроны каждого слоя for (int i = 0; i < layersCNT; i++) { ArrayResize (layers [i].l, layerConfig [i]); } // Подсчитываем общее количество весов в сети weightsCNT = 0; for (int i = 0; i < layersCNT - 1; i++) { // Для каждого нейрона следующего слоя нужно: // - одно значение смещения (bias) // - веса для связей со всеми нейронами текущего слоя weightsCNT += layerConf [i] * layerConf [i + 1] + layerConf [i + 1]; } return weightsCNT; }
Метод "LayerCalc" выполняет вычисления для одного слоя нейронной сети, используя гиперболический тангенс в качестве функции активации. Основные шаги:
1. Входные и выходные параметры:
- inLayer [] — массив входных значений от предыдущего слоя
- weights [] — массив весов содержит смещения и веса для связей
- outLayer [] — массив для хранения выходных значений текущего слоя
- inSize — количество нейронов во входном слое
- outSize — количество нейронов в выходном слое
2. Цикл по нейронам выходного слоя. Для каждого нейрона в выходном слое:
- начинает с значения смещения (bias)
- добавляет взвешенные входные значения (каждое входное значение умножается на соответствующий вес)
- считается значение функции активации для нейрона
3. Применение функции активации:
- использует гиперболический тангенс для нелинейного преобразования значения в диапазон от -1 до 1
- результат записывается в массив выходных значений "outLayer []"
//+----------------------------------------------------------------------------+ //| Вычисление значений одного слоя сети | //| Реализует формулу: y = tanh(bias + w1*x1 + w2*x2 + ... + wn*xn) | //+----------------------------------------------------------------------------+ void C_MLP::LayerCalc (double &inLayer [], double &weights [], double &outLayer [], const int inSize, const int outSize) { // Вычисляем значение для каждого нейрона в выходном слое for (int i = 0; i < outSize; i++) { // Начинаем со значения смещения (bias) для текущего нейрона temp = weights [cnt_W]; cnt_W++; // Добавляем взвешенные входы от каждого нейрона предыдущего слоя for (int u = 0; u < inSize; u++) { temp += inLayer [u] * weights [cnt_W]; cnt_W++; } // Применяем функцию активации "гиперболический тангенс" // f(x) = 2/(1 + e^(-x)) - 1 // Диапазон значений f(x): [-1, 1] outLayer [i] = 2.0 / (1.0 + exp (-temp)) - 1.0; } }
Реализуем работу искусственной нейронной сети, последовательно вычисляя значения всех слоев — от входного до выходного.
1. Входные и выходные параметры:
- inLayer [] — массив входных значений, которые поступают в нейронную сеть
- weights [] — массив весов, который включает в себя как веса для связей между нейронами, так и смещения
- outLayer [] — массив, в который будут записаны выходные значения последнего слоя нейронной сети
2. Сброс счетчика весов: переменная "cnt_W", которая отслеживает текущую позицию в массиве весов, сбрасывается в 0 перед началом вычислений.
3. Копирование входных данных: входные данные из "inLayer" копируются в первый слой сети с помощью функции "ArrayCopy".
4. Цикл по слоям:
- цикл проходит по всем слоям нейронной сети.
- для каждого слоя вызывается функция "LayerCalc", которая вычисляет значения для текущего слоя на основе выходных значений предыдущего слоя, весов и размеров слоев.
5. После завершения вычислений для всех слоев, выходные значения последнего слоя копируются в массив "outLayer" с помощью функции "ArrayCopy".
//+----------------------------------------------------------------------------+ //| Последовательно вычисляет значения всех слоев от входа к выходу | //+----------------------------------------------------------------------------+ void C_MLP::ANN (double &inLayer [], // входные значения double &weights [], // веса сети (включая смещения) double &outLayer []) // значения выходного слоя { // Сбрасываем счетчик весов перед началом прохода cnt_W = 0; // Копируем входные данные в первый слой сети ArrayCopy (layers [0].l, inLayer, 0, 0, WHOLE_ARRAY); // Последовательно вычисляем значения каждого слоя for (int i = 0; i < layersCNT - 1; i++) { LayerCalc (layers [i].l, // выход предыдущего слоя weights, // веса сети (включая смещения/bias) layers [i + 1].l, // следующий слой layerConf [i], // размер текущего слоя layerConf [i + 1]); // размер следующего слоя } // Копируем значения последнего слоя в выходной массив ArrayCopy (outLayer, layers [layersCNT - 1].l, 0, 0, WHOLE_ARRAY); }
Настала очередь написать советник для автоматической торговой стратегии с использованием машинного обучения на базе нейронной сети MLP.
1. Подключим библиотеки для торговых операций, обработки информации торгового символа, математических функций, многослойного перцептрона (MLP) и алгоритмов оптимизации.
2. Параметры торговли — объем позиции, часы начала и окончания торговли. Параметры обучения — выбор оптимизатора, структура нейронной сети, количество баров для анализа, глубина истории для обучения, период актуальности модели и порог сигнала.
3. Объявление классов и переменных — объекты классов для утилит, нейронной сети, а также переменные для хранения входных данных, весов и времени последнего обучения.
#include "#Symbol.mqh" #include <Math\AOs\Utilities.mqh> #include <Math\AOs\NeuroNets\MLP.mqh> #include <Math\AOs\PopulationAO\#C_AO_enum.mqh> //------------------------------------------------------------------------------ input group "---Trade parameters-------------------"; input double Lot_P = 0.01; // Объем позиции input int StartTradeH_P = 3; // Час начала торговли input int EndTradeH_P = 12; // Час окончания торговли input group "---Training parameters----------------"; input E_AO OptimizerSelect_P = AO_CLA; // Выбрать оптимизатор input int NumbTestFuncRuns_P = 5000; // Общее количество запусков тестовой функции input string MLPstructure_P = "1|1"; // Скрытые слои, <4|6|2> - три скрытых слоя input int BarsAnalysis_P = 3; // Кол-во баров для анализа input int DepthHistoryBars_P = 10000; // Глубина истории для обучения в барах input int RetrainingPeriod_P = 12; // Длительность в часах актуальности модели input double SigThr_P = 0.5; // Порог сигнала //------------------------------------------------------------------------------ C_AO_Utilities U; C_MLP NN; int InpSigNumber; int WeightsNumber; double Inputs []; double Weights []; double Outs [1]; datetime LastTrainingTime = 0; C_Symbol S; C_NewBar B; int HandleS; int HandleR;
В качестве информации, поступающей на обработку в нейронную сеть, я выбрал первое, что пришло в голову: OHLC — цены баров (по умолчанию в настройках 3 предыдущих бара перед текущим) и значения индикаторов RSI и Stochastic на этих барах. Функция "OnInit ()" инициализирует торговую стратегию с использованием нейронной сети.
1. Инициализация индикаторов — создаются объекты для RSI и Stochastic.
2. Расчет количества входных сигналов для сети на основе входного параметра "BarsAnalysis_P".
3. Настройка структуры нейронной сети — разбивается строка входного параметра с конфигурацией сети, проверяется корректность количества слоев и нейронов. Входной строковый параметр задает количество скрытых слоев сети и нейронов в них, по умолчанию параметр равен "1|1", что означает 2 скрытых слоя в сети по одному нейрону в каждом.
4. Инициализация нейронной сети — вызывается метод для инициализации сети, создаются массивы для весов и входных данных.
5. Вывод информации — печатаются данные о количестве слоев и параметров сети.
6. Возвращается статус успешной инициализации.
Функция обеспечивает подготовку всех необходимых компонентов для работы торговой стратегии.
//—————————————————————————————————————————————————————————————————————————————— int OnInit () { //---------------------------------------------------------------------------- // Инициализация индикаторов: Stochastic и RSI HandleS = iStochastic (_Symbol, PERIOD_CURRENT, 5, 3, 3, MODE_EMA, STO_LOWHIGH); HandleR = iRSI (_Symbol, PERIOD_CURRENT, 14, PRICE_TYPICAL); // Расчет количества входов в нейронную сеть на основе количества баров для анализа InpSigNumber = BarsAnalysis_P * 2 + BarsAnalysis_P * 4; // Вывод информации о количестве входов Print ("Количество входов в сеть : ", InpSigNumber); //---------------------------------------------------------------------------- // Инициализация структуры многослойного MLP string sepResult []; int layersNumb = StringSplit (MLPstructure_P, StringGetCharacter ("|", 0), sepResult); // Проверка, что количество скрытых слоев больше 0 if (layersNumb < 1) { Print ("Ошибка конфигурации сети, скрытых слоев < 1..."); return INIT_FAILED; // Возвращаем ошибку инициализации } // Увеличиваем количество слоев на 2 (входной и выходной) layersNumb += 2; // Инициализация массива для конфигурации нейронной сети int nnConf []; ArrayResize (nnConf, layersNumb); // Установка количества входов и выходов в конфигурацию сети nnConf [0] = InpSigNumber; // Входной слой nnConf [layersNumb - 1] = 1; // Выходной слой // Заполнение конфигурации скрытых слоев for (int i = 1; i < layersNumb - 1; i++) { nnConf [i] = (int)StringToInteger (sepResult [i - 1]); // Преобразование строкового значения в целое число // Проверка, что количество нейронов в слое больше 0 if (nnConf [i] < 1) { Print ("Ошибка конфигурации сети, в слое ", i, " <= 0 нейронов..."); return INIT_FAILED; // Возвращаем ошибку инициализации } } // Инициализация нейронной сети и получение количества весов WeightsNumber = NN.Init (nnConf); if (WeightsNumber <= 0) { Print ("Ошибка инициализации сети MLP..."); return INIT_FAILED; // Возвращаем ошибку инициализации } // Изменение размера массива входных данных и весов ArrayResize (Inputs, InpSigNumber); ArrayResize (Weights, WeightsNumber); // Инициализация весов случайными значениями в диапазоне [-1, 1] (для отладки) for (int i = 0; i < WeightsNumber; i++) Weights [i] = 2 * (rand () / 32767.0) - 1; // Вывод информации о конфигурации сети Print ("Количество всех слоев : ", layersNumb); Print ("Количество параметров сети: ", WeightsNumber); //---------------------------------------------------------------------------- // Инициализация торгового и барного классов S.Init (_Symbol); B.Init (_Symbol, PERIOD_CURRENT); return (INIT_SUCCEEDED); // Возвращаем успешный результат инициализации } //——————————————————————————————————————————————————————————————————————————————
Основная логика торговой стратегии реализована в функции "OnTick ()". Стратегия простая: если сигнал нейрона выходного слоя превышает порог, заданный в параметрах, то сигнал интерпретируется как соответствующий направлению купить/продать, и если нет открытых позиций, и текущее время разрешено для торговли, то открываем позицию. Позиция закрывается, в случае получения от нейронной сети противоположного сигнала, или принудительно, в случае завершения разрешенного для торговли времени. Проговорим основные шаги стратегии:
1. Проверка на необходимость нового обучения. Если с последнего обучения прошло достаточно времени, запускается процесс обучения нейронной сети. В случае ошибки — выводится сообщение.
2. Проверка нового бара. Если текущий тик не является началом нового бара, выполнение функции прекращается.
3. Получение данных. Код запрашивает данные о ценах (открытие, закрытие, максимум, минимум) и значения индикаторов (RSI и Stochastic).
4. Нормализация данных. Находятся максимумы и минимумы среди полученных данных цен символа, после чего, все данные нормализуются в диапазоне от -1 до 1.
5. Прогнозирование. Нормализованные данные передаются в нейронную сеть для получения выходных сигналов.
6. Генерация торгового сигнала. На основе выходных данных формируется сигнал для покупки (1) или продажи (-1).
7. Управление позициями. Если текущая позиция противоречит сигналу, она закрывается. Если сигнал на открытие новой позиции совпадает с разрешённым временем, позиция открывается. В противном случае, если есть открытая позиция, она закрывается.
Таким образом, логика в OnTick () реализует полный цикл автоматической торговли, включая обучение, получение данных, нормализацию, прогнозирование и управление позициями.
//—————————————————————————————————————————————————————————————————————————————— void OnTick () { // Проверка, нужно ли переобучить нейронную сеть if (TimeCurrent () - LastTrainingTime >= RetrainingPeriod_P * 3600) { // Запуск процесса обучения нейронной сети if (Training ()) LastTrainingTime = TimeCurrent (); // Обновление времени последнего обучения else Print ("Ошибка обучения..."); // Вывод сообщения об ошибке return; // Завершение выполнения функции } //---------------------------------------------------------------------------- // Проверка, является ли текущий тик началом нового бара if (!B.IsNewBar ()) return; //---------------------------------------------------------------------------- // Объявление массивов для хранения данных о ценах и индикаторах MqlRates rates []; double rsi []; double sto []; // Получение данных о ценах if (CopyRates (_Symbol, PERIOD_CURRENT, 1, BarsAnalysis_P, rates) != BarsAnalysis_P) return; // Получение значений Stochastic if (CopyBuffer (HandleS, 0, 1, BarsAnalysis_P, sto) != BarsAnalysis_P) return; // Получение значений RSI if (CopyBuffer (HandleR, 0, 1, BarsAnalysis_P, rsi) != BarsAnalysis_P) return; // Инициализация переменных для нормализации данных int wCNT = 0; double max = -DBL_MAX; // Начальное значение для максимума double min = DBL_MAX; // Начальное значение для минимума // Поиск максимума и минимума среди high и low for (int b = 0; b < BarsAnalysis_P; b++) { if (rates [b].high > max) max = rates [b].high; // Обновление максимума if (rates [b].low < min) min = rates [b].low; // Обновление минимума } // Нормализация входных данных для нейронной сети for (int b = 0; b < BarsAnalysis_P; b++) { Inputs [wCNT] = U.Scale (rates [b].high, min, max, -1, 1); wCNT++; // Нормализация high Inputs [wCNT] = U.Scale (rates [b].low, min, max, -1, 1); wCNT++; // Нормализация low Inputs [wCNT] = U.Scale (rates [b].open, min, max, -1, 1); wCNT++; // Нормализация open Inputs [wCNT] = U.Scale (rates [b].close, min, max, -1, 1); wCNT++; // Нормализация close Inputs [wCNT] = U.Scale (sto [b], 0, 100, -1, 1); wCNT++; // Нормализация Stochastic Inputs [wCNT] = U.Scale (rsi [b], 0, 100, -1, 1); wCNT++; // Нормализация RSI } // Преобразование данных из Inputs в Outs NN.ANN (Inputs, Weights, Outs); //---------------------------------------------------------------------------- // Генерация торгового сигнала на основе выходных данных нейронной сети int signal = 0; if (Outs [0] > SigThr_P) signal = 1; // Сигнал на покупку if (Outs [0] < -SigThr_P) signal = -1; // Сигнал на продажу // Получение типа открытой позиции int posType = S.GetPosType (); S.GetTick (); if ((posType == 1 && signal == -1) || (posType == -1 && signal == 1)) { if (!S.PosClose ("", ORDER_FILLING_FOK) != 0) posType = 0; else return; } MqlDateTime time; TimeToStruct (TimeCurrent (), time); // Проверка разрешенного времени для торговли if (time.hour >= StartTradeH_P && time.hour < EndTradeH_P) { // Открытие новой позиции в зависимости от сигнала if (posType == 0 && signal != 0) S.PosOpen (signal, Lot_P, "", ORDER_FILLING_FOK, 0, 0.0, 0.0, 1); } else { if (posType != 0) S.PosClose ("", ORDER_FILLING_FOK); } } //——————————————————————————————————————————————————————————————————————————————
Далее рассмотрим обучение нейронной сети на исторических данных:
1. Получение данных. Загружаются исторические данные о ценах, а также значения индикаторов RSI и Stochastic.
2. Определение торгового времени. Создается массив, который отмечает, какие бары попадают в разрешенное время для торговли.
3. Настройка параметров оптимизации. Инициализируются границы и шаги параметров для оптимизации.
4. Выбор алгоритма оптимизации. Определяется алгоритм оптимизации и задается размер популяции.
5. Основной цикл оптимизации весов нейронной сети:
- для каждого решения в популяции вычисляется значение целевой функции, оценивающее его качество.
- популяция решений обновляется на основе результатов.
6. Вывод результатов. Печатается имя алгоритма, лучший результат и копируются лучшие параметры в массив весов.
7. Освобождается память, занятая объектом алгоритма оптимизации.
Функция осуществляет процесс обучения нейронной сети для нахождения лучших параметров на исторических данных.
//—————————————————————————————————————————————————————————————————————————————— bool Training () { MqlRates rates []; double rsi []; double sto []; int bars = CopyRates (_Symbol, PERIOD_CURRENT, 1, DepthHistoryBars_P, rates); Print ("Обучение на истории ", bars, " баров"); if (CopyBuffer (HandleS, 0, 1, DepthHistoryBars_P, sto) != bars) return false; if (CopyBuffer (HandleR, 0, 1, DepthHistoryBars_P, rsi) != bars) return false; MqlDateTime time; bool truTradeTime []; ArrayResize (truTradeTime, bars); ArrayInitialize (truTradeTime, false); for (int i = 0; i < bars; i++) { TimeToStruct (rates [i].time, time); if (time.hour >= StartTradeH_P && time.hour < EndTradeH_P) truTradeTime [i] = true; } //---------------------------------------------------------------------------- int popSize = 50; // Размер популяции для алгоритма оптимизации int epochCount = NumbTestFuncRuns_P / popSize; // Общее количество эпох (итераций) для оптимизации double rangeMin [], rangeMax [], rangeStep []; // Массивы для хранения границ и шагов параметров ArrayResize (rangeMin, WeightsNumber); // Изменение размера массива min границ ArrayResize (rangeMax, WeightsNumber); // Изменение размера массива max границ ArrayResize (rangeStep, WeightsNumber); // Изменение размера массива шагов for (int i = 0; i < WeightsNumber; i++) { rangeMax [i] = 5.0; rangeMin [i] = -5.0; rangeStep [i] = 0.01; } //---------------------------------------------------------------------------- C_AO *ao = SelectAO (OptimizerSelect_P); // Выбор алгоритма оптимизации ao.params [0].val = popSize; // Назначение размера популяции.... ao.SetParams (); //... (необязательно, тогда будет использован размер популяции по умолчанию) ao.Init (rangeMin, rangeMax, rangeStep, epochCount); // Инициализация алгоритма с заданными границами и количеством эпох // Основной цикл по количеству эпох for (int epochCNT = 1; epochCNT <= epochCount; epochCNT++) { ao.Moving (); // Выполнение одной эпохи алгоритма оптимизации // Вычисление значения целевой функции для каждого решения в популяции for (int set = 0; set < ArraySize (ao.a); set++) { ao.a [set].f = TargetFunction (ao.a [set].c, rates, rsi, sto, truTradeTime); //FF.CalcFunc (ao.a [set].c); //ObjectiveFunction (ao.a [set].c); // Применение целевой функции к каждому решению } ao.Revision (); // Обновление популяции на основе результатов целевой функции } //---------------------------------------------------------------------------- // Вывод имени алгоритма, лучшего результата и количества запусков функции Print (ao.GetName (), ", лучший результат: ", ao.fB); ArrayCopy (Weights, ao.cB); delete ao; // Освобождение памяти, занятой объектом алгоритма return true; } //——————————————————————————————————————————————————————————————————————————————
Реализуем целевую функцию для оценки эффективности торговой стратегии, использующей нейронную сеть.
1. Инициализация переменных. Устанавливаются переменные для отслеживания прибыли, убытков, количества сделок и других параметров.
2. Обработка исторических данных. Цикл проходит по историческим данным и проверяет, разрешено ли открытие позиций на текущем баре.
3. Нормализация данных. Для каждого бара нормализуются значения цен (high, low, open, close) и индикаторов (RSI и Stochastic) для последующей передачи в нейронную сеть.
4. Прогнозирование сигналов. Нормализованные данные подаются в нейронную сеть, которая генерирует торговые сигналы (покупка или продажа).
5. Управление виртуальными позициями осуществляется согласно торговой стратегии в OnTick ().
6. Расчет результата. В конце функции вычисляется общее соотношение прибыли к убыткам, умноженное на количество сделок, с учетом понижающего коэффициента за дисбаланс между покупками и продажами.
Функция оценивает эффективность торговой стратегии, анализируя прибыль и убытки на основе сигналов, генерируемых нейронной сетью, и возвращает числовое значение, отражающее ее качество (по сути функция осуществляет один прогон по истории назад от текущего положении во времени торгового советника).
//—————————————————————————————————————————————————————————————————————————————— double TargetFunction (double &weights [], MqlRates &rates [], double &rsi [], double &sto [], bool &truTradeTime []) { int bars = ArraySize (rates); // Инициализация переменных для нормализации данных int wCNT = 0; double max = 0.0; double min = 0.0; int signal = 0; double profit = 0.0; double allProfit = 0.0; double allLoss = 0.0; int dealsNumb = 0; int sells = 0; int buys = 0; int posType = 0; double posOpPrice = 0.0; double posClPrice = 0.0; // Прогон по истории for (int h = BarsAnalysis_P; h < bars - 1; h++) { if (!truTradeTime [h]) { if (posType != 0) { posClPrice = rates [h].open; profit = (posClPrice - posOpPrice) * signal - 0.00003; if (profit > 0.0) allProfit += profit; else allLoss += -profit; if (posType == 1) buys++; else sells++; allProfit += profit; posType = 0; } continue; } max = -DBL_MAX; // Начальное значение для максимума min = DBL_MAX; // Начальное значение для минимума // Поиск максимума и минимума среди high и low for (int b = 1; b <= BarsAnalysis_P; b++) { if (rates [h - b].high > max) max = rates [h - b].high; // Обновление максимума if (rates [h - b].low < min) min = rates [h - b].low; // Обновление минимума } // Нормализация входных данных для нейронной сети wCNT = 0; for (int b = BarsAnalysis_P; b >= 1; b--) { Inputs [wCNT] = U.Scale (rates [h - b].high, min, max, -1, 1); wCNT++; // Нормализация high Inputs [wCNT] = U.Scale (rates [h - b].low, min, max, -1, 1); wCNT++; // Нормализация low Inputs [wCNT] = U.Scale (rates [h - b].open, min, max, -1, 1); wCNT++; // Нормализация open Inputs [wCNT] = U.Scale (rates [h - b].close, min, max, -1, 1); wCNT++; // Нормализация close Inputs [wCNT] = U.Scale (sto [h - b], 0, 100, -1, 1); wCNT++; // Нормализация Stochastic Inputs [wCNT] = U.Scale (rsi [h - b], 0, 100, -1, 1); wCNT++; // Нормализация RSI } // Преобразование данных из Inputs в Outs NN.ANN (Inputs, weights, Outs); //---------------------------------------------------------------------------- // Генерация торгового сигнала на основе выходных данных нейронной сети signal = 0; if (Outs [0] > SigThr_P) signal = 1; // Сигнал на покупку if (Outs [0] < -SigThr_P) signal = -1; // Сигнал на продажу if ((posType == 1 && signal == -1) || (posType == -1 && signal == 1)) { posClPrice = rates [h].open; profit = (posClPrice - posOpPrice) * signal - 0.00003; if (profit > 0.0) allProfit += profit; else allLoss += -profit; if (posType == 1) buys++; else sells++; allProfit += profit; posType = 0; } if (posType == 0 && signal != 0) { posType = signal; posOpPrice = rates [h].open; } } dealsNumb = buys + sells; double ko = 1.0; if (sells == 0 || buys == 0) return -DBL_MAX; if (sells / buys > 1.5 || buys / sells > 1.5) ko = 0.001; return (allProfit / (allLoss + DBL_EPSILON)) * dealsNumb; } //——————————————————————————————————————————————————————————————————————————————
На рисунке 2 представлен график баланса торговых результатов, полученных с использованием советника на основе MLP на новых, незнакомых для нейронной сети данных. На вход подаются нормированные значения цен OHLC, а также индикаторы RSI и Stochastic, рассчитанные на основе указанного количества баров. Советник осуществляет торговлю, пока нейронная сеть остается актуальной; в противном случае, он проводит обучение сети и затем продолжает торговлю. Таким образом, результаты, показанные на рисунке 2, отражают эффективность работы на OOS (out of sample).
Рисунок 2. Результат работы советника на незнакомых для MLP данных
Выводы
В статье представлен простой и доступный способ использования нейронной сети в торговом советнике, который подходит для широкого круга трейдеров и не требует глубоких знаний в области машинного обучения. Этот метод исключает необходимость нормализации значений целевой функции для подачи их в нейронную сеть в качестве ошибки, а также устраняет потребность в методах, предотвращающих "взрыв весов". Кроме того, он решает проблему "ступора сети" и предлагает интуитивно понятное обучение с наглядным контролем результатов.
Следует отметить, что в советнике отсутствуют необходимые проверки при осуществлении торговых операций, и он предназначен исключительно для ознакомительных целей.
Программы, используемые в статье
# | Имя | Тип | Описание |
---|---|---|---|
1 | #C_AO.mqh | Включаемый файл | Родительский класс популяционных алгоритмов оптимизации |
2 | #C_AO_enum.mqh | Включаемый файл | Перечисление популяционных алгоритмов оптимизации |
3 | Utilities.mqh | Включаемый файл | Библиотека вспомогательных функций |
4 | #Symbol.mqh | Включаемый файл | Библиотека торговых и вспомогательных функций |
5 | ANN EA.mq5 | Советник | Советник на базе нейронной сети MLP |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Здравствуйте, Cape.
К статье прикреплен архив, который содержит все необходимые файлы. Прямо сейчас скачал из статьи архив, открыл, и убедился, что нужные вам файлы присутствуют:
Обучал на EURUSD M15, если мне не изменяет память.
Рядом с противоположным сигналом не работает
Привет, Андрей,
Понял, спасибо за быстрый ответ.
CapeCoddah
Попробуйте его на счете типа неттинг. Статья дает только представление, вы должны адаптировать советник к торговым условиям вашего брокера.
Большое спасибо за статью и понимание. Отличная идея. Я реализовал некоторые независимые операции с позициями и заставил их работать на хеджирующем счете (мой брокер).
Вы лучшие.
Большое спасибо за то, что поделились этой статьей и своим пониманием. Отличная идея. Я реализовал некоторые независимые операции с позициями и заставил их работать на хеджирующем счете (мой брокер).
Вы лучшие.
Супер!