preview
Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Окончание)

Нейросети в трейдинге: Эффективное извлечение признаков для точной классификации (Окончание)

MetaTrader 5Торговые системы |
711 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

Мы подошли к заключительной фазе знакомства с фреймворком Mantis. Сначала подробно разобрали его теоретическую основу: как модель воспринимает многоканальные временные ряды, зачем нужны дифференциальные сигналы, как формируются локальные токены. Затем углубились в архитектуру и посмотрели, каким образом структурируются патчи, и как модель учится извлекать устойчивые паттерны из рыночного шума. Сейчас настал момент связать всё воедино.

Фреймворк Mantis построен как последовательность независимых, но логически связанных модулей. Каждый из них выполняет специализированную задачу — от первичной обработки исходных данных до формирования токенов и агрегации информации. В основе лежит идея: минимальная предвзятость, максимум извлечённых закономерностей из самих данных.

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

Каждый из этих каналов подаётся на сверточные блоки. Здесь начинается первичный анализ. Свертки извлекают локальные признаки, обучаясь различать устойчивые микро-паттерны. На выходе каждого канала получается 256 признаков, представляющих его локальное поведение.

После свертки начинается следующий этап: данные разбиваются на 32 непересекающихся патча. Каждый из них охватывает определённый участок временного ряда. По каждому проводится усреднение по каналам (mean-pooling). Таким образом получаем 32 токена, каждый длиной 256 признаков. Это важный момент. Патчи — это локализованные фрагменты поведения рынка, как бы сжатые мини-графики. Они фиксируют фазы от импульсов и консолидаций, до откатов и дивергенций. Подобное разбиение делает поведение модели более устойчивым и адаптивным. Она начинает видеть рынок не как поток чисел, а как череду узнаваемых сценариев.

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

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

За этим всем стоит простая, но мощная идея: сохранить локальную точность и не потерять глобальный контекст. В трейдинге это критично. Например, последовательность мелких свечей в момент затишья может не нести информации. Но если они появляются после резкого объёма, да ещё и у важного уровня — контекст меняется полностью. Mantis это учитывает автоматически.

Фреймворк Mantis — это не просто архитектура. Это гибкий инструмент, способный адаптироваться к различным типам торговых стратегий.

Авторская визуализация фреймворка Mantis представлена ниже.


Архитектура модели

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

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

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

Именно этот эмбеддинг передаётся в следующую часть архитектуры, построенной по принципу Actor–Director–Critic. Такой подход позволяет разделить обязанности между модулями и обеспечить стабильность поведения агента. Actor получает эмбеддинг и формирует торговое решение. Director — структурный фильтр и поведенческий корректор, который классифицирует предложенные Actor действия на допустимые и ошибочные, подавая мощные сигналы обратной связи. Его задача — отсечь заведомо неразумные или нестабильные решения, особенно в зонах рыночной турбулентности. Critic завершает цикл, оценивая стратегическую целесообразность действий, исходя из текущего состояния и истории взаимодействий. Он играет роль внутреннего советника: даже если действие технически возможно, действительно ли оно стоит риска в текущем контексте.

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

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&actor,
                        CArrayObj *&director,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!director)
     {
      director = new CArrayObj();
      if(!director)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

В параметрах метода мы получаем указатели на четыре динамических массива, каждый из которых предназначен для хранения архитектурных описаний соответствующих компонентов модели: Энкодера, Актёра, Режиссёра и Критика. Эти массивы выступают в роли контейнеров, куда поочерёдно добавляются объекты, описывающие структуру нейросетевых блоков.

Внутри тела метода в первую очередь осуществляется проверка валидности полученных указателей. Если какой-либо из них оказался неинициализированным или указывает на недопустимую область памяти, автоматически создаётся новый экземпляр соответствующего объекта. Такой подход избавляет от необходимости заранее подготавливать массивы вручную, повышая модульность кода и его пригодность для масштабируемых решений.

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

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

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Разрозненные и разномасштабные исходные данные направляются в блок пакетной нормализации. Здесь все значения приводятся к сопоставимому масштабу, устраняется перекос в амплитудах, что особенно критично при работе с многомерными финансовыми временными рядами. Нормализация позволяет модели воспринимать данные не как случайный набор цифр, а как логически согласованный поток информации.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatDiff;
   prev_count = descr.count = HistoryBars;
   descr.layers = BarDescr;
   descr.step = 1;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   int prev_out = descr.window_out = NSkills;
     {
      int temp[] = {PeriodSeconds(PERIOD_H1), PeriodSeconds(PERIOD_D1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisPatching;
   descr.count = prev_count;
   descr.layers = prev_out;
   descr.window = EmbeddingSize;
   descr.window_out = NSkills;
   descr.step = Segments;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.step;
   prev_out = descr.layers;

Блок внимания в нашем Энкодере реализован как единый объект — CNeuronMantisAttentionUnit. Этот компонент играет ключевую роль в улавливании важных взаимосвязей между патчами и каналами. В параметрах объекта мы задаём количество внутренних модулей кросс-внимания, что позволяет гибко настраивать глубину и широту анализа информации.

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

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisAttentionUnit;
   descr.step = 4;
   descr.count = prev_count;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = 3;
   descr.window_out = NSkills;
   descr.window = prev_out;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

//---
   CLayerDescription *latent = encoder.At(7);
//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = AccountDescr;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Параллельно, по вспомогательной магистрали, в модель поступает латентное представление рыночной среды, сформированное Энкодером. Совмещение этих двух потоков данных — внутреннего состояния и внешнего контекста — позволяет Актёру принимать решения, в которых уже заложен элемент стратегического мышления и риск-менеджмента. Модель обучается учитывать не только текущую ситуацию на рынке, но и собственные ресурсы, уровень допустимого риска и динамику текущей позиции. Такой подход делает поведение Агента осознанным и устойчивым, даже в условиях повышенной рыночной турбулентности.

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

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = AccountDescr;        // Inputs window
   descr.step = latent.windows[0];     // Cross window
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SoftPlus;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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


Контрастное обучение

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

Контрастное обучение в данном случае выступает как фундамент для развития рыночного чутья у модели. Без жёсткого учителя, но с чётким пониманием различий между парами состояний, Энкодер учится строить такие представления, которые помогут максимально эффективно различать близкие, но по сути разные рыночные сценарии.

Реализация соответствующего алгоритма представлена в советнике "…\MQL5\Experts\Mantis\StudyContrast.mq5". Данный советник управляет процессом формирования пар положительных и отрицательных примеров, производит обучающую итерацию, накапливает статистику и сохраняет обученные параметры.

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

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

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input datetime             Start          = D'2020.01.01';
input datetime             End            = D'2025.01.01';
input int                  Iterations     = 100000;
input int                  Batch          = 50;
input group                "---- Indicators ----"
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_M1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

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

Процесс начинается с определения границ исторических данных. С помощью функции iBarShift определяется смещение от текущего бара до момента начала и окончания обучения.

void Train(void)
  {
   int start = iBarShift(Symb.Name(), TimeFrame, Start);
   int end = iBarShift(Symb.Name(), TimeFrame, End);
   int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates);

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

if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
   !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

Следующий этап — загрузка данных. Для этого реализован цикл, в котором поочерёдно вызываются методы Refresh всех индикаторов и проверяется количество  рассчитанных значений. Цикл завершится либо после успешной загрузки данных, либо по истечении 100 попыток.

int count = -1;
bool load = false;
do
  {
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   count++;
   load = (RSI.BarsCalculated() >= bars &&
           CCI.BarsCalculated() >= bars &&
           ATR.BarsCalculated() >= bars &&
           MACD.BarsCalculated() >= bars
          );
   Sleep(100);
   count++;
  }
while(!load && count < 100);
if(!load)
  {
   PrintFormat("%s -> %d The training data has not been loaded",
                                        __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

В завершении процесса подготовки данных, выставляем нужное направление индексации массива котировок (ArraySetAsSeries) и объявляем необходимые локальные переменные.

   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   vector<float> result, target, neg_target;
   bool Stop = false;

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

   uint ticks = GetTickCount();
//---
   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
     {
      int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }

Для этого состояния создаются буферы исходных данных и выполняется прямой проход Энкодера.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
cEncoder.getResults(Result);

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

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

//--- Positive
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                 (CBufferFloat*)GetPointer(bTime)) ||
   !cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

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

//--- Negotive
if(!Result.GetData(target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
for(int b = 0; b < Batch; b++)
  {
   int negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
   int count = 0;
   while(negot == posit)
     {
      negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      count++;
      if(count > 100)
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
     }
   if(Stop)
            break;

В теле вложенного цикла мы случайным образом выбираем ещё одно состояние из обучающего диапазона — отличное от ранее использованного эталона. Здесь стоит подчеркнуть важный момент: мы допускаем любое состояние, лишь бы оно не совпадало с эталоном. Теоретически возможны даже пересечения диапазонов.

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

Таким образом, наше контрастное обучение заставляет Энкодер не просто распознавать состояния, а выделять ключевые особенности, создавая информативные и различимые токены, необходимые для последующей работы Актёра.

Для выбранного состояния генерируем буферы исходных данных и осуществляем прямой проход Энкодера.

if(!CreateBuffers(negot + end, GetPointer(bState), GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }
//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

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

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

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

cEncoder.getResults(result);
neg_target = result * 2 - target;
neg_target = neg_target - neg_target.Max();
if(!neg_target.Activation(neg_target, AF_SOFTMAX) ||
   !Result.AssignArray(neg_target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

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

if(!cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

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

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

    if(GetTickCount() - ticks > 500)
      {
       double percent = double(iter + b) * 100.0 / (Iterations);
       string str = StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Encoder",
                                   percent, cEncoder.getRecentAverageError());
       Comment(str);
       ticks = GetTickCount();
      }
   }
}

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

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Encoder",
                                           cEncoder.getRecentAverageError());
   ExpertRemove();
//---
  }

Это аккуратное и контролируемое завершение сеанса, которое гарантирует сохранение всех важных данных и корректное освобождение ресурсов.

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


Тестирование

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

Первый этап — контрастное обучение Энкодера. Оно проводится на исторических данных за последние пять лет по валютной паре EURUSD на таймфрейме М1. Такой объём и детализация данных позволяют Энкодеру выработать качественные и информативные латентные представления состояния рынка, что является фундаментом для дальнейшей работы всей системы.

Далее наступает второй этап — офлайн обучение ключевых компонентов системы: Актера, Режиссёра и Критика. Для этого используется обучающая выборка, собранная на данных 2024 года, с сохранением всех ранее указанных параметров. В процессе применяется концепция почти идеальной траектории, позволяющая моделям учиться на максимально достоверных примерах действий и оценок. Этот этап важен для закрепления базовых стратегий и критериев принятия решений.

Третий этап — онлайн тонкая настройка моделей, которая проводится уже непосредственно в тестере стратегий на том же историческом интервале. Это позволяет адаптировать модели к изменяющимся рыночным условиям и уточнить параметры с максимальной точностью.

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

За период тестирования модель совершила 881 сделку, из которых 447 завершились с прибылью, что соответствует доле успешных сделок на уровне 50.74%. Это указывает на нейтральный баланс между прибыльными и убыточными сделками. Показатель фактора прибыли (Profit Factor) составляет 1.25.

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

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


Заключение

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

Отдельное внимание было уделено этапу самоконтролируемого контрастного обучения Энкодера, организованного в советнике StudyContrast.mq5. Мы отказались от статической выборки, реализовав динамическую загрузку данных из терминала и обогащение живыми примерами за последние пять лет по инструменту EURUSD М1. Положительные и отрицательные пары, сформированные благодаря мягкой аугментации и контролируемым шумам, позволили модели на практике учиться видеть сквозь рыночный шум.

Итоговое тестирование модели на периоде Января–Марта 2025 года продемонстрировало прибыльность реализованных решений. Стратегия имеет право на существование и показывает положительную динамику на тестовом отрезке, однако, нуждается в улучшении ряда параметров.


Ссылки


Программы, используемые в статье

# Имя Тип Описание
1 Research.mq5 Советник Советник сбора примеров
2 ResearchRealORL.mq5
Советник
Советник сбора примеров методом Real-ORL
3 StudyContrast.mq5 Советник Советник контрастного обучения Энкодера
4 Study.mq5 Советник Советник офлайн обучения моделей
5 StudyOnline.mq5
Советник
Советник онлайн обучения моделей
6 Test.mq5 Советник Советник для тестирования модели
7 Trajectory.mqh Библиотека класса Структура описания состояния системы и архитектуры моделей
8 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
9 NeuroNet.cl Библиотека Библиотека кода OpenCL-программы
Прикрепленные файлы |
MQL5.zip (2794.71 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Maxim Dmitrievsky
Maxim Dmitrievsky | 30 мая 2025 в 17:49
Странно, почему торгует только в одну сторону. А сигналы на закрытие сделок как формируются?
От начального до среднего уровня: Массив (IV) От начального до среднего уровня: Массив (IV)
В этой статье мы рассмотрим, как можно сделать нечто очень похожее на то, что реализовано в таких языках, как C, C++ и Java. Речь идет о передаче практически бесконечного числа параметров внутрь функции или процедуры. Хоть может показаться, что это довольно продвинутая тема, на мой взгляд, то, что здесь будет показано, сможет легко реализовать любой, кто понял предыдущие концепции. При условии, что они действительно были усвоены.
Переосмысливаем классические стратегии (Часть X): Может ли ИИ управлять MACD? Переосмысливаем классические стратегии (Часть X): Может ли ИИ управлять MACD?
Присоединяйтесь к нам, поскольку мы провели эмпирический анализ индикатора MACD, чтобы проверить, поможет ли применение искусственного интеллекта к стратегии, включая индикатор, повысить точность прогнозирования пары EURUSD. Мы одновременно оценивали, легче ли прогнозировать сам индикатор, чем цену, а также позволяет ли значение индикатора прогнозировать будущие уровни цен. Мы предоставим вам информацию, необходимую для принятия решения о том, стоит ли вам инвестировать свое время в интеграцию MACD в ваши торговые стратегии с использованием искусственного интеллекта.
Индикатор CAPM модели на рынке Forex Индикатор CAPM модели на рынке Forex
Адаптация классической модели CAPM для валютного рынка Forex в MQL5. Индикатор рассчитывает ожидаемую доходность и премию за риск на основе исторической волатильности. Показатели возрастают на пиках и впадинах, отражая фундаментальные принципы ценообразования. Практическое применение для контртрендовых и трендовых стратегий с учетом динамики соотношения риска и доходности в реальном времени. Включает математический аппарат и техническую реализацию.
Компоненты View и Controller для таблиц в парадигме MVC на MQL5: Простые элементы управления Компоненты View и Controller для таблиц в парадигме MVC на MQL5: Простые элементы управления
В статье рассмотрены простые элементы управления как составляющие части более сложных графических элементов компонента View в рамках реализации таблиц в парадигме MVC (Model-View-Controller). Реализован базовый функционал компонента Controller для интерактивного взаимодействия элементов с пользователем и друг с другом. Это вторая статья, посвященная компоненту View, и четвёртая в серии статей о создании таблиц для клиентского терминала MetaTrader 5.