preview
Нейросети в трейдинге: Использование языковых моделей для прогнозирования временных рядов

Нейросети в трейдинге: Использование языковых моделей для прогнозирования временных рядов

MetaTrader 5Торговые системы | 29 июля 2024, 14:35
549 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

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

Между тем, появление базовых моделей в обработке естественного языка (NLP) и компьютерном зрении (CV), ознаменовало важные вехи в эффективном обучении представлений. Предварительное обучение базовых моделей временных рядов с использованием огромных объемов данных способствует улучшению производительности в последующих задачах. Кроме того, большие языковые модели предоставляют способ использовать существующие представления модели, полученные во время предварительного обучения вместо того, чтобы требовать обучения с нуля. Однако существующие базовые структуры и методы в языковых моделях не полностью улавливают эволюцию временных паттернов, которые являются основополагающими для моделирования временных рядов.

Авторы статьи "TEMPO: Prompt-based Generative Pre-trained Transformer for Time Series Forecasting" решают актуальные задачи адаптации больших предварительно обученных моделей для задач прогнозирования временных рядов и предлагают комплексную модель на основе GPT, которая получила название TEMPO. Она состоит из двух ключевых аналитических компонентов для эффективного обучения представлений временных рядов. Один фокусируется на моделировании специфических паттернов временных рядов, таких как тренды и сезонность. А другой сосредотачивается на получении более универсальных и передаваемых инсайтов из внутренних свойств данных с помощью подхода на основе подсказок. В частности, TEMPO сначала декомпозирует исходные данные мультимодального временных рядов на три компоненты: тренд, сезонность и остатки. Каждая из этих компонент затем отображается в соответствующее скрытое пространство для построения исходного эмбединга временного ряда для GPT.

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

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


1. Алгоритм TEMPO

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В качестве альтернативного варианта возможно создание отдельных блоков GPT для работы с различными типами компонентов временных рядов.

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

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

2. Реализация средствами MQL5

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

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

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

Поступающие на вход модели исходные данные проходят деление на компоненты: тренд, сезонность и остаток. Для извлечения тренда авторы метода использовали подсчет среднего значения исходных данных с помощью скользящего окна. Что в целом напоминает стандартный индикатор Moving Average. В своей реализации я предпочел использование ранее рассмотренного метода PLR. На мой взгляд это более информативный метод, способный идентифицировать тенденции различной длины. Однако результаты работы данного метода нельзя напрямую вычесть из исходного ряда. Здесь необходима доработка алгоритма, о которой мы поговорим более детально в процессе реализации.

Второй момент — извлечение сезонности. Думаю, здесь очевидное решение использования частотного спектра. Но как вы знаете, DFT способен полностью представить исходный временной ряд в частотной области. И обратное преобразование iDFT вернет исходный временной ряд без искажений. Для отделения сезонной составляющей исходного временного ряда от шума и выбросов нам необходимо отсечь некоторые частоты спектра. Тут становится вопрос об объеме и списке обнуляемых частот. На этот вопрос нет однозначного ответа. Мы уже обсуждали подобные вопросы при прогнозировании временных рядов в частотной области. Но на этот раз я подошел к решению вопроса несколько с иной стороны. В анализе данных мы используем мультимодальный временной ряд, который относится к одному финансовому инструменту. И вполне ожидаемо, что циклы отдельных компонент будут согласованы между собой. Так почему бы нам не воспользоваться механизмом Self-Attention для выявления согласованных частот в спектрах отдельных унитарных временных рядов. Мы ожидаем, что согласованные частоты спектра выделят сезонную составляющую.

Таким образом мы можем разделить исходные данные на отдельные компоненты, предусмотренные методом TEMPO. Работа выстраиваемой модели частично проясняется. Для разбиения унитарных моделей на отдельные сегменты и их эмбединга у нас уже есть готовое решение. То же можно сказать и об архитектурных решениях на базе Transformer. Но есть вопрос о "подсказках". Авторы метода предлагают использовать подсказки, способные подтолкнуть модель GPT к генерации последовательности в ожидаемом контексте. И в данной работе я решил в качестве "подсказок" использовать результаты работы PLR.

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

Мы обсудили основные моменты выстраиваемой модели и теперь можем переходить к практической работе.

2.1 Дополнение OpenCL программы

А начнем мы свою работу с создания новых кернелов на стороне OpenCL программы. Как было сказано выше, для выделения основных тенденций из мультимодального временного ряда исходных данных мы будем использовать метод кусочно-линейного представления (PLR), который предполагает представление каждого сегмента в виде 3 значений: угол наклона линии, смещение и размер сегмента. Очевидно, что, имея такое представление временного ряда, довольно сложно вычесть тенденции из исходных данных. Тем не менее это возможно. Для реализации такого функционала мы создадим кернел CutTrendAndOther. В параметрах данный кернел получает 4 указателя на буфера данных. 2 из них содержат исходные данные в виде тензора исходного временного ряда (inputs) и тензора кусочно-линейного представления данных (plr). Результаты операций мы сохраним в 2 других буфера:

  • trend — тенденции в виде обычного временного ряда;
  • other — разница значений между исходными данными и линией тренда.

__kernel void CutTrendAndOther(__global const float *inputs,
                               __global const float *plr,
                               __global float *trend,
                               __global float *other
                              )
  {
   const size_t i = get_global_id(0);
   const size_t lenth = get_global_size(0);
   const size_t v = get_global_id(1);
   const size_t variables = get_global_size(1);

Данный кернел мы планируем вызывать в 2 мерном пространстве задач. Первое измерение представляет размер последовательности исходных данных, а второе — количество анализируемых переменных (унитарных последовательностей). В теле кернела мы идентифицируем текущий поток во всех измерениях пространства задач.

После чего определим необходимые константы.

//--- constants
   const int shift_in = i * variables + v;
   const int step_in = variables;
   const int shift_plr = v;
   const int step_plr = 3 * step_in;

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

//--- calc position
   int pos = -1;
   int prev_in = 0;
   int dist = 0;
   do
     {
      pos++;
      prev_in += dist;
      dist = (int)fmax(plr[shift_plr + pos * step_plr + 2 * step_in] * lenth, 1);
     }
   while(!(prev_in <= i && (prev_in + dist) > i));

На основании параметров найденного сегмента мы определим значение линии тенденции в текущей точке и её отклонение от значения исходного временного ряда.

//--- calc trend
   float sloat = plr[shift_plr + pos * step_plr];
   float intercept = plr[shift_plr + pos * step_plr + step_in];
   pos = i - prev_in;
   float trend_i = sloat * pos + intercept;
   float other_i = inputs[shift_in] - trend_i;

И теперь нам остается лишь сохранить полученные значения в соответствующие элементы глобальных буферов результатов.

//--- save result
   trend[shift_in] = trend_i;
   other[shift_in] = other_i;
  }

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

__kernel void CutTrendAndOtherGradient(__global float *inputs_gr,
                                       __global const float *plr,
                                       __global float *plr_gr,
                                       __global const float *trend_gr,
                                       __global const float *other_gr
                                      )
  {
   const size_t i = get_global_id(0);
   const size_t lenth = get_global_size(0);
   const size_t v = get_global_id(1);
   const size_t variables = get_global_size(1);

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

//--- constants
   const int shift_in = i * variables + v;
   const int step_in = variables;
   const int shift_plr = v;
   const int step_plr = 3 * step_in;

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

//--- calc position
   int pos = -1;
   int prev_in = 0;
   int dist = 0;
   do
     {
      pos++;
      prev_in += dist;
      dist = (int)fmax(plr[shift_plr + pos * step_plr + 2 * step_in] * lenth, 1);
     }
   while(!(prev_in <= i && (prev_in + dist) > i));

Но на этот раз, мы рассчитываем градиенты ошибок параметров сегмента.

//--- get gradient
   float other_i_gr = other_gr[shift_in];
   float trend_i_gr = trend_gr[shift_in] - other_i_gr;
//--- calc plr gradient
   pos = i - prev_in;
   float sloat_gr = trend_i_gr * pos;
   float intercept_gr = trend_i_gr;

И сохраняем результаты в буфера данных.

//--- save result
   plr_gr[shift_plr + pos * step_plr] += sloat_gr;
   plr_gr[shift_plr + pos * step_plr + step_in] += intercept_gr;
   inputs_gr[shift_in] = other_i_gr;
  }

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

  • выделение тенденций, как реализовано в выше представленном кернеле;
  • в качестве "подсказок" модели внимания, о чем упоминалось выше.

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

Кроме того, мы создали кернелы CutOneFromAnother и CutOneFromAnotherGradient для разделения сезонной составляющей и прочих данных. Алгоритм данных кернелов очень прост и состоит буквально из 2-3 строк кода. Я думаю, что Вам не составит труда разобраться с ним самостоятельно. А полный код всех программ, используемых при подготовке данной статьи, представлен во вложении.

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

2.2 Создание класса метода TEMPO

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

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

class CNeuronTEMPOOCL   :  public CNeuronBaseOCL
  {
protected:
   //--- constants
   uint              iVariables;
   uint              iSequence;
   uint              iForecast;
   uint              iFFT;
   //--- Trend
   CNeuronPLROCL     cPLR;
   CNeuronBaseOCL    cTrend;
   //--- Seasons
   CNeuronBaseOCL    cInputSeasons;
   CNeuronTransposeOCL cTranspose[2];
   CBufferFloat      cInputFreqRe;
   CBufferFloat      cInputFreqIm;
   CNeuronBaseOCL    cInputFreqComplex;
   CNeuronBaseOCL    cNormFreqComplex;
   CBufferFloat      cMeans;
   CBufferFloat      cVariances;
   CNeuronComplexMLMHAttention cFreqAtteention;
   CNeuronBaseOCL    cUnNormFreqComplex;
   CBufferFloat      cOutputFreqRe;
   CBufferFloat      cOutputFreqIm;
   CNeuronBaseOCL    cOutputTimeSeriasRe;
   CBufferFloat      cOutputTimeSeriasIm;
   CBufferFloat      cZero;
   //--- Noise
   CNeuronBaseOCL    cResidual;
   //--- Forecast
   CNeuronBaseOCL    cConcatInput;
   CNeuronBatchNormOCL cNormalize;
   CNeuronPatching   cPatching;
   CNeuronBatchNormOCL cNormalizePLR;
   CNeuronPatching   cPatchingPLR;
   CNeuronPositionEncoder acPE[2];
   CNeuronMLCrossAttentionMLKV cAttention;
   CNeuronTransposeOCL  cTransposeAtt;
   CNeuronConvOCL    acForecast[2];
   CNeuronTransposeOCL  cTransposeFrc;
   CNeuronRevINDenormOCL cRevIn;
   CNeuronConvOCL    cSum;
   //--- Complex functions
   virtual bool      FFT(CBufferFloat *inp_re, CBufferFloat *inp_im, 
                         CBufferFloat *out_re, CBufferFloat *out_im, bool reverse = false);
   virtual bool      ComplexNormalize(void);
   virtual bool      ComplexUnNormalize(void);
   virtual bool      ComplexNormalizeGradient(void);
   virtual bool      ComplexUnNormalizeGradient(void);
   //---
   bool              CutTrendAndOther(CBufferFloat *inputs);
   bool              CutTrendAndOtherGradient(CBufferFloat *inputs_gr);
   bool              CutOneFromAnother(void);
   bool              CutOneFromAnotherGradient(void);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   //---
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   //---

public:
                     CNeuronTEMPOOCL(void)   {};
                    ~CNeuronTEMPOOCL(void)   {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint sequence, uint variables, uint forecast, uint heads, uint layers, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronTEMPOOCL;   }
   //---
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual CBufferFloat   *getWeights(void)  override;
  };

Обратите внимание, что несмотря на большое разнообразие вложенных объектов, все они объявлены статично. Это позволяет нам оставить пустыми конструктор и деструктор класса. А всю работу по очистке памяти после удаления объекта класса переложить на систему.

Непосредственная инициализация вложенных переменных и объектов класса реализована в методе Init. Как обычно, в параметрах метода мы получаем основные параметры, позволяющие однозначно определить архитектуру создаваемого слоя. Здесь мы видим уже знакомые нам параметры:

  • sequence — размер анализируемой последовательности мультимодального временного ряда;
  • variables — количество анализируемых переменных (унитарных последовательностей);
  • forecast — глубина планирования прогнозных значений;
  • heads — количество голов внимания в используемых механизмах Self-Attention;
  • layers — количество слоев в блоках внимания.

bool CNeuronTEMPOOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                           uint sequence, uint variables, uint forecast, uint heads, uint layers, 
                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
//--- base
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, forecast * variables, 
                                                          optimization_type, batch))
      return false;

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

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

//--- constants
   iVariables = variables;
   iForecast = forecast;
   iSequence = MathMax(sequence, 1);

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

//--- Calculate FFTsize
   uint size = iSequence;
   int power = int(MathLog(size) / M_LN2);
   if(MathPow(2, power) < size)
      power++;
   iFFT = uint(MathPow(2, power));

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

//--- trend
   if(!cPLR.Init(0, 0, OpenCL, iVariables, iSequence, true, optimization, iBatch))
      return false;

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

   if(!cTrend.Init(0, 1, OpenCL, iSequence * iVariables, optimization, iBatch))
      return false;

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

//--- seasons
   if(!cInputSeasons.Init(0, 2, OpenCL, iSequence * iVariables, optimization, iBatch))
      return false;

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

   if(!cTranspose[0].Init(0, 3, OpenCL, iSequence, iVariables, optimization, iBatch))
      return false;
   if(!cTranspose[1].Init(0, 4, OpenCL, iVariables, iSequence, optimization, iBatch))
      return false;

Результаты частотного разложения сигнала мы будем сохранять в 2 буфера данных. Один для вещественной, а второй для мнимой части сигнала.

   if(!cInputFreqRe.BufferInit(iFFT * iVariables, 0) || !cInputFreqRe.BufferCreate(OpenCL))
      return false;
   if(!cInputFreqIm.BufferInit(iFFT * iVariables, 0) || !cInputFreqIm.BufferCreate(OpenCL))
      return false;

А вот для работы блока внимания в частотном домене нам потребуется конкатенировать 2 буфера данных в один объект.

   if(!cInputFreqComplex.Init(0, 5, OpenCL, iFFT * iVariables * 2, optimization, batch))
      return false;

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

   if(!cNormFreqComplex.Init(0, 6, OpenCL, iFFT * iVariables * 2, optimization, batch))
      return false;
   if(!cMeans.BufferInit(iVariables, 0) || !cMeans.BufferCreate(OpenCL))
      return false;
   if(!cVariances.BufferInit(iVariables, 0) || !cVariances.BufferCreate(OpenCL))
      return false;

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

   if(!cFreqAtteention.Init(0, 7, OpenCL, iFFT, 32, heads, iVariables, layers, optimization, batch))
      return false;

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

После выделения ключевых частотных характеристик мы выполняем обратные операции. Сначала вернем частоты в исходное распределение.

   if(!cUnNormFreqComplex.Init(0, 8, OpenCL, iFFT * iVariables * 2, optimization, batch))
      return false;

Затем выделим вещественную и мнимую часть сигнала в отдельные буферы данных.

   if(!cOutputFreqRe.BufferInit(iFFT * iVariables, 0) || !cOutputFreqRe.BufferCreate(OpenCL))
      return false;
   if(!cOutputFreqIm.BufferInit(iFFT * iVariables, 0) || !cOutputFreqIm.BufferCreate(OpenCL))
      return false;

И преобразуем их во временную область.

   if(!cOutputTimeSeriasRe.Init(0, 9, OpenCL, iFFT * iVariables, optimization, iBatch))
      return false;
   if(!cOutputTimeSeriasIm.BufferInit(iFFT * iVariables, 0) || 
      !cOutputTimeSeriasIm.BufferCreate(OpenCL))
      return false;

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

   if(!cZero.BufferInit(iFFT * iVariables, 0) || !cZero.BufferCreate(OpenCL))
      return false;

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

//--- Noise
   if(!cResidual.Init(0, 10, OpenCL, iSequence * iVariables, optimization, iBatch))
      return false;

После разделения сигнала исходных данных на 3 компоненты мы переходим к следующему этапу алгоритма TEMPO — прогнозированию последующих значений. Здесь мы сначала конкатенируем данные 3-х компонент в единый тензор.

//--- Forecast
   if(!cConcatInput.Init(0, 11, OpenCL, 3 * iSequence * iVariables, optimization, iBatch))
      return false;

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

   if(!cNormalize.Init(0, 12, OpenCL, 3 * iSequence * iVariables, iBatch, optimization))
      return false;

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

   int window = MathMin(5, (int)iSequence - 1);
   int patches = (int)iSequence - window + 1;
   if(!cPatching.Init(0, 13, OpenCL, window, 1, 8, patches, 3 * iVariables, optimization, iBatch))
      return false;
   if(!acPE[0].Init(0, 14, OpenCL, patches, 3 * 8 * iVariables, optimization, iBatch))
      return false;

И добавим к полученным сегментам позиционное кодирование.

Аналогичные операции мы выполним для кусочно-линейного представления исходного временного ряда.

   int plr = cPLR.Neurons();
   if(!cNormalizePLR.Init(0, 15, OpenCL, plr, iBatch, optimization))
      return false;
   plr = MathMax(plr/(3 * (int)iVariables),1);
   if(!cPatchingPLR.Init(0, 16, OpenCL, 3, 3, 8, plr, iVariables, optimization, iBatch))
      return false;
   if(!acPE[1].Init(0, 17, OpenCL, plr, 8 * iVariables, optimization, iBatch))
      return false;

И инициализируем слой кросс-внимания, который проанализирует разложенный нами сигнал на 3 компоненты в контексте кусочно-линейного представления исходного временного ряда.

   if(!cAttention.Init(0, 18, OpenCL, 3 * 8 * iVariables, 3 * iVariables, MathMax(heads, 1), 
                       8 * iVariables, MathMax(heads / 2, 1), patches, plr, MathMax(layers, 1), 
                       2, optimization, iBatch))
      return false;

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

   if(!cTransposeAtt.Init(0, 19, OpenCL, patches, 3 * 8 * iVariables, optimization, iBatch))
      return false;

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

   if(!acForecast[0].Init(0, 20, OpenCL, patches, patches, iForecast, 3 * 8 * iVariables, 
                                                                      optimization, iBatch))
      return false;
   acForecast[0].SetActivationFunction(LReLU);

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

   if(!acForecast[1].Init(0, 21, OpenCL, 8 * iForecast, 8 * iForecast, iForecast, 3 * iVariables, 
                                                                            optimization, iBatch))
      return false;
   acForecast[1].SetActivationFunction(TANH);

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

   if(!cTransposeFrc.Init(0, 22, OpenCL, 3 * iVariables, iForecast, optimization, iBatch))
      return false;

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

   if(!cRevIn.Init(0, 23, OpenCL, 3 * iVariables * iForecast, 11, GetPointer(cNormalize)))
      return false;

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

   if(!cSum.Init(0, 24, OpenCL, 3, 3, 1, iVariables, iForecast, optimization, iBatch))
      return false;
   cSum.SetActivationFunction(None);

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

   SetActivationFunction(None);
   SetOutput(cSum.getOutput(), true);
   SetGradient(cSum.getGradient(), true);
//---
   return true;
  }

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

После инициализации объекта мы переходим к следующему этапу — построению алгоритма прямого прохода. Должен сказать, что для реализации прямого прохода были построены ряд методов постановки в очередь выполнения выше описанных кернелов. Алгоритм подобных методов Вам уже знаком. И новые методы не используют каких-либо конструктивных особенностей. Поэтому подобные методы мы оставим для самостоятельного изучения. Как Вы помните, полный код данного класса и всех его методов представлен во вложении. А я предлагаю посмотреть на реализацию основного алгоритма прямого прохода в методе CNeuronTEMPOOCL::feedForward.

bool CNeuronTEMPOOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- trend
   if(!cPLR.FeedForward(NeuronOCL))
      return false;

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

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

После определения тенденций мы вычтем их влияние из исходных данных.

   if(!CutTrendAndOther(NeuronOCL.getOutput()))
      return false;

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

   if(!cTranspose[0].FeedForward(cInputSeasons.AsObject()))
      return false;

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

   if(!FFT(cTranspose[0].getOutput(), NULL,GetPointer(cInputFreqRe),GetPointer(cInputFreqIm),false))
      return false;

Конкатенируем вещественную и мнимую часть частотных характеристик в единый тензор.

   if(!Concat(GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), cInputFreqComplex.getOutput(),
                                                                           1, 1, iFFT * iVariables))
      return false;

И нормализуем полученные значения.

   if(!ComplexNormalize())
      return false;

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

   if(!cFreqAtteention.FeedForward(cNormFreqComplex.AsObject()))
      return false;

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

   if(!ComplexUnNormalize())
      return false;
   if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
                cUnNormFreqComplex.getOutput(), 1, 1, iFFT * iVariables))
      return false;
   if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
           GetPointer(cInputFreqRe), GetPointer(cOutputTimeSeriasIm), true))
      return false;
   if(!DeConcat(cOutputTimeSeriasRe.getOutput(), cOutputTimeSeriasRe.getGradient(), 
                GetPointer(cInputFreqRe), iSequence, iFFT - iSequence, iVariables))
      return false;
   if(!cTranspose[1].FeedForward(cOutputTimeSeriasRe.AsObject()))
      return false;

После чего выделим значение 3 компоненты.

//--- Noise
   if(!CutOneFromAnother())
      return false;

Выделив 3 компоненты из временного ряда, мы конкатенируем их в единый тензор.

//--- Forecast
   if(!Concat(cTrend.getOutput(), cTranspose[1].getOutput(), cResidual.getOutput(),
              cConcatInput.getOutput(), 1, 1, 1, 3 * iSequence * iVariables))
      return false;

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

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

   if(!cNormalize.FeedForward(cConcatInput.AsObject()))
      return false;

Нормализованные данные мы разделим на сегменты и создадим к ним эмбединги.

   if(!cPatching.FeedForward(cNormalize.AsObject()))
      return false;

После чего добавим позиционное кодирование для однозначной идентификации позиции каждого элемента в тензоре.

   if(!acPE[0].FeedForward(cPatching.AsObject()))
      return false;

Аналогичным образом подготовим данные кусочно-линейного представления временного ряда. Сначала нормализуем их.

   if(!cNormalizePLR.FeedForward(cPLR.AsObject()))
      return false;

А затем разобьем на сегменты и добавим позиционное кодирование.

   if(!cPatchingPLR.FeedForward(cPatchingPLR.AsObject()))
      return false;
   if(!acPE[1].FeedForward(cPatchingPLR.AsObject()))
      return false;

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

   if(!cAttention.FeedForward(acPE[0].AsObject(), acPE[1].getOutput()))
      return false;

Затем мы транспонируем данные.

   if(!cTransposeAtt.FeedForward(cAttention.AsObject()))
      return false;

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

   if(!acForecast[0].FeedForward(cTransposeAtt.AsObject()))
      return false;
   if(!acForecast[1].FeedForward(acForecast[0].AsObject()))
      return false;

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

Вернем прогнозные данные в исходное представление.

   if(!cTransposeFrc.FeedForward(acForecast[1].AsObject()))
      return false;

И добавим параметры статистического распределения исходных данных, которые были изъяты при нормализации конкатенированного тензора компонент.

   if(!cRevIn.FeedForward(cTransposeFrc.AsObject()))
      return false;

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

   if(!cSum.FeedForward(cRevIn.AsObject()))
      return false;
//---
   return true;
  }

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

Как Вы знаете, в нашей реализации обратный проход, обычно, строится из 2 методов:

  • calcInputGradients — распределения градиента ошибки до всех элементов в соответствии с их влиянием на общий результат;
  • updateInputWeights — корректировка параметров модели с целью минимизации ошибки.

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

bool CNeuronTEMPOOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//--- Devide to Trend, Seasons and Noise
   if(!cRevIn.calcHiddenGradients(cSum.AsObject()))
      return false;

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

//--- Forecast gradient
   if(!cTransposeFrc.calcHiddenGradients(cRevIn.AsObject()))
      return false;

Затем пропустим градиент ошибки через MLP прогнозирования последовательности.

   if(!acForecast[1].calcHiddenGradients(cTransposeFrc.AsObject()))
      return false;
   if(acForecast[1].Activation() != None &&
      !DeActivation(acForecast[1].getOutput(), acForecast[1].getGradient(),
                    acForecast[1].getGradient(), acForecast[1].Activation())
     )
      return false;
   if(!acForecast[0].calcHiddenGradients(acForecast[1].AsObject()))
      return false;

И блок кросс-внимания.

//--- Attention gradient
   if(!cTransposeAtt.calcHiddenGradients(acForecast[0].AsObject()))
      return false;
   if(!cAttention.calcHiddenGradients(cTransposeAtt.AsObject()))
      return false;
   if(!acPE[0].calcHiddenGradients(cAttention.AsObject(), acPE[1].getOutput(), 
                                   acPE[1].getGradient(), (ENUM_ACTIVATION)acPE[1].Activation()))
      return false;

Блок кросс-внимания при прямом проходе получает данные из 2 потоков информации:

  • конкатенированные компоненты;
  • кусочно-линейное представление исходных данных.

Мы последовательно распределим градиент ошибки по обоим направлениям. Сначала по направлению PLR.

//--- Gradient to PLR
   if(!cPatchingPLR.calcHiddenGradients(acPE[1].AsObject()))
      return false;
   if(!cNormalizePLR.calcHiddenGradients(cPatchingPLR.AsObject()))
      return false;
   if(!cPLR.calcHiddenGradients(cNormalizePLR.AsObject()))
      return false;

А затем на конкатенированный тензор компонент.

//--- Gradient to Concatenate buffer of Trend, Season and Noise
   if(!cPatching.calcHiddenGradients(acPE[0].AsObject()))
      return false;
   if(!cNormalize.calcHiddenGradients(cPatching.AsObject()))
      return false;
   if(!cConcatInput.calcHiddenGradients(cNormalize.AsObject()))
      return false;

Далее мы распределим градиент ошибки на буферы отдельных компонент.

//--- DeConcatenate
   if(!DeConcat(cTrend.getGradient(), cOutputTimeSeriasRe.getGradient(), cResidual.getGradient(),
                cConcatInput.getGradient(), 1, 1, 1, 3 * iSequence * iVariables))
      return false;

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

//--- Seasons
   if(!CutOneFromAnotherGradient())
      return false;
   if(!SumAndNormilize(cOutputTimeSeriasRe.getGradient(), cTranspose[1].getGradient(), 
                       cTranspose[1].getGradient(), 1, false, 0, 0, 0, 1))
      return false;

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

   if(!cOutputTimeSeriasRe.calcHiddenGradients(cTranspose[1].AsObject()))
      return false;
   if(!Concat(cOutputTimeSeriasRe.getGradient(), GetPointer(cZero), GetPointer(cInputFreqRe), 
                                                 iSequence, iFFT - iSequence, iVariables))
      return false;

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

   if(!SumAndNormilize(GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputTimeSeriasIm),

                       GetPointer(cOutputTimeSeriasIm), 1, false, 0, 0, 0, -0.5f))
      return false;

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

   if(!FFT(GetPointer(cInputFreqRe), GetPointer(cOutputTimeSeriasIm), GetPointer(cOutputFreqRe),

           GetPointer(cOutputFreqIm), false))
      return false;

И проведем через слой частотного внимания до исходных данных.

   if(!Concat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
              cUnNormFreqComplex.getGradient(), 1, 1, iFFT * iVariables))
      return false;
   if(!ComplexUnNormalizeGradient())
      return false;
   if(!cNormFreqComplex.calcHiddenGradients(cFreqAtteention.AsObject()))
      return false;
   if(!ComplexNormalizeGradient())
      return false;
   if(!DeConcat(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
                cInputFreqComplex.getGradient(), 1, 1, iFFT * iVariables))
      return false;
   if(!FFT(GetPointer(cOutputFreqRe), GetPointer(cOutputFreqIm), 
           GetPointer(cInputFreqRe), GetPointer(cInputFreqIm), true))
      return false;
   if(!DeConcat(cTranspose[0].getGradient(), GetPointer(cInputFreqIm), 
                GetPointer(cInputFreqRe), iSequence, iFFT - iSequence, iVariables))
      return false;
   if(!cInputSeasons.calcHiddenGradients(cTranspose[0].AsObject()))
      return false;

И добавим градиент ошибки шума к полученному градиенту исходных данных.

   if(!SumAndNormilize(cInputSeasons.getGradient(), cResidual.getGradient(),
                       cInputSeasons.getGradient(), 1, 1, false, 0, 0, 1))
      return false;

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

//--- trend
   if(!CutTrendAndOtherGradient(NeuronOCL.getGradient()))
      return false;
//---  input gradient
   if(!NeuronOCL.calcHiddenGradients(cPLR.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), cInputSeasons.getGradient(), 
                       NeuronOCL.getGradient(), 1, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

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


Заключение

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

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


Ссылки

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

# Имя Тип Описание
1 Research.mq5 Советник Советник сбора примеров
2 ResearchRealORL.mq5
Советник
Советник сбора примеров методом Real-ORL
3 Study.mq5 Советник Советник обучения Моделей
4 StudyEncoder.mq5 Советник
Советник обучения Энкодера
5 Test.mq5 Советник Советник для тестирования модели
6 Trajectory.mqh Библиотека класса Структура описания состояния системы
7 NeuroNet.mqh Библиотека класса Библиотека классов для создания нейронной сети
8 NeuroNet.cl Библиотека Библиотека кода программы OpenCL
Прикрепленные файлы |
MQL5.zip (2433.12 KB)
Возможности Мастера MQL5, которые вам нужно знать (Часть 13): DBSCAN для класса сигналов советника Возможности Мастера MQL5, которые вам нужно знать (Часть 13): DBSCAN для класса сигналов советника
Основанная на плотности пространственная кластеризация для приложений с шумами (Density Based Spatial Clustering for Applications with Noise, DBSCAN) - это неконтролируемая форма группировки данных, которая практически не требует каких-либо входных параметров, за исключением всего двух, что по сравнению с другими подходами, такими как k-средние, является преимуществом. Разберемся в том, как это может быть полезно в тестировании и торговле с применением советников, собранных в Мастере.
Фильтр сезонности и временные периоды в моделях глубокого обучения с ONNX и Python в советнике Фильтр сезонности и временные периоды в моделях глубокого обучения с ONNX и Python в советнике
Можем ли мы извлечь выгоду из сезонности при создании моделей для глубокого обучения с помощью Python? Помогает ли фильтрация данных в моделях ONNX получить лучшие результаты? Какой период времени использовать? Обо всем этом расскажем в этой статье.
Разрабатываем мультивалютный советник (Часть 16): Влияние разных историй котировок на результаты тестирования Разрабатываем мультивалютный советник (Часть 16): Влияние разных историй котировок на результаты тестирования
Разрабатываемый советник должен показывать хорошие результаты при торговле у разных брокеров. Но мы пока что для тестов использовали котировки с демо-счёта от MetaQuotes. Посмотрим, готов ли наш советник к работе на торговом счёте с другими котировками по сравнению с теми, которые использовались при тестировании и оптимизации.
Нейросети в трейдинге: "Легкие" модели прогнозирования временных рядов Нейросети в трейдинге: "Легкие" модели прогнозирования временных рядов
Легковесные модели прогнозирования временных рядов обеспечивают высокую производительность, используя минимальное количество параметров. Что, в свою очередь, снижает расход вычислительных ресурсов и ускоряет принятие решений. При этом они достигают качества прогнозов, сопоставимого с более сложными моделями.