preview
Нейросети в трейдинге: Интеллектуальный конвейер прогнозов (Окончание)

Нейросети в трейдинге: Интеллектуальный конвейер прогнозов (Окончание)

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

Введение

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

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

В первой статье, посвященной фреймворку Time-MoE, мы шаг за шагом разобрали, как превратить сухие идеи статьи авторской работы "Time-MoE: Billion-Scale Time Series Foundation Models with Mixture of Experts" в работающий код на MQL5. Мы создали модуль CNeuronSwiGLUOCL, который преобразовывает сырые данные в скрытые векторы, смешав две проекции.

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

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



Модуль внимания

Прежде чем приступить к построению полной архитектуры модели, нам предстоит внести один небольшой, но крайне важный штрих: заменить привычный блок FeedForward в архитектуре Transformer на созданный нами модуль разреженной смеси экспертов. Напомним, что в основе Time‑MoE лежит Decoder‑Only архитектура, в котором ключевую роль играет кросс‑внимание (Cross‑Attention), позволяющее декодеру учитывать информацию из контекстного слоя.

Для этого в качестве базового класса выбирается уже готовый компонент кросс‑внимания — CNeuronCrossDMHAttention — который предоставляет все необходимые механизмы. Наша задача — лишь подменить блок FeedForward на вызов CNeuronTimeMoESparseExperts. Структура нового объекта CNeuronTimeMoEAttention представлена ниже.

class CNeuronTimeMoEAttention   :  public CNeuronCrossDMHAttention
  {
protected:
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override
                            { return        feedForward(NeuronOCL); }
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
                     CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override
                            { return        calcInputGradients(NeuronOCL); }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override
                            { return        updateInputWeights(NeuronOCL); }

public:
                     CNeuronTimeMoEAttention(void)  {};
                    ~CNeuronTimeMoEAttention(void)  {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count,
                          uint window_cross, uint units_cross,
                          uint heads, uint layers,
                          uint experts, uint experts_dimension, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronTimeMoEAttention; }
  }; 

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

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

bool CNeuronTimeMoEAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint window, uint window_key, uint units_count,
                                   uint window_cross, uint units_cross, uint heads,
                                   uint layers, uint experts, uint experts_dimension,
                                   uint topK, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count,
                                                     optimization_type, batch))
      return false;

В теле метода первым делом вызываем одноименный метода базового нейронного слоя. Это позволяет создать скелет модуля. А дальше начинается самое интересное. Мы подготавливаем массив внутренних слоёв cLayers и привязываем его к нашему OpenCL‑интерфейсу.

cLayers.Clear();
cLayers.SetOpenCL(OpenCL);
CNeuronRelativeSelfAttention *attention = NULL;
CNeuronRelativeCrossAttention *cross = NULL;
CNeuronTimeMoESparseExperts *MoE = NULL;
bool use_self = units_count > 0;
int layer = 0;

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

Затем в цикле по заданному числу внутренних слоёв (layers) постепенно наращиваем единую цепочку. Если по основному информационному потоку идет более одного токена, то сначала создаём и инициализируем модуль Self-Attention, добавляя его в cLayers. Это позволяет декодеру самостоятельно взглянуть на уже сгенерированные токены, прежде чем переключиться на внешние источники контекста.

for(uint i = 0; i < layers; i++)
  {
   if(use_self)
     {
      attention = new CNeuronRelativeSelfAttention();
      if(!attention ||
         !attention.Init(0, layer, OpenCL, window, window_key, units_count, heads,
                                                          optimization, iBatch) ||
         !cLayers.Add(attention)
        )
        {
         delete attention;
         return false;
        }
      layer++;
     }

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

cross = new CNeuronRelativeCrossAttention();
if(!cross ||
   !cross.Init(0, layer, OpenCL, window, window_key, units_count, heads,
                     window_cross, units_cross, optimization, iBatch) ||
   !cLayers.Add(cross)
  )
  {
   delete cross;
   return false;
  }
layer++;

Но главная изюминка следует сразу за Cross‑Attention: мы создаём экземпляр CNeuronTimeMoESparseExperts. Именно здесь настраивается наш MoE‑блок: мы указываем число экспертов, размер их проекций (то есть сколько признаков каждый эксперт обрабатывает) и параметр topK, определяющий, сколько специалистов активируются в каждом проходе. И, разумеется, не забываем про связку с OpenCL‑контекстом.

 MoE = new CNeuronTimeMoESparseExperts();
 if(!MoE ||
    !MoE.Init(0, layer, OpenCL, window, experts_dimension, units_count, 1,
                                   experts, topK, optimization, iBatch) ||
    !cLayers.Add(MoE)
   )
   {
    delete MoE;
    return false;
   }
 layer++;
}

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

   SetOutput(MoE.getOutput(), true);
   SetGradient(MoE.getGradient(), true);
//---
   return true;
  }

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

Но есть один нюанс. Модуль кросс-внимания требует 2 информационных потока: основной и контекст. А в фреймворке Time-MoE мы видим лишь один. Этот вопрос решается минимальным вмешательством в основные механизмы. Мы лишь переопределяем методы прямого и обратного прохода, которые получая на вход один поток информации перенаправляют его в двух направлениях.

bool CNeuronTimeMoEAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
   return CNeuronCrossDMHAttention::feedForward(NeuronOCL, NeuronOCL.getOutput());
  }

На первый взгляд, такая операция превращает модуль Cross-Attention в Self-Attention. Но наша идея заключается в том, чтобы по основному информационному потоку передать лишь токены последнего временного шага. Это решается путем уменьшения количества анализируемых элементов по основному информационному потоку. А по информационному потоку контекста передаем полный набор информации, позволяя обогатить токены основного информационного потока всем историческим контекстом. Благодаря этому, сохраняются все преимущества Decoder‑Only архитектуры Time‑MoE: каждый новый токен спрашивает совет у экспертов о своём узком подпространстве и одновременно опирается на всю накопленную динамику рынка.

Полный код данного класса и всех его методов представлен во вложении.



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

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

Однако, на этом этапе возникает важный момент. Общий pipeline, который мы используем, предусматривает только один выход у модели — универсальный вектор признаков, используемый всеми компонентами: Actor, Director и Critic. В то же время авторская архитектура Time-MoE предполагает наличие нескольких прогнозных голов, каждая из которых работает с разным горизонтом планирования. Это создаёт потенциальный конфликт. Смешивать все выходы в одном буфере — значит усложнять обучение и терять интерпретируемость.

Мы решили не идти по пути объединения всего в одну структуру. Вместо этого был реализован чёткий логический раздел между Энкодером и прогнозными головами. Энкодер (Time-MoE) выступает как универсальный механизм извлечения признаков — он работает один для всех. А дальше для каждого горизонта планирования создаётся своя собственная модель, которая получает на вход результаты работы Энкодера и строит прогноз в рамках своей задачи.

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

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

Вся инициализация сосредоточена в методе CreateDescriptions, который собирает архитектуры по слоям, начиная с Энкодера.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&forecast1,
                        CArrayObj *&forecast2,
                        CArrayObj *&forecast3,
                        CArrayObj *&actor,
                        CArrayObj *&director,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!forecast1)
     {
      forecast1 = new CArrayObj();
      if(!forecast1)
         return false;
     }
   if(!forecast2)
     {
      forecast2 = new CArrayObj();
      if(!forecast2)
         return false;
     }
   if(!forecast3)
     {
      forecast3 = new CArrayObj();
      if(!forecast3)
         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;
   uint 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 = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

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

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

Следом идёт слой типа SwiGLU-эмбеддиинга. Он повышает глубину признаков и формирует начальную основу для абстрактного представления состояния.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronSwiGLUOCL;
   descr.count = Segments;
   descr.window = (prev_out + Segments - 1) / Segments;
   descr.variables = prev_count;
   prev_out = descr.window_out = EmbeddingSize;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.count;
   uint prev_var = descr.variables;

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

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeRCDOCL;
   descr.count = prev_var;
   descr.window = prev_count;
   descr.step = prev_out;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out * prev_var;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTimeMoEAttention;
   descr.window_out = EmbeddingSize / 4;
     {
      uint temp[] = {prev_out, prev_out, 8, TopK};               //Window Main, Window Cross, Experts dimension, TopK
      if(ArrayCopy(descr.windows, temp) < ArraySize(temp))
         return false;
     }
     {
      uint temp[] = {prev_var, prev_var * prev_count, NExperts}; //Units Main, Units Cross, Experts
      if(ArrayCopy(descr.units, temp) < ArraySize(temp))
         return false;
     }
   descr.layers = 6;
   descr.step = 4;                                                // Attention heads
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

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

//---
   CLayerDescription *latent = descr;
//--- Forecast 1
   forecast1.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = int(latent.windows[0] * latent.units[0]);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = latent.units[0];
   descr.window = latent.windows[0];
   descr.step = descr.window;
   descr.layers = 1;
   descr.window_out = 1;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = BarDescr;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

Дело в том, что архитектура BatchNorm включает на выходе два параметра: масштабирование (scale) и смещение (bias), которые могут быть обучаемыми. И хотя они традиционно применяются для стабилизации и ускорения обучения, в нашем случае они берут на себя иную роль — изучение обратного преобразования, приближающего прогнозные значения к исходному распределению. Таким образом, вместо хранения статистики нормализации, мы позволяем модели самой выучить, как денормализовать данные, подстраиваясь под реальные условия.

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

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNorm;
   descr.count = BarDescr;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }

Всё построено максимально эффективно: константное окно, один слой, фиксированная длина.

Вторая и третья прогнозные модели (forecast2, forecast3) использует те же исходные данные, что и первая. И практически полностью повторяют её архитектуру, но отличаются увеличенным окном прогноза. Параметры прогнозного свёрточного слоя копируются из первой головы, но количество фильтров расширяется до требуемого горизонта планирования.

//--- Forecast 2
   forecast2.Clear();
//--- Input layer
   if(!forecast2.Add(forecast1.At(0)))
      return false;
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   if(!descr.Copy(forecast1.At(1)))
     {
      delete descr;
      return false;
     }
   prev_out = descr.window_out = NForecast / 2;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.count; 

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

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

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

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.window = prev_out;
   descr.count = prev_count;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.window = prev_count;
   descr.step = prev_count;
   prev_count = descr.count = prev_out;
   descr.layers = 1;
   prev_out = descr.window_out = BarDescr;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }

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

Заканчивается всё финальной пакетной нормализацией, восстанавливающей исходное распределение данных.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.activation = None;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }

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

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

Далее идёт модель Actor, то есть, исполнитель стратегии. На вход она получает дескрипторы текущего состояния счёта. Полученные данные проходят стандартную нормализацию.

//--- 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 = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
     {
      uint temp[] = {AccountDescr,         // Inputs window
                    latent.windows[0]     // Cross window
                   };
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
     {
      uint temp[] = {1,                 // Inputs units
                    latent.units[0]    // Cross units
                   };
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;                  // Heads
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.layers = 3;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

После внимания следует цепочка из трёх полносвязных слоёв, который преобразует полученные данные в вероятности действий (NActions). Именно этот выход используется при генерации торговых решений.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = BatchSize;
   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 = BatchSize;
   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 = BatchSize;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

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

Архитектуры моделей Critic и Director строятся по аналогии с Актёром. Различие лишь в том, что на вход подаётся не состояние счёта, а вектор действий, сгенерированный самим Актёром. А на выходе формируется числовая оценка этих действий — в случае Критика это значение value-функции, а у Режиссёра — сигнальный градиент бинарной классификации (хорошее/плохое действие). Внутренняя структура — те же нормализационные блоки, механизм перекрёстного внимания и каскад выходных слоёв. Чтобы не загромождать текст повторением уже описанных деталей, мы не будем останавливаться на них подробно. Полная архитектура всех компонентов представлена во вложении.



Обучение моделей

Следующим этапом нашей работы становится обучение моделей. И здесь нас поджидает один из ключевых вызовов. Дело в том, что авторы оригинального фреймворка Time-MoE обучали свои нейросети на массивной, если не сказать титанической, выборке Time-300B. Эта выборка охватывает более 300 миллиардов временных точек из девяти разных предметных областей, включая экономику, энергетику, транспорт, здравоохранение и другие домены. Такой объём данных обеспечивает впечатляющую обобщающую способность модели — но и требует ресурсов, недоступных в рамках локального или полуавтоматизированного обучения.

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

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

Суть механизма заключается в обучении агента на псевдореальных данных. Где реальная история котировок получается из терминала, а оценка действий осуществляется смоделированной средой. Весь процесс построен в рамках метода Train советника "…\Experts\TimeMoE\Study.mq5". Именно отсюда начинается пошаговая обработка исторических данных, формирование обучающих пакетов, сбор контекста торгового счёта и последовательное обучение модели на основе обратного распространения ошибки.

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

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;
     }
//---
   int count = -1;
   bool calculated = false;
   do
     {
      count++;
      calculated = (RSI.BarsCalculated() >= bars &&
                    CCI.BarsCalculated() >= bars &&
                    ATR.BarsCalculated() >= bars &&
                    MACD.BarsCalculated() >= bars
                   );
      Sleep(100);
      count++;
     }
   while(!calculated && count < 100);
   if(!calculated)
     {
      PrintFormat("%s -> %d The training data has not been loaded", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
//---
   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars + NForecast;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }

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

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

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

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

const vector<float> account = SampleAccount(GetPointer(bState), datetime(bTime[0]));
if(!bAccount.AssignArray(account))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

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

После формирования исходных данных, начинается прямой проход модели: Encoder кодирует рынок, после чего три блока Forecast[i] строят прогнозы по разным временным горизонтам.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
for(uint f = 0; f < caForecast.Size(); f++)
   if(!caForecast[f].feedForward(GetPointer(cEncoder), -1, (CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f);
      Stop = true;
      break;
     }

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

if(!cActor.feedForward(GetPointer(bAccount), 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cCritic.feedForward(GetPointer(cActor), -1, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cDirector.feedForward(GetPointer(cActor), -1, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

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

//--- Study
for(uint f = 0; f < caForecast.Size(); f++)
   if(!caForecast[f].backProp(Result, (CBufferFloat*)NULL) ||
      !cEncoder.backPropGradient((CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f);
      Stop = true;
      break;
     }

Для того, чтобы получить объективную оценку действия, используется функция CheckAction. Она симулирует открытие виртуальной позиции и рассчитывает ожидаемую прибыль с учетом коэффициента дисконтирования на основе реальных исторических данных. На основе этих данных формируется награда (reward), которая возвращается в модель и становится основой для пересчёта параметров всех компонентов — Actor, Critic, Director.

cActor.getResults(Action);
double equity = bAccount[2] * bAccount[0] * EtalonBalance / (1 + bAccount[1]);
double reward = CheckAction(Action, Result, equity);
Result.Clear();
if(!Result.Add(float(reward)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cCritic.backProp(Result, GetPointer(cEncoder), LatentLayer) ||
   !cActor.backPropGradient(GetPointer(cEncoder), LatentLayer, -1, true))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!Result.Update(0, float(reward > 0)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cDirector.backProp(Result, GetPointer(cEncoder), LatentLayer) ||
   !cActor.backPropGradient(GetPointer(cEncoder), LatentLayer, -1, true))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  } 

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

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

 if(GetTickCount() - ticks > 500)
   {
    double percent = double(iter) * 100.0 / (Iterations);
    string str = "";
    for(uint f = 0; f < caForecast.Size(); f++)
       str += StringFormat("%-12s%d %6.2f%% -> Error %15.8f\n", "Forecast", f, percent,
                                                caForecast[f].getRecentAverageError());
    str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Critic", percent, 
                                                      cCritic.getRecentAverageError());
    str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Director", percent, 
                                                    cDirector.getRecentAverageError());
    Comment(str);
    ticks = GetTickCount();
   }
}

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

   Comment("");
//---
   for(uint f = 0; f < caForecast.Size(); f++)
      PrintFormat("%s -> %d -> %-15s%d %10.7f", __FUNCTION__, __LINE__, "Forecast", f,
                                               caForecast[f].getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Critic", 
                                                     cCritic.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Director", 
                                                   cDirector.getRecentAverageError());
   ExpertRemove();
//---
  }

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

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

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



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

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

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

Затем — онлайн-настройка. Она проводится в тестере стратегий MetaTrader 5. Здесь модель взаимодействует с историей уже в реалистичном режиме: свеча за свечой, с рыночным шумом, случайными колебаниями и нестабильностью. Это помогает адаптировать поведение агента под живую динамику и скорректировать стратегию в условиях, приближённых к реальным.

После обучения модель протестировали на новых данных — котировках за Январь 2025 года. Все настройки были зафиксированы заранее и не менялись. Это гарантирует объективность и прозрачность оценки. Результаты теста приведены ниже.

Результаты тестирования выглядят неоднозначно, и их стоит оценивать с осторожностью. С одной стороны, совокупная доходность традиционно важна: при начальном депозите $100 система завершила период с чистой прибылью $1 209. Это в 12 раз превышает стартовый капитал. График баланса демонстрирует уверенный рост до середины месяца, а затем относительную стабилизацию на уровне $1 350–1 400.

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

Показатель Profit Factor составил 1.49 — для многих это считается приемлемым уровнем, однако при таких просадках он едва ли компенсирует возможные периоды убыточности. Recovery Factor (отношение чистой прибыли к максимальной просадке) почти равен 1, что говорит об очень медленном восстановлении после потерь.

Статистика сделок показывает, что за месяц было открыто почти 2 .5К сделок, из которых 53.98 % оказались прибыльными. Средний выигрыш лишь слегка превосходит средний убыток.

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


Заключение

В данной работе мы шаг за шагом перенесли ключевые идеи фреймворка Time‑MoE из академической статьи в реальный код на MQL5 и OpenCL. Мы реализовали SwiGLU‑эмбеддинг, сконструировали разреженную смесь экспертов и встроили её в механизм кросс‑внимания. Организовали полный конвейер обучения внутри архитектуры Actor–Director–Critic. Благодаря чёткой модульной структуре, все компоненты оказались взаимосвязаны, но при этом легко настраиваемы и расширяемы.

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

Тестирование на Январских котировках 2025 года показало, что Агент способен приносить стабильную прибыль, но сопровождается глубокими просадками. Это указывает на необходимость дальнейшей оптимизации риск‑менеджмента и тонкой настройки критериев торговых операций.


Ссылки


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

#ИмяТипОписание
1Study.mq5СоветникСоветник офлайн обучения моделей
2StudyOnline.mq5
Советник
Советник онлайн обучения моделей
3Test.mq5СоветникСоветник для тестирования модели
4Trajectory.mqhБиблиотека классаСтруктура описания состояния системы и архитектуры моделей
5NeuroNet.mqhБиблиотека классаБиблиотека классов для создания нейронной сети
6NeuroNet.clБиблиотекаБиблиотека кода OpenCL-программы

Прикрепленные файлы |
MQL5.zip (2856.61 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (1)
Maxim Dmitrievsky
Maxim Dmitrievsky | 19 июн. 2025 в 08:11
Почему-то опять сделки в одну сторону, как будто агент ничему на самом деле не учится. Интересно было бы увидеть баланс на трен. данных. Нет opencl, не могу проверить :)
Наблюдатель Connexus (Часть 8): Добавление Request Observer (Наблюдатель запросов) Наблюдатель Connexus (Часть 8): Добавление Request Observer (Наблюдатель запросов)
В этой заключительной части нашей серии библиотеки Connexus мы рассмотрели реализацию паттерна Наблюдатель, а также основные рефакторинги в путях к файлам и именах методов. В этой серии представлена вся разработка Connexus, предназначенная для упрощения HTTP-взаимодействия в сложных приложениях.
Возможности Мастера MQL5, которые вам нужно знать (Часть 46): Ишимоку Возможности Мастера MQL5, которые вам нужно знать (Часть 46): Ишимоку
Ichimuko Kinko Hyo — известный японский индикатор, представляющий собой систему определения тренда. Как и в предыдущих статьях, мы рассмотрим этот индикатор с использованием паттернов и поделимся стратегиями и отчетами о тестировании, применив классы библиотеки Мастера MQL5.
Стратегия орла — Eagle Strategy (ES) Стратегия орла — Eagle Strategy (ES)
Eagle Strategy — алгоритм, имитирующий двухфазную охотничью стратегию орла: глобальный поиск через полеты Леви методом Мантенья, чередуется с интенсивной локальной эксплуатацией светлячкового алгоритма, математически обоснованный подход к балансу между исследованием и эксплуатацией, а также биоинспирированная концепция, объединяющая два природных феномена в единый вычислительный метод.
Загрузка данных Международного валютного фонда на Python Загрузка данных Международного валютного фонда на Python
Загрузка данных Международного валютного фонда на Python: добываем данные IMF для применения в макроэкономических валютных стратегиях. Как макроэкономика может помочь трейдеру и алготрейдеру?