
Нейросети в трейдинге: Распутывание структурных компонентов (Окончание)
Введение
В данной статье мы переходим к завершающему этапу реализации собственного видения подходов, предложенных авторами фреймворка SCNN, средствами MQL5. SCNN (Seasonal Convolutional Neural Network) — это специализированная архитектура, разработанная для анализа временных рядов с выраженной структурой. Её основная цель — разделение исходных данных на несколько ключевых компонент: долгосрочную тенденцию, сезонную, краткосрочную составляющие, а также учёт пространственной взаимосвязи между переменными. Такая декомпозиция позволяет не только повысить качество прогноза, но и обеспечить интерпретируемость модели — редкое, но крайне ценное качество в задачах алгоритмической торговли.
SCNN опирается на классические принципы анализа, включая нормализацию по периодам и работу с сезонной трансформацией, но сочетает их с современными методами обработки информации: механизмом внимания, параметрической проекцией признаков, а также свёрточной агрегацией результатов. Авторская визуализация фреймворка SCNN представлена ниже.
В предыдущих работах мы последовательно рассмотрели структуру и назначение всех внутренних компонентов модели, реализовав их через специализированные классы и тщательно выстроив каждый элемент будущей архитектуры. Работа оказалась непростой, но результат того стоит: теперь перед нами — набор полноценных модулей, каждый из которых выполняет строго определённую роль в системе.
Следующим логическим шагом становится их объединение в единую, согласованную структуру. Именно на этом этапе формируется архитектурный каркасSCNN, который определяет, как именно будет организовано движение информационных потоков внутри модели — от исходных данных до финального прогноза. Мы переходим от этапа подготовки к этапу функционирования: компоненты оживают, взаимодействуют и, наконец, образуют единый аналитический механизм.
Завершив техническую сборку, перейдем к самой ожидаемой части — тестированию модели на исторических данных. Это позволит оценить не только корректность реализации, но и практическую устойчивость подхода к различным режимам рынка. SCNN — это не просто ещё одна нейросетевая архитектура. Это попытка соединить точность вычислений с прозрачностью принятия решений.
SCNN Энкодер
В предыдущей статье мы завершили разбор архитектуры SCNN-Энкодера, создаваемого в объекте CNeuronSCNNEncoder, и детально рассмотрели процедуру инициализации всех его внутренних компонентов. Каждый из модулей, будь то нормализация, транспонирование или пространственная адаптация, был подготовлен к работе и настроен на приём и обработку исходных данных. Структура объекта представлена ниже.
class CNeuronSCNNEncoder : public CNeuronTransposeOCL { protected: CNeuronPeriodNorm cLongNorm; CNeuronTransposeVRCOCL cSeasonTransp; CNeuronPeriodNorm cSeasonNorm; CNeuronTransposeVRCOCL cUnSeasonTransp; CNeuronPeriodNorm cShortNorm; CNeuronAdaptSpatialNorm cAdaptSpatNorm; CNeuronBaseOCL cConcatenated; CNeuronSwiGLUOCL cProjection; CNeuronTransposeOCL cTranspose; CNeuronConvOCL caFusion[2]; CNeuronBaseOCL cFusionOut; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSCNNEncoder(void) {}; ~CNeuronSCNNEncoder(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint variables, uint forecast, uint season_period, uint short_period, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; //--- virtual int Type(void) override const { return defNeuronSCNNEncoder; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { } };
Сегодня мы продолжаем начатую работу и переходим к построению ключевого элемента — алгоритма прямого прохода. Именно он определяет, как данные последовательно проходят через все уровни модели, преобразуются, агрегируются и в конечном итоге приводят к формированию выходного представления.
Реализация прямого прохода в методе feedForward демонстрирует слаженную работу всей системы.
bool CNeuronSCNNEncoder::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cLongNorm.FeedForward(NeuronOCL)) return false;
Исходные данные сначала проходят через блок долгосрочной нормализации, где устраняется смещение и различия в масштабе, накопленные за длительный период. Это создаёт выровненную основу для дальнейшего анализа.
Затем обработанные данные по цепочке передаются в блок выделения сезонной компоненты. Здесь, как мы обсуждали в предыдущей статье, выполняется транспонирование с заданным шагом, соответствующим периоду сезонности. Благодаря этому, элементы временного ряда группируются в последовательности по фазам цикла, позволяя более эффективно выявлять устойчивые сезонные паттерны. К полученной структуре применяется нормализация по периодам, что усиливает выразительность сезонных флуктуаций. А обратное транспонирование подготавливает данные к последующим этапам обработки.
if(!cSeasonTransp.FeedForward(cLongNorm.AsObject())) return false; if(!cSeasonNorm.FeedForward(cSeasonTransp.AsObject())) return false; if(!cUnSeasonTransp.FeedForward(cSeasonNorm.AsObject())) return false;
Следом за сезонной обработкой наступает этап выделения краткосрочной компоненты. На этом шаге мы фактически извлекаем локальные, быстрые колебания, которые могут содержать важную информацию о недавних изменениях рыночной динамики.
if(!cShortNorm.FeedForward(cUnSeasonTransp.AsObject())) return false;
Завершающим штрихом в фазе предварительной обработки становится применение пространственной нормализации. Этот этап критически важен для согласования информации, извлечённой из различных компонент — долгосрочной, сезонной и краткосрочной. Пространственная нормализация позволяет адаптировать масштаб и взаимное влияние входных переменных, устраняя перекосы, возникающие из-за различий в динамике между признаками. Особенность этого шага в том, что он реализован с учётом пространственных зависимостей и использует механизм внимания, что делает результирующее представление особенно выразительным и информативным.
if(!cAdaptSpatNorm.FeedForward(cShortNorm.AsObject())) return false;
Прежде чем перейти к следующему этапу, необходимо объединить результаты работы всех модулей, включая нормализованные данные и рассчитанные статистические параметры, в единый согласованный тензор. Это критически важный шаг, обеспечивающий дальнейшую целостность потока информации в модели.
Стоит особо подчеркнуть: хотя размерность самих временных последовательностей после нормализации сохраняется, мы сознательно отказались от растягивания статистических параметров (средних и стандартных отклонений) до длины всей последовательности. Это решение было принято в пользу экономии памяти — ведь повторение одного и того же значения по всей длине не несёт дополнительной информации. Однако такая оптимизация приводит к различию в размерностях между временными рядами и соответствующими статистиками.
Несмотря на это, у нас сохраняется единая сетка по числу анализируемых унитарных последовательностей — и это ключевой момент. Именно на него мы и опираемся при конкатенации. Все операции выполняются последовательно в разрезе этих унитарных сегментов, что обеспечивает корректность итоговой сборки и позволяет избежать искажений в структуре данных.
В самом начале этапа объединения мы формируем первый конкатенированный блок, включающий в себя необработанные исходные данные и результат работы модуля долгосрочной нормализации. Это позволяет сохранить контекст начального ряда, одновременно дополняя его информацией о долговременных трендах, выявленных в ходе нормализации. Кроме того, к этим данным мы добавляем рассчитанные статистические параметры (средние значения и стандартные отклонения), которые играют роль своеобразных маркеров масштаба и вариабельности.
uint windows[3] = {NeuronOCL.Neurons() / iWindow, cLongNorm.GetPeriod()*cLongNorm.GetUnits(), 2 * cLongNorm.GetUnits() }; if(!Concat(NeuronOCL.getOutput(), cLongNorm.getOutput(), cLongNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
Следующим шагом мы расширяем наш тензор за счёт информации, извлечённой из сезонной компоненты.
windows[0] = windows[0] + windows[1] + windows[2]; windows[1] = cSeasonNorm.GetPeriod() * cSeasonNorm.GetUnits(); windows[2] = 2 * cSeasonNorm.GetUnits(); if(!cConcatenated.SwapOutputs() || !Concat(cConcatenated.getPrevOutput(), cSeasonNorm.getOutput(), cSeasonNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
После сезонной информации к тензору поочерёдно добавляются данные краткосрочной и сопряжённой (пространственной) компонент. На этом этапе мы продолжаем придерживаться логики объединения по унитарным последовательностям, последовательно расширяя тензор за счёт новых признаков.
windows[0] = windows[0] + windows[1] + windows[2]; windows[1] = cShortNorm.GetPeriod() * cShortNorm.GetUnits(); windows[2] = 2 * cShortNorm.GetUnits(); if(!cConcatenated.SwapOutputs() || !Concat(cConcatenated.getPrevOutput(), cShortNorm.getOutput(), cShortNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false; windows[0] = windows[0] + windows[1] + windows[2]; windows[1] = cAdaptSpatNorm.GetUnits(); windows[2] = 2 * cAdaptSpatNorm.GetUnits(); if(!cConcatenated.SwapOutputs() || !Concat(cConcatenated.getPrevOutput(), cAdaptSpatNorm.getOutput(), cAdaptSpatNorm.GetMeanSTDevs().getOutput(), cConcatenated.getOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
Краткосрочная составляющая предоставляет модели свежий контекст — локальные колебания, всплески и микропаттерны, которые особенно важны для высокой чувствительности прогноза. Пространственная нормализация, в свою очередь, завершает фазу подготовки, обеспечивая дополнительную устойчивость и сглаживание исходных данных в разрезе всех переменных.
В результате, мы формируем содержательное и сбалансированное представление временного ряда, в котором каждая компонентна представлена в согласованной форме. Этот объединённый тензор передаётся в слой проекции, задача которого сформировать взвешенное представление исходных данных с учётом горизонта прогнозирования. На этом этапе данные подстраиваются под требуемую выходную размерность, адаптируясь к задачам будущего анализа. Именно здесь закладывается основа для генерации осмысленного прогноза, способного учитывать как историческую глубину, так и структуру ожидаемых изменений.
if(!cProjection.FeedForward(cConcatenated.AsObject())) return false;
Представления отдельных унитарных последовательностей, полученные на предыдущих этапах, проходят согласование в структуру мультимодального временного ряда в модуле Fusion. Здесь происходит переход от представления в разрезе унитарных последовательностей к временным шагам: тензор транспонируется, и дальнейшая обработка строится уже по временной оси.
На этом этапе задействуются два последовательных сверточных слоя. Первый из них использует функцию активации TANH, позволяя выявить насыщенное, нелинейное представление каждого признака — по сути, это нормализованная форма признакового сигнала. Второй слой, использующий SIGMOID, играет роль гейта: он выделяет степень важности каждого признака во временном контексте, мягко взвешивая полученные значения.
Финальный результат получается поэлементным перемножением выходов двух слоев, что позволяет эффективно отфильтровать шум и подчеркнуть наиболее значимые характеристики временного ряда — уже в согласованном пространстве прогностической задачи.
if(!cTranspose.FeedForward(cProjection.AsObject())) return false; for(uint i = 0; i < caFusion.Size(); i++) if(!caFusion[i].FeedForward(cTranspose.AsObject())) return false; if(!ElementMult(caFusion[0].getOutput(), caFusion[1].getOutput(), cFusionOut.getOutput())) return false; //--- return CNeuronTransposeOCL::feedForward(cFusionOut.AsObject()); }
На заключительном этапе прямого прохода данные проходят обратное транспонирование, возвращаясь из временного представления к исходной структуре унитарных последовательностей. Это позволяет сохранить согласованность форматов на входе и выходе Энкодера.
Таким образом, метод feedForward представляет собой тщательно организованную и продуманную архитектуру, в которой каждый компонент выполняет строго определённую функцию. От нормализации и декомпозиции до проекций и мультимодальной агрегации — весь процесс направлен на формирование насыщенного, структурированного и информативного описания временного ряда.
После завершения разбора алгоритма прямого прохода, логично перейти к не менее важному этапу — обратному распространению ошибки. Именно в этот момент начинается обучение модели: значения градиентов, рассчитанные на выходе, постепенно передаются назад по всей цепочке операций, позволяя скорректировать внутренние параметры каждого модуля. Метод calcInputGradients реализует этот процесс, обеспечивая строгую последовательность и согласованность с архитектурой прямого прохода.
bool CNeuronSCNNEncoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; //--- if(!CNeuronTransposeOCL::calcInputGradients(cFusionOut.AsObject())) return false; if(!ElementMultGrad(caFusion[0].getOutput(), caFusion[0].getGradient(), caFusion[1].getOutput(), caFusion[1].getGradient(), cFusionOut.getGradient(), caFusion[0].Activation(), caFusion[1].Activation())) return false; if(!cTranspose.CalcHiddenGradients(caFusion[0].AsObject())) return false;
Обратный проход начинается с верхнего уровня модели, где в качестве входа используются градиенты, полученные от следующего слоя модели (или от функции потерь, если речь идёт о самом выходе). Первыми обрабатываются сверточные блоки, задействованные в модуле Fusion. Здесь важно понимать, что на этапе прямого прохода мы использовали два параллельных сверточных канала: один с функцией активации TANH для генерации нелинейного признака, второй — с SIGMOID, выполняющий роль гейта, определяющего степень значимости признака. На выходе эти каналы перемножались поэлементно, формируя итоговое представление.
В обратном проходе эта операция умножения требует особой обработки. Метод ElementMultGrad корректно раскладывает градиенты обратно на обе ветки, учитывая особенности каждой функции активации. Это критически важно для точности вычислений, поскольку именно здесь решается, как скорректировать веса внутри свёрток.
Затем мы переходим к передаче градиентов на уровень модуля Transpose, который, напомним, в прямом проходе отвечал за переориентацию осей тензора — от представления унитарных последовательностей к временным шагам. Именно в этом виде данные поступали на вход обоим сверточным слоям модуля Fusion, поэтому теперь задача обратного прохода — корректно объединить градиенты, поступающие от обеих ветвей.
Первым шагом мы спускаем градиент ошибки от одного из сверточных слоёв. Вместо того, чтобы копировать информацию всего тензора, мы используем подмену указателя на буфер данных, что позволяет эффективно переключить область памяти и сохранить полученные значения без лишних затрат ресурсов. Затем, с подменённым указателем, запускаем обратный проход второго сверточного слоя.
CBufferFloat* temp = cTranspose.getGradient(); if(!cTranspose.SetGradient(cTranspose.getPrevOutput(), false) || !cTranspose.CalcHiddenGradients(caFusion[1].AsObject()) || !SumAndNormilize(temp, cTranspose.getGradient(), temp, cTranspose.GetCount(), false, 0, 0, 0, 1) || !cTranspose.SetGradient(temp, false)) return false;
После этого два потока градиентов аккумулируются путём поэлементного суммирования, отражая комбинированное влияние обеих ветвей на итоговую ошибку. В завершение указатели буферов возвращаются в исходное состояние, обеспечивая корректность структуры данных для дальнейшего распространения градиентов. Такой подход гарантирует согласованность модели и оптимальную работу с памятью при обучении.
Далее ошибка передается в блок Projection, ответственный за взвешивание информации перед модулями свертки. Градиент от него передается вниз к блоку Concatenated, где начинается один из наиболее технически сложных этапов — обратная деконкатенация.
if(!cProjection.CalcHiddenGradients(cTranspose.AsObject())) return false; if(!cConcatenated.CalcHiddenGradients(cProjection.AsObject())) return false;
Напомним, что на этапе прямого прохода мы поэтапно объединяли данные от различных нормализаторов: долгосрочного, сезонного, краткосрочного и пространственного. Причём, вместе с нормализованными данными в объединённый тензор включались и статистические параметры (средние значения, стандартные отклонения).
Сейчас задача стоит обратная — развернуть совмещённый тензор обратно на составные части. Для этого используется последовательная процедура DeConcat, в которой размеры окон для каждого компонента рассчитываются в строгом соответствии с предыдущей компоновкой. Этот этап требует особой точности, так как малейшая ошибка в размере окна может привести к смещению или потере данных.
uint windows[3] = {0}; windows[1] = cAdaptSpatNorm.GetUnits(); windows[2] = 2 * cAdaptSpatNorm.GetUnits(); windows[0] = cConcatenated.Neurons() / iWindow - windows[1] - windows[2]; if(!DeConcat(cConcatenated.getPrevOutput(), cAdaptSpatNorm.getGradient(), cAdaptSpatNorm.GetMeanSTDevs().getGradient(), cConcatenated.getGradient(), windows[0], windows[1], windows[2], iWindow)) return false; windows[1] = cShortNorm.GetPeriod() * cShortNorm.GetUnits(); windows[2] = 2 * cShortNorm.GetUnits(); windows[0] = windows[0] - windows[1] - windows[2]; if(!DeConcat(cConcatenated.getGradient(), cShortNorm.getPrevOutput(), cShortNorm.GetMeanSTDevs().getGradient(), cConcatenated.getPrevOutput(), windows[0], windows[1], windows[2], iWindow)) return false; windows[1] = cSeasonNorm.GetPeriod() * cSeasonNorm.GetUnits(); windows[2] = 2 * cSeasonNorm.GetUnits(); windows[0] = windows[0] - windows[1] - windows[2]; if(!DeConcat(cConcatenated.getPrevOutput(), cSeasonNorm.getPrevOutput(), cSeasonNorm.GetMeanSTDevs().getGradient(), cConcatenated.getGradient(), windows[0], windows[1], windows[2], iWindow)) return false; windows[1] = cLongNorm.GetPeriod() * cLongNorm.GetUnits(); windows[2] = 2 * cLongNorm.GetUnits(); windows[0] = windows[0] - windows[1] - windows[2]; if(!DeConcat(NeuronOCL.getPrevOutput(), cLongNorm.getPrevOutput(), cLongNorm.GetMeanSTDevs().getGradient(), cConcatenated.getPrevOutput(), windows[0], windows[1], windows[2], iWindow)) return false;
На этом этапе стоит отдельно подчеркнуть важный технический момент. Все модули нормализации, за исключением пространственной, не были финальной точкой в цепочке подготовки данных. Их результаты использовались в качестве исходных данных для следующих этапов обработки. Это означает, что на момент обратного прохода каждый такой модуль получает градиент ошибки не из одного источника, а из нескольких. Чтобы корректно учесть вклад всех последующих операций, мы не можем просто перезаписывать градиент, как это принято в более простой архитектуре. Вместо этого используется резервный буфер: каждый модуль сохраняет свой текущий градиент туда, прежде чем получить новый.
Таким образом, обратный проход по цепочке нормализаций реализуется с накоплением градиентов в специальных буферах. Это обеспечивает точное восстановление влияния каждого компонента модели и исключает потери информации.
После успешной деконкатенации, каждый из модулей нормализации — начиная с краткосрочного cShortNorm, затем сезонного cSeasonNorm и, наконец, долгосрочного cLongNorm — получает соответствующую долю градиента ошибки, отражающую влияние цепочки обработки данных в прямом проходе. Но как мы уже отмечали, каждый из этих нормализаторов участвовал не только в процессе подготовки данных. Поэтому просто передать назад градиент по одному пути было бы недостаточно.
На практике это реализуется следующим образом. После того, как нормализатор получает градиент ошибки от объекта, использующего его данные, мы прибавляем его к уже имеющимся значениям, полученным на этапе деконкатенации. Таким образом, на каждом этапе мы аккуратно суммируем информацию из нескольких источников. Только после этой процедуры агрегирования результирующий градиент передаётся далее по цепочке вниз, к предыдущему слою.
if(!cShortNorm.CalcHiddenGradients(cAdaptSpatNorm.AsObject()) || !SumAndNormilize(cShortNorm.getGradient(), cShortNorm.getPrevOutput(), cShortNorm.getGradient(), cShortNorm.GetPeriod(), false, 0, 0, 0, 1)) return false; if(!cUnSeasonTransp.CalcHiddenGradients(cShortNorm.AsObject())) return false; if(!cSeasonNorm.CalcHiddenGradients(cUnSeasonTransp.AsObject()) || !SumAndNormilize(cSeasonNorm.getGradient(), cSeasonNorm.getPrevOutput(), cSeasonNorm.getGradient(), cSeasonNorm.GetPeriod(), false, 0, 0, 0, 1)) return false; if(!cSeasonTransp.CalcHiddenGradients(cSeasonNorm.AsObject())) return false; if(!cLongNorm.CalcHiddenGradients(cSeasonTransp.AsObject()) || !SumAndNormilize(cLongNorm.getGradient(), cLongNorm.getPrevOutput(), cLongNorm.getGradient(), cLongNorm.GetPeriod(), false, 0, 0, 0, 1)) return false;
Подобный подход гарантирует, что ни одна информационная связь, проложенная в прямом проходе, не будет утрачена или искажена на этапе обратного распространения ошибки. Именно эта строгость и аккуратность в передаче градиентов — один из факторов устойчивости и высокой объяснимости фреймворка SCNN в условиях реального рынка.
Завершается обратный проход расчётом градиентов на самом входе — объекте NeuronOCL. Этот шаг критически важен, так как именно здесь сходятся все информационные потоки, прошедшие через сложную цепочку преобразований и нормализаций.
if(!NeuronOCL.CalcHiddenGradients(cLongNorm.AsObject()) || !SumAndNormilize(NeuronOCL.getGradient(), NeuronOCL.getPrevOutput(), NeuronOCL.getGradient(), cLongNorm.GetPeriod(), false, 0, 0, 0, 1)) return false; //--- return true; }
Важно отметить, что в этом месте встречаются два ключевых потока: один — от сложной системы нормализаций, второй — от проекционного слоя, отвечающего за формирование признакового пространства для прогнозирования. Их корректная агрегация обеспечивает непрерывность градиентного потока и корректное обновление весов модели при обучении.
Таким образом, метод calcInputGradients реализует не просто технический откат по цепочке, а выверенную, модульную и структурированную процедуру обратного распространения ошибки. Каждое действие в нём чётко соотносится с предыдущими шагами прямого прохода, что делает архитектуру SCNN-Энкодера цельной, симметричной и легко интерпретируемой.
Оптимизация обучаемых параметров ЭнкодераCNeuronSCNNEncoder реализована через делегирование ответственности соответствующим внутренним модулям, каждый из которых содержит собственные веса и реализует свою логику обновления. Метод updateInputWeights выполняет последовательную передачу управления этим модулям, обеспечивая централизованное, но модульное управление обучением.
bool CNeuronSCNNEncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cAdaptSpatNorm.UpdateInputWeights(cShortNorm.AsObject())) return false; if(!cProjection.UpdateInputWeights(cConcatenated.AsObject())) return false; for(uint i = 0; i < caFusion.Size(); i++) if(!caFusion[i].UpdateInputWeights(cTranspose.AsObject())) return false; //--- return true; }
Благодаря модульной структуре, алгоритм обновления весов остаётся простым и логичным, не перегружая код лишними зависимостями.
Полный исходный код класса CNeuronSCNNEncoder, включающий реализацию всех ключевых методов, представлен во вложении. Благодаря прозрачной структуре и детальной декомпозиции на модули, этот код может служить основой для дальнейших экспериментов и модификаций.
Модуль верхнего уровня
Авторы фреймворка SCNN предлагают использовать иерархическую архитектуру, в которой несколько энкодеров выстраиваются в единый стек. Это решение направленно на повышение выразительности модели и, как следствие, улучшение качества прогнозирования. Каждый следующий энкодер в такой цепочке работает не изолированно, а использует результаты предыдущего в виде исходных данных. А использование остаточных связей обеспечивает дополнительный контекст для анализа. Тем самым модель способна учитывать более широкий контекст, углубляя анализ и улавливая как локальные, так и долгосрочные зависимости во временном ряду.
Однако такая гибкость имеет и свою цену. При переходе от одного слоя к другому возникает вполне конкретная проблема — различие в размере тензора на входе и выходе. Дело в том, что на выходе каждого SCNN-Энкодера мы получаем унитарные последовательности в сопоставимом формате, но увеличенные на заданный горизонт планирования.
И вот здесь возникает непростая задача. С одной стороны, для следующего слоя необходимо подать данные без включения в них прогнозных значений — иначе мы будем передавать вперёд информацию из будущего, что недопустимо. С другой стороны, мы не можем просто отбросить эти прогнозные значения, так как они необходимы для финального суммирования выходов всех слоёв. Проще говоря, мы должны одновременно учитывать и то, что модель уже увидела, и то, что она предсказала, не разрушая при этом временную структуру данных.
Для решения этой задачи создадим объект верхнего уровня — CNeuronSCNN. Это обобщённый управляющий элемент, инкапсулирующий в себе всю стековую структуру Энкодеров. Его задача — не просто вызывать Энкодеры по очереди, а обеспечить корректную работу всей архитектуры: передавать данные между слоями, выравнивать их размеры, аккумулировать прогнозы, управлять обучением и обратным распространением ошибки. Именно он становится связующим звеном между вычислительной логикой и архитектурной согласованностью модели.
Структура нового объекта представлена ниже.
class CNeuronSCNN : public CNeuronBaseOCL { protected: CLayer cLayers; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSCNN(void) {}; ~CNeuronSCNN(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint variables, uint forecast, uint season_period, uint short_period, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; //--- virtual int Type(void) override const { return defNeuronSCNN; } virtual void TrainMode(bool flag) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { } };
Внутри CNeuronSCNN размещён объект типа CLayer, который по сути представляет собой контейнер для всех вложенных Энкодеров. Единственный внутренний объект класса объявлен статично, что позволяет нам оставить пустыми конструктор и деструктор класса, переложим управление памятью на систему.
Для запуска всей конструкции в работу, необходимо корректно собрать архитектуру из составных частей. Эта задача решается в методе Init, где создаётся и настраивается вся динамическая структура объекта CNeuronSCNN.
bool CNeuronSCNN::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units_count, uint variables, uint forecast, uint season_period, uint short_period, uint layers, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, (units_count + forecast)*variables, optimization_type, batch)) return false; SetActivationFunction(None);
Здесь всё начинается с базовой инициализации через одноименный метод родительского класса, где создаются базовые интерфейсы нейронного слоя. Далее отключается функция активации — она на этом уровне не требуется, поскольку Энкодеры сами по себе содержат всю необходимую нелинейность и предварительную обработку данных.
После этого, контейнер cLayers очищается и подготавливается к размещению необходимых объектов.
cLayers.Clear(); cLayers.SetOpenCL(OpenCL); CNeuronSCNNEncoder* encoder = NULL; CNeuronBaseOCL* residual = NULL; for(uint l = 0; l < layers; l++) { encoder = new CNeuronSCNNEncoder(); if(!encoder) return false; if(!encoder.Init(0, l, OpenCL, units_count, variables, forecast, season_period, short_period, optimization, iBatch) || !cLayers.Add(encoder)) return false; encoder.SetActivationFunction(None); if((l + 1) == layers) break;
Цикл по количеству слоёв (параметр layers) начинает поочерёдно создавать Энкодеры CNeuronSCNNEncoder. Для каждого слоя создаётся объект-энкодер и инициализируется с параметрами: размерностью входов, количеством переменных, длиной прогноза и периодами сезонности. Если инициализация проходит успешно, объект добавляется в контейнер слоёв.
Но работа на этом не заканчивается. Чтобы сохранить остаточные связи между слоями, между энкодерами вставляются дополнительные слои, выступающие как своего рода буферы согласования. Эти промежуточные блоки подхватывают выходы предыдущих Энкодеров и отбрасывают прогнозные значения, обеспечивая непрерывность потока данных между уровнями. Здесь снова отключается функция активации — ведь эти блоки выполняют скорее служебную, нежели вычислительную роль.
residual = new CNeuronBaseOCL(); if(!residual) return false; if(!residual.Init(0, l, OpenCL, units_count * variables, optimization, iBatch) || !cLayers.Add(residual)) return false; residual.SetActivationFunction(None); } if(!SetGradient(encoder.getGradient(), true)) return false; //--- return true; }
Важно отметить, что финальный Энкодер в стеке не получает последующий служебный слой. А его градиент используется в качестве основного для всего верхнего слоя, что позволяет исключить излишнюю операцию копирования данных.
Таким образом, метод Init превращается из просто инициализатора в своего рода строителя объекта, на ходу собирающего всю стековую структуру Энкодеров, аккуратно подстраивая каждую деталь, учитывая выравнивание размеров, прогнозные горизонты и логистику потока градиентов. Благодаря такой продуманной архитектуре, становится возможным не только эффективное обучение, но и масштабирование модели под реальные задачи, будь то краткосрочный прогноз цен или долгосрочный анализ сезонных тенденций.
После завершения этапа инициализации, когда структура многослойной SCNN-модели собрана и готова к работе, наступает ключевой момент — выполнение прямого прохода. Именно здесь модель начинает свою основную функцию: обработку исходных данных, формирование прогнозов и накопление информации на каждом уровне.
bool CNeuronSCNN::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!Output.Fill(0)) return false; CNeuronBaseOCL* inputs = NeuronOCL; CNeuronSCNNEncoder* current = NULL; CNeuronBaseOCL* residual = NULL; int layers = cLayers.Total();
Всё начинается с проверки корректности полученного указателя исходных данных. Если они отсутствуют, выполнение прекращается. После чего буфер результатов очищается, чтобы избежать накопления артефактов от предыдущих итераций. Далее переменная inputs инициализируется указателем на внешний источник данных — тем, что мы передаём на вход Энкодеру. А затем начинается обход по всем слоям.
Архитектура модели построена так, что слои чередуются: на чётных позициях располагаются SCNN-Энкодеры, на нечётных — буферы остаточных связей, реализованные как базовые нейронные объекты. Именно поэтому в цикле шаг равен двум — один проход охватывает и Энкодер, и соответствующий остаточный блок.
for(int l = 0; l < layers; l += 2) { current = cLayers[l]; if(!current || !current.FeedForward(inputs) || !SumAndNormilize(Output, current.getOutput(), Output, current.GetCount(), false, 0, 0, 0, 1)) return false; if((l + 1) == layers) break;
На каждом витке цикла мы извлекаем текущий Энкодер, вызываем его метод FeedForward, а затем добавляем выход модуля к результирующему прогнозу. Таким образом, уже в процессе прямого прохода начинает формироваться аккумулированный итог всех прогнозов по слоям.
Но если это ещё не последний слой, предстоит дополнительная работа. Мы извлекаем следующий (нечётный) объект — буфер остаточной связи. Перед ним стоит задача скорректировать размерности исходных данных для следующего Энкодера, устранив избыточность, возникшую из-за добавления прогнозных значений. Эта задача решается методом DeConcat, который отделяет прогнозную часть от чистой истории, возвращая данные в исходную форму. После этого происходит сложение новых входов с предыдущими, обеспечивая эффект остаточных связей, и их последующая нормализация. Каждый слой работает не в изоляции, а с учётом результатов предыдущих, получая более богатое представление временного ряда.
uint variables = current.GetWindow(); uint dimension = inputs.Neurons() / variables; uint forecast = current.GetCount() - dimension; residual = cLayers[l + 1]; if(!residual) return false; if(!DeConcat(residual.getOutput(), current.getPrevOutput(), current.getOutput(), dimension, forecast, variables) || !SumAndNormilize(residual.getOutput(), inputs.getOutput(), residual.getOutput(), dimension, true, 0, 0, 0, 1)) return false; inputs = residual; } //--- return true; }
Полученные после деконкатенации и нормализации данные передаются следующему Энкодеру — и так продолжается до завершения прохода по всем уровням. На выходе формируется совокупный прогноз, результат совместной работы всех Энкодеров блока.
Таким образом, метод feedForward представляет собой тщательно выстроенный процесс, где каждый шаг направлен на обогащение информации, устранение искажений и подготовку сбалансированного прогноза. Он реализует в действии всю философию стековой архитектуры: последовательное углубление анализа без потери исторического контекста и с учётом вкладов каждого уровня.
После завершения прямого прохода, переходим к не менее важной стадии — обратному распространению ошибки. Здесь реализуется передача градиентов от выходного слоя обратно к исходным данным, позволяющая оптимизировать параметры модели. Процесс организован в методе calcInputGradients.
bool CNeuronSCNN::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!PrevOutput.Fill(0)) return false; //--- CNeuronBaseOCL* inputs = NULL; CNeuronSCNNEncoder* current = cLayers[-1]; CNeuronBaseOCL* residual = NULL; int layers = cLayers.Total() - 2;
Процедура начинается с базовых проверок: наличие указателя на внешний объект NeuronOCL и обнуление вспомогательного буфера PrevOutput. Далее определяется количество слоёв, участвующих в обучении. Здесь важно понимать, что подсчёт ведётся в обратном порядке, начиная с последнего Энкодера, поскольку обратный проход требует движения от выхода к входу модели.
Цикл с шагом "-1" охватывает все слои, включая объекты остаточных связей, и для каждого выполняется соответствующая логика в зависимости от типа слоя.
for(int l = layers; l >= 0; l--) switch(cLayers[l].Type()) { case defNeuronBaseOCL: inputs = cLayers[l]; if(!inputs || !inputs.CalcHiddenGradients(current)) return false; if(!!residual) if(!SumAndNormilize(inputs.getGradient(), residual.getGradient(), inputs.getGradient(), current.GetWindow(), false, 0, 0, 0, 1)) return false; residual = inputs; break;
Если перед нами базовый объект CNeuronBaseOCL, то выполняем два действия. Во-первых, рассчитываем градиенты скрытого слоя на основе текущего Энкодера. А во-вторых, при наличии предыдущего остаточного блока (переменная residual), суммируем его градиенты с текущими, корректируя передаваемое значение. Это обеспечивает непрерывность информации между слоями и позволяет избежать потерь, вызванных остаточной структурой архитектуры.
Если же текущий слой — это SCNN-Энкодер, тогда логика становится более комплексной. Сначала мы убеждаемся, что объект residual уже определён. Если нет — просто переходим дальше. Иначе — извлекаем объект, находящийся через два слоя вперёд, то есть тот, который подавал данные на вход текущему Энкодеру во время прямого прохода. Это позволяет точно восстановить структуру данных.
case defNeuronSCNNEncoder: current = cLayers[l]; if(!residual) break; inputs = cLayers[l + 2]; if(!inputs) return false; if(!Concat(residual.getGradient(), PrevOutput, current.getGradient(), residual.Neurons() / current.GetWindow(), current.GetCount() - residual.Neurons() / current.GetWindow(), current.GetWindow()) || !SumAndNormilize(current.getGradient(), inputs.getGradient(), current.getGradient(), current.GetWindow(), false, 0, 0, 0, 1)) return false; break; default: return false; break; }
Затем, с помощью функции Concat, мы восстанавливаем форму градиента: к текущему градиенту остаточного слоя добавляется нулевые значения их PrevOutput, соответствующие прогнозной части. Это необходимо, потому что при прямом проходе данные были разделены на историю и прогноз, и сейчас эту структуру нужно точно воспроизвести в градиенте ошибки. Далее происходит их суммирование с градиентом из последующего Энкодера — это важный момент, ведь информация может идти по двум информационным потокам, и они должны быть сведены в единую форму.
После завершения прохода по всем слоям, выполняется финальный шаг — передача градиента на самый внешний уровень. Здесь NeuronOCL получает градиент от нижнего SCNN-Энкодера, а также — при наличии остаточного слоя — происходит итоговое суммирование градиентов.
if(!NeuronOCL.CalcHiddenGradients(current)) return false; if(!!residual) if(!SumAndNormilize(NeuronOCL.getGradient(), residual.getGradient(), NeuronOCL.getGradient(), current.GetWindow(), false, 0, 0, 0, 1)) return false; //--- return true; }
Таким образом, метод calcInputGradients воплощает ключевую идею стековой архитектуры: каждый слой получает возможность скорректировать свои параметры не только с учётом собственной ошибки, но и с учётом того, как он влияет на всю модель в целом. Это обеспечивает тонкую настройку и высокую чувствительность к особенностям временных рядов.
После завершения расчёта градиентов по всем уровням стековой архитектуры, наступает финальный этап обучения — обновление весов. Именно здесь происходит практическая реализация всей проделанной ранее работы: мы преобразуем накопленную информацию об ошибке в корректировки обучаемых параметров модели.
Метод updateInputWeights отвечает за поэтапное обновление весов всех обучаемых блоков внутри модели. Начинается всё с того, что переменная inputs инициализируется указателем на внешний источник данных — объект NeuronOCL.
bool CNeuronSCNN::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL* inputs = NeuronOCL; CNeuronBaseOCL* current = NULL; //--- for(int l = 0; l < cLayers.Total(); l++) { current = cLayers[l]; if(!current) return false; if(current.Type() == defNeuronSCNNEncoder) if(!current.UpdateInputWeights(inputs)) return false; inputs = current; } //--- return true; }
Далее разворачивается итерация по всем слоям модели. На каждом шаге цикла проверяется тип слоя. Нас интересуют только SCNN-Энкодеры, поскольку именно они содержат обучаемые параметры. Если текущий слой соответствует этому типу, вызывается метод UpdateInputWeights, которому передаются исходные данные. Это позволяет корректно перенастроить веса в зависимости от полученного градиента. В случае успешного обновления, заменяем указатель в переменной inputs, чтобы стать входом для следующего слоя. Тем самым сохраняется логическая связность, характерная для прямого прохода и обратного распространения ошибки.
Метод updateInputWeights завершает цикл обучения, позволяя каждому Энкодеру адаптироваться к ошибкам модели и внести свою лепту в общий процесс оптимизации. Всё реализовано сдержанно, по-деловому, без лишней сложности, но при этом с полным соблюдением логики архитектуры.
Полный код данного класса CNeuronSCNN и всех его методов представлен во вложении.
Архитектура модели
После завершения описания логики объектов построения фреймворка SCNN, естественным шагом становится погружение в архитектуру самой модели, ведь именно она определяет, насколько точно и устойчиво система способна извлекать паттерны из данных. И здесь перед нами разворачивается процесс построения конфигурации нейросетевого пайплайна, который осуществляется в методе CreateDescriptions.
Этот метод ответственен за создание и заполнение описаний слоёв — тех строительных блоков, из которых впоследствии формируется вычислительная модель. Если говорить просто — здесь, словно по кирпичику, закладываются уровни будущей нейросети: от слоя исходных данных до энкодеров и прогнозных ветвей. На старте мы видим создание и инициализацию шести контейнеров: под основной Энкодер состояния окружающей среды, три варианта прогнозных моделей, а также под ветви Actor и Critic, используемые в задачах обучения с подкреплением.
Сам Энкодер состояния окружающей среды начинается с базового полносвязного слоя, принимающего вектор исходных данных, сформированный на основе исторических баров.
bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&forecast1, CArrayObj *&forecast2, CArrayObj *&forecast3, CArrayObj *&actor, 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(!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; } //--- 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; }
Далее — слой добавления признаков первой разности CNeuronConcatDiff, который создаёт производные признаки, помогая сети лучше улавливать локальные изменения.
Особое внимание заслуживает слой CMamba4CastEmbeding, представляющий собой современный архитектурный элемент. Он извлекает скрытые признаки c учётом нескольких временных окон (в данном случае дневного и месячного), создавая тем самым эмбеддинги с учетом временных гармоник. Именно здесь нейросеть впервые задумывается о сезонности и долгосрочных трендах.
//--- 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_D1), PeriodSeconds(PERIOD_MN1)}; 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;
Далее эмбеддинги транспонируются в представления унитарных последовательностей.
И венцом всей конструкции Энкодера состояния окружающей среды становится интеграция ранее описанного модуля — стека SCNN-Энкодеров. Именно этот блок, выстроенный с четырьмя уровнями вложенности и аккуратно настроенный на восприятие сезонных и короткосрочных закономерностей, формирует ядро интеллектуальной обработки информации.
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSCNN; descr.variables = prev_count; { uint temp[]={prev_out,NForecast,SeasonPeriod,ShortPeriod}; if(ArrayCopy(descr.windows,temp)<(int)temp.Size()) return false; } descr.count=descr.windows[0]+descr.windows[1]; descr.layers=4; descr.batch = BatchSize; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } uint variables=descr.variables; uint count=descr.count;
Здесь происходит нечто большее, чем простая трансформация данных. Этот модуль берёт на себя ключевую аналитическую нагрузку: он не только извлекает признаки, но и структурирует их в виде многослойных скрытых представлений, каждое из которых обобщает наблюдаемые закономерности в своём масштабе. На выходе мы получаем прогнозные вектора, уже обогащённые остаточными связями и памятью предыдущих уровней.
По сути, именно в этом элементе происходит превращение сырых исходных данных в высокоуровневое описание состояния рынка. Здесь абстрактные шаблоны поведения превращаются в числовые структуры, готовые к интерпретации либо прогнозными модулями, либо управляющими ветвями модели. И именно с этого момента архитектура обретает полноту — переходя от подготовки и нормализации к анализу, интерпретации и принятию решений.
Здесь стоит особо подчеркнуть важный технический момент. На выходе стека SCNN-Энкодеров мы получаем массив унитарных последовательностей — это удобно и логично для последующих моделей прогнозирования. Идеально подходит для генерации прогнозов на заданном горизонте. Но он оказывается не совсем уместным в контексте анализа, который будут выполнять модули Актера и Критика.
Дело в том, что для принятия решений агенту необходимо не просто знать каждый прогноз отдельно, а понимать их как связную временную структуру — некую мультимодальную динамику, охватывающую как краткосрочные, так и долгосрочные аспекты поведения системы. Для этого требуется иная организация данных: последовательное упорядочивание временных шагов с сохранением их взаимосвязей.
Именно поэтому мы добавляем ещё один слой транспонирования. Этот шаг превращает наш тензор из набора унитарных прогнозов в формат, соответствующий временным шагам мультимодальной последовательности. Каждая временная точка получает доступ ко всем переменным прогноза, собранным из разных каналов. Благодаря этому, Актер и Критик могут воспринимать динамику среды во времени как целостную картину, а не как изолированные фрагменты.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = variables; prev_count = descr.window = count; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Таким образом, этот заключительный трансформационный слой завершает архитектуру Энкодера и играет ключевую роль в обеспечении корректного интерфейса между механизмом прогнозирования и модулями принятия решений.
Архитектура моделей прогнозирования и принятия решений была перенесена из наших предыдущих разработок практически без изменений. Эти компоненты уже доказали свою эффективность, поэтому в рамках данной статьи мы не будем останавливаться на них подробно. Тем, кто заинтересован в более глубоком понимании архитектурных решений, рекомендуем обратиться к материалам, размещённым во вложении.
Кроме того, во вложении находятся исходные коды программ, обеспечивающих обучение и тестирование моделей. Эти материалы дадут возможность не только проследить внутреннюю логику построения системы, но и воспроизвести полный цикл эксперимента.
Тестирование
Процесс обучения модели построен в два последовательных этапа. На первом, офлайн-этапе, обучение проводилось на исторических данных валютной пары EURUSD таймфрейм H1 за весь 2024 год. Этот период охватывал широкий спектр рыночных сценариев. Разнообразие данных позволило модели освоить как типичные, так и редкие рыночные ситуации.
После завершения офлайн-обучения мы перешли к второму этапу — тонкой онлайн-настройке, выполненной в условиях, максимально приближенных к реальному рынку. Обучение проходило в тестере стратегий MetaTrader 5, где модель анализировала потоковые данные по свечам шаг за шагом. Это позволило не только проверить устойчивость модели к шумам и рыночным искажениям, но и обеспечить её адаптивность к изменяющимся условиям. Такой подход значительно повысил живучесть модели, минимизировал переобучение и улучшил способность к обобщению.
Завершением стала проверка модели на полностью новых данных — котировках за период с Января по Март2025 года. Все параметры и настройки, полученные в ходе обучения, были сохранены без изменений. Таким образом, полученные результаты дают объективную оценку как точности, так и практической надёжности предложенного метода. Результаты тестирования представлены ниже.
За три месяца тестирования наша модель продемонстрировала рост капитала с $100 до примерно $430, при этом средняя прибыль в сделке ($13,17) превосходила средний убыток ($11,71). Доля выигрышных операций близка к 48%, а коэффициент прибыли около 1.04 говорит о том, что система работает с небольшим запасом в пользу профита.
Вместе с тем, максимальная просадка превысила 82%, причем самое глубокое падение случилось во второй декаде Марта. Период максимально удаления от обучающей выборки. Это свидетельствует о том, что модель испытывает трудности при столкновении с новыми рыночными условиями и недостаточно устойчива к неожиданным всплескам волатильности.
В целом тест показал, что SCNN-архитектура способна извлекать прибыль и сохранять баланс между риском и доходом, но для практического применения необходимы дополнительные механизмы управления рисками с расширением периода обучения. Это позволит модели не только стабильно работать на знакомом рынке, но и более уверенно адаптироваться к новым, непредсказуемым условиям.
Заключение
В данной статье мы завершили разработку и практическую проверку SCNN-модели для прогнозирования временных рядов. Проделана большая работа: от рассмотрения теоретической идеи декомпозиции временных рядов до полноценного обученного стека Энкодеров и внедрение предложенных подходов в архитектуру Актер-Критик.
Тест на данных валютной пары EURUSD с Января по Март 2025 года подтвердил способность модели генерировать прибыль и формировать сбалансированные прогнозы, однако выявил и уязвимость к резким рыночным изменениям за рамками обучающей выборки. Глубокие просадки в Марте подчёркивают необходимость проведения дальнейшей работы по оптимизации модели.
Ссылки
- Disentangling Structured Components: Towards Adaptive, Interpretable and Scalable Time Series Forecasting
- Другие статьи серии
Программы, используемые в статье
# | Имя | Тип | Описание |
---|---|---|---|
1 | Study.mq5 | Советник | Советник офлайн обучения моделей |
2 | StudyOnline.mq5 | Советник | Советник онлайн обучения моделей |
3 | Test.mq5 | Советник | Советник для тестирования модели |
4 | Trajectory.mqh | Библиотека класса | Структура описания состояния системы и архитектуры моделей |
5 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
6 | NeuroNet.cl | Библиотека | Библиотека кода OpenCL-программы |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования