Нейросети в трейдинге: Адаптивное восприятие рыночной динамики (Окончание)
Введение
Финансовый рынок — это не удобочитаемая таблица. Он — поток. Непрерывный, разреженный, местами взрывной. События на рынке появляются не по расписанию. Они возникaют там, где меняется ликвидность, распадаются заявки и мелькают всплески объёма. Понимание этого потока — основное условие для построения надёжной, чувствительной и адаптивной системы прогнозирования. STE-FlowNet родился именно из этого наблюдения: смотреть на рынок как на поток событий и научить модель чувствовать его движение.
Такой взгляд меняет всё. Базовые модели, оперирующие только последовательностью свечей или агрегированных признаков, априори теряют часть информации о структуре движения — о том, где и когда возник дисбаланс между спросом и предложением. Event-driven анализ позволяет фиксировать сигналы высокой значимости в момент их появления. Это даёт низкую латентность реакции. Это повышает устойчивость к усредняющим эффектам, когда важная информация растворяется в агрегации. И ещё. Поток событий естественным образом комбинирует локальную чувствительность и глобальную устойчивость. Модель видит и мгновенный всплеск, и фоновую тенденцию. Причём без искусственного разделения на короткий и длинный горизонт.
STE-FlowNet перенёс в мир финансов идею пространственно-временного восприятия, проверенную в других областях как метод извлечения динамики из событийных данных. В основе лежит не магия, а чёткая инженерия. Он задуман как инструмент точного и интерпретируемого моделирования краткосрочной рыночной динамики прямого извлечения микродвижений цен и их локальных взаимосвязей во времени.
Фреймворк STE-FlowNet можно представить как команду опытных аналитиков на финансовом рынке, каждый из которых выполняет свою роль в обработке потока событий. Энкодер — это главный стратег, который фиксирует каждую сделку, каждое изменение цены и объема, аккумулируя знания о текущей и прошлой динамике рынка. Его ConvGRU-память работает как опытный трейдер, помнящий всю историю последних движений и мгновенно оценивающий значимость новых сигналов.
Промежуточные карты движения и skip-соединения — это аналитическая сеть помощников, которые сохраняют детали и мгновенно передают критически важную информацию дальше. Они позволяют модели видеть как быстрые всплески активности, так и медленные, но важные тренды, подобно команде, которая одновременно отслеживает микроскопические колебания котировок и глобальные рыночные тенденции.
Корреляционный слой и итеративное уточнение — это группа экспертов, корректирующих прогнозы на основе предыдущих оценок. Каждый новый шаг учитывает как текущие данные, так и всю накопленную историю, создавая динамическую, адаптивную стратегию предсказания движения рынка.
Финальный декодер с Projection Head — это портфельный менеджер, который превращает все внутренние оценки и прогнозы в конкретные действия: сигнал входа или выхода, вероятностную траекторию движения, оценку риска и силы рыночного импульса. Он соединяет все части команды и формирует практическое решение, готовое к применению в реальном времени.
Самоконтролируемое обучение и баланс между фиксацией локальных аномалий и глобальной гладкостью потока делают STE-FlowNet устойчивым к шуму, всплескам и редким событиям. В финансовом контексте это позволяет точно фиксировать резкие колебания цен и объема, выявлять критические точки и поддерживать принятие решений в условиях высокой волатильности.
Авторская визуализация фреймворка STE-FlowNet представлена ниже.

В предыдущих работах мы завершили реализацию базового контура Энкодера и заложили основу потоковой обработки данных. Сегодня мы продолжим построение фреймворка — добавим резидуальные блоки, реализуем полноценный Flow Decoder и сформируем объект верхнего уровня, отвечающий за интегральное прохождение рыночного сигнала.
Residual-блок
Сегодня мы начинаем работу с построения Residual-блока — своего рода мини-конструкции внутри модели. Прежде чем брать в руки инструменты, то есть писать код, давайте взглянем на чертеж, предложенный авторами фреймворка. Внутри блока мы видим две последовательные пары сверточных блоков с остаточными связями, а результаты работы этих элементов затем аккуратно конкатенируются с полученными на вход исходными данными.
Особое внимание стоит уделить остаточным связям. При знакомстве с SEW-ResNet мы уже рассмотрели проблемы использования похожих конструкций в спайк-нейросетях. На ум сразу приходит Spike-Element-Wise блок — проверенный компонент, который позволяет сохранять структуру сигналов даже при сложной нагрузке.
Зачем изобретать велосипед, если есть готовое и надёжное решение? Предлагаю построить наш Residual-блок с использованием SEW-подходов, как опытный архитектор использует проверенные элементы для создания надёжного здания. Такой подход обеспечит стабильность, сохранит форму локальных признаков и ускорит процесс разработки, превращая наш блок в прочную и функциональную часть всей модели.
Предложенное решение реализуем в рамках нового класса CNeuronSTEFlowNetResidualBlock, который наследует функционал от базового класса CNeuronBaseOCL. Новый объект полностью инкапсулирует всю логику Residual-блока и взаимодействие с OpenCL.
class CNeuronSTEFlowNetResidualBlock : public CNeuronBaseOCL { protected: CLayer cBlock; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSTEFlowNetResidualBlock(void) {}; ~CNeuronSTEFlowNetResidualBlock(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels, uint units, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch); //-- virtual int Type(void) override const { return defNeuronSTEFlowNetResidualBlock; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { }; virtual void TrainMode(bool flag) override; virtual bool Clear(void) override; };
В представленной структуре объявлен лишь один внутренний компонент cBlock — динамический массив объектов, который мы наполняем при инициализации.
Метод Init можно представить как формирование полноценной команды специалистов для анализа рынка.
bool CNeuronSTEFlowNetResidualBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels, uint units, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, 2 * chanels * units, optimization_type, batch)) return false; activation = None;
Сначала мы проверяем готовность родительского класса к работе. Это как убедиться, что офис и инфраструктура готовы к приему сотрудников. Если базовая настройка не удалась — блок не запускается.
Далее мы очищаем рабочие столы и подключаем вычислительный суперкомпьютер OpenCL, чтобы вся команда могла эффективно обрабатывать информацию.
CNeuronSpikeResNeXtBottleneck* conv = NULL; CNeuronSpikeResNeXtBlock* residual = NULL; //--- cBlock.Clear(); cBlock.SetOpenCL(OpenCL);
Первым в команду входит Bottleneck — главный аналитик, который фильтрует и сжимает анализируемые данные, выделяя ключевые признаки. Он получает свои инструменты: каналы, группы, батчи — и становится готов выполнять анализ. Если он по какой-то причине не готов, запуск команды прерывается — нет смысла продолжать без ведущего аналитика.
uint index = 0; conv = new CNeuronSpikeResNeXtBottleneck(); if(!conv || !conv.Init(0, index, OpenCL, chanels, chanels, units, units, group_size, groups, optimization, iBatch) || !cBlock.Add(conv)) { DeleteObj(conv) return false; }
Затем в команду добавляются два блока с остаточными связями (Residual Blocks). Их роль — проверка и уточнение анализа, поддержка стратегии и сохранение важной информации. Каждый новый аналитик проходит инициализацию, получает доступ к ресурсам и включается в общий поток работы. Если хотя бы один блок не создаётся, команда останавливается, чтобы не запускать неполную стратегию.
for(int i = 0; i < 2; i++) { index++; residual = new CNeuronSpikeResNeXtBlock(); if(!residual || !residual.Init(0, index, OpenCL, chanels, chanels, units, units, group_size, groups, optimization, iBatch) || !cBlock.Add(residual)) { DeleteObj(residual) return false; } } //--- return true; }
Когда все внутренние компоненты успешно добавлены, команда полностью готова к работе. Блок может принимать сигналы, перерабатывать данные и передавать результаты дальше. В итоге cBlock становится сплочённой командой, где каждый сотрудник знает свою задачу, взаимодействует с коллегами и обеспечивает точность прогнозов даже в условиях высокой динамики рынка.
Алгоритм прямого прохода реализован в методе feedForward, который можно представить как работу слаженной команды аналитиков. Каждый из них обрабатывает поток информации по очереди.
bool CNeuronSTEFlowNetResidualBlock::feedForward(CNeuronBaseOCL *NeuronOCL) { if(cBlock.Total() <= 0) return false;
Сначала проверяем, есть ли члены нашей команды. Без них никакой работы не получится. Затем данные поступают к первому аналитику из внутреннего массива cBlock.
CNeuronBaseOCL* prev = NeuronOCL; CNeuronBaseOCL* current = NULL; for(int i = 0; i < cBlock.Total(); i++) { current = cBlock[i]; if(!current || !current.FeedForward(prev)) return false; prev = current; }
Каждый следующий член команды получает результаты предыдущего, обрабатывает их по своим правилам и передаёт дальше, словно серия специалистов уточняет прогноз и фильтрует шумы рынка. Если кто-то из команды не справляется, весь процесс останавливается, чтобы не исказить результаты.
Когда сигнал проходит через всех аналитиков, их совместная работа объединяется с исходными данными блока. Это как если бы главный аналитик собрал все отчёты, добавил к ним исходную информацию и сформировал полное, сбалансированное представление о текущей ситуации.
if(!Concat(prev.getOutput(), NeuronOCL.getOutput(), Output, 1, 1, Neurons() / 2)) return false; //--- return true; }
После этого Residual-блок возвращает результат, готовый к дальнейшему использованию в вычислительном тракте модели.
Линейная структура прямого прохода делает распространение градиентов ошибки достаточно простым и прозрачным. Поэтому методы обратного прохода оставлены для самостоятельного изучения читателем. Полный исходный код класса и всех его методов представлен во вложении к статье, что позволит подробно ознакомиться с реализацией Residual-блока и его внутренней логикой.
Декодер
Следующим этапом нашей работы становится построение Декодера, ключевого компонента вычислительного тракта, который превращает скрытые представления Энкодера в полезную для анализа информацию. Особое внимание здесь следует уделить двум внутренним информационным потокам, которые обеспечивают более глубокое и точное восстановление динамики исходных данных. Кроме того, использование латентных состояний Энкодера позволяет Декодеру не просто реконструировать данные, а формировать целостное представление динамики системы, интегрируя краткосрочные колебания и долгосрочные тенденции. В финансовом контексте это можно интерпретировать как способность модели учитывать мгновенные рыночные импульсы, отдельные сделки и общие тренды, что критически важно для построения прогнозов и стратегий.
Архитектура Декодера предусматривает последовательную обработку каждого уровня признаков с параллельным учётом латентного состояния, что усиливает точность прогноза и устойчивость модели к шуму и редким аномалиям. Каждый этап декодирования включает в себя транспонированные свёртки и пропускные соединения, позволяя восстанавливать пространственные и временные характеристики с высокой детализацией. Такой подход обеспечивает сохранение критически важной информации о локальных микродвижениях и глобальных фазах, создавая полноценную основу для дальнейшего анализа и принятия решений.
В конечном счёте, Декодер служит мостом между внутренними представлениями Энкодера и практическими приложениями модели, превращая скрытую динамику в формализуемую прогностическую структуру, которая может использоваться для оценки рисков, выявления ключевых точек рынка и построения адаптивных стратегий.
Мы строим Декодер в виде отдельного нового класса CNeuronSTEFlowNetDecoder, который выполняет ключевую роль в превращении скрытых состояний Энкодера в прогностические карты потока. Декодер организован вокруг двух внутренних компонентов — cMainLine и cFlow, которые обрабатывают основные признаки и промежуточные потоки соответственно. Кроме того, он хранит указатель на Энкодер cEncoder, что позволяет использовать латентные состояния для точного восстановления динамики.
class CNeuronSTEFlowNetDecoder : public CNeuronSpikeActivation { protected: CLayer cMainLine; CLayer cFlow; CNeuronSTEFlowNetEncoder* cEncoder; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSTEFlowNetDecoder(void) {}; ~CNeuronSTEFlowNetDecoder(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &chanels[], uint &units[], uint group_size, uint groups, uint heads, uint dimension_k, CNeuronSTEFlowNetEncoder* encoder, ENUM_OPTIMIZATION optimization_type, uint batch); //-- virtual int Type(void) override const { return defNeuronSTEFlowNetDecoder; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual void SetOpenCL(COpenCLMy *obj) override; virtual void SetEncoder(CNeuronSTEFlowNetEncoder* encoder) { cEncoder = encoder; } //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void TrainMode(bool flag) override; virtual bool Clear(void) override; };
Инициализация объекта Декодера представляет собой тщательный и поэтапный процесс, где каждый элемент декодера создаётся, настраивается и добавляется в соответствующие информационные потоки — cMainLine и cFlow.
bool CNeuronSTEFlowNetDecoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &chanels[], uint &units[], uint group_size, uint groups, uint heads, uint dimension_k, CNeuronSTEFlowNetEncoder *encoder, ENUM_OPTIMIZATION optimization_type, uint batch) { if(chanels.Size() != units.Size()) return false; //--- const uint layers = chanels.Size() - 1; if(layers < 1) return false;
Сначала метод проверяет корректность полученных параметров: количество элементов массива описания каналов должно совпадать с размером массива количества элементов в анализируемых последовательностях, а общее число слоёв должно быть не меньше одного.
Затем вызывается инициализация родительского класса, призванная подготовить блок к работе, задавая размеры тензора результатов и метод оптимизации параметров.
if(!CNeuronSpikeActivation::Init(numOutputs, myIndex, open_cl, units[layers]*chanels[layers], optimization_type, batch)) return false; cEncoder = encoder; uint enc_layers = (!cEncoder ? 0 : cEncoder.Layers());
После этого сохраняется указатель на Энкодер, чтобы Декодер имел доступ к латентным состояниям, и определяется количество слоёв Энкодера для корректного взаимодействия.
Внутренние массивы cMainLine и cFlow очищаются и подключаются к OpenCL-контексту, после чего начинается поэтапное формирование блоков.
CNeuronSSAMResNeXtBlock* conv_res = NULL; CNeuronConvOCL* conv = NULL; CNeuronBatchNormOCL* norm = NULL; CNeuronSpikeActivation* sa = NULL; CNeuronSpikeConvGRU* gru = NULL; CNeuronBaseOCL* concat = NULL; //--- cMainLine.Clear(); cFlow.Clear(); cMainLine.SetOpenCL(OpenCL); cFlow.SetOpenCL(OpenCL);
На первом шаге создаётся основной Residual-блок CNeuronSSAMResNeXtBlock, который инициирует главный поток обработки признаков.
uint index = 0; //--- Main Line conv_res = new CNeuronSSAMResNeXtBlock(); if(!conv_res || !conv_res.Init(0, index, OpenCL, chanels[0], chanels[1], units[0], units[1], group_size, groups, heads, dimension_k, optimization, iBatch) || !cMainLine.Add(conv_res)) { DeleteObj(conv_res) return false; }
Затем добавляется обычный сверточный слой CNeuronConvOCL, активируемый функцией SoftPlus. И слой пакетной нормализации CNeuronBatchNormOCL, который обеспечивает стабильность и согласованность данных.
index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, chanels[1], chanels[1], chanels[1], units[1], 1, optimization, iBatch) || !cMainLine.Add(conv)) { DeleteObj(conv) return false; } conv.SetActivationFunction(SoftPlus); index++; norm = new CNeuronBatchNormOCL(); if(!norm || !norm.Init(0, index, OpenCL, conv.Neurons(), iBatch, optimization) || !cMainLine.Add(norm)) { DeleteObj(norm) return false; } norm.SetActivationFunction(None);
Дальнейшие слои формируют чередующиеся блоки. Spike Activation в основном потоке для нелинейной активации и SSAM ResNeXt в потоке Flow, обрабатывающем объединённые локальные и глобальные признаки с использованием латентных состояний Энкодера.
uint ch_in = chanels[0]; //--- for(uint i = 1; i < layers; i++) { //--- Main Line index++; sa = new CNeuronSpikeActivation(); if(!sa || !sa.Init(0, index, OpenCL, norm.Neurons(), optimization, iBatch) || !cMainLine.Add(sa)) { DeleteObj(sa) return false; } //--- Flow index++; conv_res = new CNeuronSSAMResNeXtBlock(); if(!conv_res || !conv_res.Init(0, index, OpenCL, ch_in, chanels[i], units[i - 1], units[i], group_size, groups, heads, dimension_k, optimization, iBatch) || !cFlow.Add(conv_res)) { DeleteObj(conv_res) return false; }
Каждый блок тщательно проверяется на корректность и только после успешной инициализации добавляется в соответствующую информационную магистраль. На каждом этапе создаются промежуточные объекты для конкатенации признаков и согласования размеров каналов, что обеспечивает правильную интеграцию данных из Энкодера и текущего объекта.
index++; ch_in = 2 * chanels[i]; if(enc_layers - i - 1 >= 0) { gru = cEncoder.GetLayer(enc_layers - i - 1); if(!gru) return false; if(gru.GetUnits() != units[i]) return false; ch_in += gru.GetChanels(); } concat = new CNeuronBaseOCL(); if(!concat || !concat.Init(0, index, OpenCL, ch_in * units[i], optimization, iBatch) || !cFlow.Add(concat)) { DeleteObj(concat) return false; } concat.SetActivationFunction(None);
В конце каждого цикла добавляются дополнительные Residual-блоки и сверточные слои с функцией SoftPlus и нормализацией, что завершает формирование структуры Main Line на данном уровне.
//--- Main Line conv_res = new CNeuronSSAMResNeXtBlock(); if(!conv_res || !conv_res.Init(0, index, OpenCL, ch_in, ch_in, units[i], units[i + 1], group_size, groups, heads, dimension_k, optimization, iBatch) || !cMainLine.Add(conv_res)) { DeleteObj(conv_res) return false; } index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, chanels[i + 1], chanels[i + 1], chanels[i + 1], units[i + 1], 1, optimization, iBatch) || !cMainLine.Add(conv)) { DeleteObj(conv) return false; } conv.SetActivationFunction(SoftPlus); index++; norm = new CNeuronBatchNormOCL(); if(!norm || !norm.Init(0, index, OpenCL, conv.Neurons(), iBatch, optimization) || !cMainLine.Add(norm)) { DeleteObj(norm) return false; } norm.SetActivationFunction(None); } //--- return true; }
Весь процесс повторяется для всех внутренних слоёв, создавая сложный, но согласованный механизм обработки. Он позволяет Декодеру точно восстанавливать пространственные и временные характеристики данных, интегрировать латентные состояния и готовить их для последующей интерпретации или прогноза.
Метод возвращает true только после того, как все внутренние компоненты успешно инициализированы и добавлены, обеспечивая корректное и готовое к работе состояние Декодера.
Алгоритм прямого прохода нашего Декодера организован как тщательно скоординированная последовательность обработки данных через два параллельных потока (Main Line и Flow) с интеграцией латентных состояний Энкодера.
bool CNeuronSTEFlowNetDecoder::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- Main Line CNeuronSSAMResNeXtBlock* conv_res = NULL; CNeuronConvOCL* conv = NULL; CNeuronBatchNormOCL* norm = NULL; CNeuronSpikeActivation* sa = NULL; CNeuronSpikeConvGRU* gru = NULL; //--- Flow CNeuronSSAMResNeXtBlock* flow = NULL; CNeuronBaseOCL* concat = NULL; //--- CNeuronBaseOCL* prev = NeuronOCL; int layers = (cMainLine.Total() + 1) / 4; int enc_layers = int(!cEncoder ? 0 : cEncoder.Layers()); int enc_dim = 0;
Сначала метод подготавливает ссылки на все внутренние блоки: Residual-блоки, сверточные слои, нормализацию, Spike-активации и ConvGRU для основного потока, а также блоки SSAM ResNeXt и объекты для конкатенации в потоке Flow. Переменная prev хранит указатель на объект с данными предыдущего шага, чтобы обеспечить последовательную обработку. А layers и enc_layers содержат количество уровней Декодера и Энкодера, соответственно, определяя порядок интеграции латентных состояний.
Декодирование происходит поэтапно. Сначала через основной поток проходят Residual-блок, сверточный слой и нормализация.
for(int i = 0; i < layers; i++) { conv_res = cMainLine[i * 4]; conv = cMainLine[i * 4 + 1]; norm = cMainLine[i * 4 + 2]; sa = cMainLine[i * 4 + 3]; if(enc_layers - i - 2 >= 0) { gru = cEncoder.GetLayer(uint(enc_layers - i - 2)); if(!gru) return false; enc_dim = (int)gru.GetChanels(); } else { gru = NULL; enc_dim = 0; } flow = cFlow[i * 2]; concat = cFlow[i * 2 + 1]; //--- Main Line if(!conv_res || !conv_res.FeedForward(prev)) return false; if(!conv || !conv.FeedForward(conv_res)) return false; if(!norm || !norm.FeedForward(conv)) return false; if(i == (layers - 1)) break;
Далее, если это не последний слой, активируется Spike-слой для нелинейного преобразования.
if(!sa || !sa.FeedForward(norm)) return false;
Параллельно поток Flow обрабатывает исходные данные и добавляет промежуточные представления, полученные от Энкодера через ConvGRU. На каждом уровне выходы этих двух потоков объединяются в объект concat, который аккуратно соединяет локальные признаки из Main Line, промежуточные карты Flow и, при наличии, латентные состояния Энкодера, создавая согласованное представление для следующего уровня.
//--- Flow if(!flow || !flow.FeedForward(prev)) return false; if(!concat) return false; if(!gru) { if(!Concat(sa.getOutput(), flow.getOutput(), concat.getOutput(), conv.GetFilters(), flow.GetChannelsOut(), conv.GetUnits())) return false; } else { if(conv.GetUnits() != gru.GetUnits()) return false; if(!Concat(sa.getOutput(), flow.getOutput(), gru.getOutput(), concat.getOutput(), conv.GetFilters(), flow.GetChannelsOut(), enc_dim, conv.GetUnits())) return false; } prev = concat; }
При этом алгоритм учитывает соответствие размерностей каналов и числа единиц между слоями Декодера и Энкодера, что обеспечивает корректное объединение информации без потери значимых деталей.
После прохождения всех слоёв Декодера вызывается финальный Spike-Activation средствами родительского объекта, который формирует окончательный результат работы Декодера.
if(!CNeuronSpikeActivation::feedforward(norm)) return false; //--- return true; }
В результате такой структуры Декодер способен аккумулировать локальные и глобальные признаки, интегрировать латентные состояния Энкодера и преобразовывать их в полное, готовое к использованию прогнозное представление. Это особенно важно для финансовых потоков, где требуется учитывать и микродвижения цен, и долгосрочные тенденции одновременно.
Однако прямой проход формирует лишь предварительный прогноз, основанный на текущих параметрах модели, который без обучения мало информативен. Для того чтобы модель действительно научилась выявлять закономерности и адекватно прогнозировать динамику, необходимо реализовать алгоритмы обратного прохода, позволяющие корректировать веса на основе ошибки между предсказанием и целевым значением.
Особое внимание следует уделить многопоточной архитектуре Декодера. Параллельные потоки Main Line и Flow, а также интеграция латентных состояний Энкодера создают сложную сеть зависимостей, где ошибка одного потока влияет на другие. Это требует тщательного подхода к распространению градиентов, чтобы корректно учитывать вклад каждого блока и избежать искажения информации при обновлении весов.
Следовательно, обратный проход в CNeuronSTEFlowNetDecoder должен не просто переносить ошибку назад, а аккуратно агрегировать её через все ветви, правильно комбинируя локальные, глобальные и латентные признаки. Только при таком подходе обучение становится эффективным, позволяя модели адаптироваться к сложным временно-пространственным закономерностям, фиксировать микродвижения и глобальные тенденции, что критически важно для анализа динамики финансовых потоков и построения надежных стратегий прогнозирования.
Алгоритм распределения градиентов ошибки реализован в методе calcInputGradients и является ключевым элементом процесса обучения модели. Он обеспечивает корректное распространение ошибки по сложной многопоточной архитектуре Декодера, аккумулируя вклад каждого блока.
bool CNeuronSTEFlowNetDecoder::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { //--- Main Line CNeuronSSAMResNeXtBlock* conv_res = cMainLine[-3]; CNeuronConvOCL* conv = cMainLine[-2]; CNeuronBatchNormOCL* norm = cMainLine[-1]; CNeuronSpikeActivation* sa = cMainLine[-4]; CNeuronSpikeConvGRU* gru = NULL; //--- Flow CNeuronSSAMResNeXtBlock* flow = cFlow[-2]; //--- int layers = cMainLine.Total() / 4; CNeuronBaseOCL* prev = (layers > 1 ? cFlow[-1] : NeuronOCL); int enc_layers = int(!cEncoder ? 0 : cEncoder.Layers()); int enc_dim = 0;
В начале метода создаются локальные переменные для хранения указателей на внутренние компоненты основного потока: Residual-блоки (conv_res), сверточные слои (conv), нормализацию (norm) и Spike-активации (sa). Для потока Flow определяются объекты flow. А переменная prev указывает на предыдущий слой, который будет участвовать в обратном распространении.
Также вычисляется количество слоёв в Декодере и Энкодере, что позволяет корректно интегрировать латентные состояния для обратного прохода.
Первым шагом вычисляются градиенты для конечного слоя нормализации.
if(!CNeuronSpikeActivation::calcInputGradients(norm)) return false;
После чего происходит последовательное вычисление градиентов для сверточных слоёв и Residual-блоков основного потока.
if(!conv || !conv.CalcHiddenGradients(norm)) return false; if(!conv_res || !conv_res.CalcHiddenGradients(conv)) return false;
Этот этап обеспечивает точное распространение ошибки через линейные и нелинейные преобразования, создавая основу для корректной коррекции весов модели.
Далее реализован цикл, проходящий от верхних слоёв к нижним.
if(!prev || !prev.CalcHiddenGradients(conv_res)) return false; //--- for(int i = layers - 1; i >= 0; i--) { if(enc_layers - i - 2 >= 0) { gru = cEncoder.GetLayer(uint(enc_layers - i - 1)); if(!gru) return false; enc_dim = (int)gru.GetChanels(); } else { gru = NULL; enc_dim = 0; } if(!sa || !flow) return false;
Для каждого уровня проверяется наличие соответствующего слоя ConvGRU в Энкодере. И если он присутствует, то включаются в общий поток для распределения градиентов, обеспечивая правильную интеграцию латентных состояний.
Внутри цикла происходит разделение градиентов между Main Line и Flow с помощью метода DeConcat, который аккуратно распределяет ошибку между локальными признаками, промежуточными картами и латентными векторами Энкодера. Это гарантирует, что каждый компонент получит адекватный сигнал для обучения.
if(!gru) { if(!DeConcat(sa.getGradient(), flow.getGradient(), prev.getGradient(), conv.GetFilters(), flow.GetChannelsOut(), conv.GetUnits())) return false; } else if(!DeConcat(sa.getGradient(), flow.getGradient(), gru.getGradient(), prev.getGradient(), conv.GetFilters(), flow.GetChannelsOut(), enc_dim, conv.GetUnits())) return false;
После распределения ошибки в Flow, осуществляется обратное распространение в Main Line: сначала через Spike-активацию, затем через нормализацию и сверточные слои в Residual-блок.
//--- Flow prev = (i > 0 ? cFlow[i * 2 - 1] : NeuronOCL); if(!prev || !prev.CalcHiddenGradients(flow)) return false; //--- Main Line conv_res = cMainLine[i * 4]; conv = cMainLine[i * 4 + 1]; norm = cMainLine[i * 4 + 2]; if(!norm.CalcHiddenGradients(sa)) return false; if(!conv || !conv.CalcHiddenGradients(norm)) return false; if(!conv_res || !conv_res.CalcHiddenGradients(conv)) return false;
Для корректного суммирования градиентов от двух информационных потоков мы используем не раз проверенный фокус с подменой указателей на буферы данных, который позволяет аккумулировать вклад, минимизируя искажения при распространении ошибки.
CBufferFloat* temp = prev.getGradient(); if(!prev.SetGradient(prev.getPrevOutput(), false) || !prev.CalcHiddenGradients(conv_res) || !SumAndNormilize(temp, prev.getGradient(), temp, 1, false, 0, 0, 0, 1) || !prev.SetGradient(temp, false)) return false; if(i==0) continue; sa = cMainLine[i * 4 - 1]; //--- Flow flow = cFlow[(i-1) * 2]; } //--- return true; }
Метод также учитывает особенности последнего слоя, корректно завершает цикл и обновляет градиенты для всех промежуточных объектов. В результате алгоритм обеспечивает стабильное и точное распространение ошибки через всю структуру декодера, позволяя сети эффективно адаптироваться к сложным временно-пространственным зависимостям, фиксировать микродвижения и глобальные тенденции, что особенно важно для финансовых потоков и стратегий прогнозирования.
Таким образом, calcInputGradients реализует полный, многопоточный механизм обратного распространения ошибки. Он гарантирует, что каждый блок и каждый канал получают корректную информацию для обновления весов, обеспечивая надежное и детализированное обучение модели.
Корректировка параметров Декодера в направлении минимизации ошибки организована через метод updateInputWeights, который выполняет непосредственное обновление весов всех внутренних компонентов модуля на основе ранее рассчитанных градиентов. Этот процесс является финальным этапом цикла обучения: после прямого прохода, построения прогноза и обратного распространения ошибки модель получает информацию о том, как каждый параметр повлиял на результат, и соответственно корректирует их для уменьшения значения функции потерь.
bool CNeuronSTEFlowNetDecoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { //--- Main Line CNeuronSSAMResNeXtBlock* conv_res = NULL; CNeuronConvOCL* conv = NULL; CNeuronBatchNormOCL* norm = NULL; CNeuronSpikeActivation* sa = NULL; CNeuronSpikeConvGRU* gru = NULL; //--- Flow CNeuronSSAMResNeXtBlock* flow = NULL; CNeuronBaseOCL* concat = NULL; //--- CNeuronBaseOCL* prev = NeuronOCL; int layers = (cMainLine.Total() + 1) / 4; int enc_layers = int(!cEncoder ? 0 : cEncoder.Layers()); int enc_dim = 0;
Метод начинается с инициализации указателей на ключевые компоненты. Кроме того, определяется количество слоёв Декодера и Энкодера, чтобы корректно интегрировать латентные состояния при обновлении весов.
Внутри цикла для каждого слоя Декодера происходит поэтапная корректировка параметров. Сначала обновляются веса Residual-блока, используя выход предыдущего слоя в качестве входного сигнала.
for(int i = 0; i < layers; i++) { conv_res = cMainLine[i * 4]; conv = cMainLine[i * 4 + 1]; norm = cMainLine[i * 4 + 2]; sa = cMainLine[i * 4 + 3]; if(enc_layers - i >= 0) { gru = cEncoder.GetLayer(uint(enc_layers - i - 1)); if(!gru) return false; enc_dim = (int)gru.GetChanels(); } else { gru = NULL; enc_dim = 0; } flow = cFlow[i * 2]; concat = cFlow[i * 2 + 1]; //--- Main Line if(!conv_res || !conv_res.UpdateInputWeights(prev)) return false;
Затем последовательно обновляются параметры сверточного слоя, нормализации и Spike-активации, обеспечивая согласованность всей Main Line и предотвращая потерю информации о градиентах на промежуточных этапах.
if(!conv || !conv.UpdateInputWeights(conv_res)) return false; if(!norm || !norm.UpdateInputWeights(conv)) return false; if(i == (layers - 1)) break; if(!sa || !sa.UpdateInputWeights(norm)) return false;
Особое внимание уделяется интеграции информации из Энкодера. Если соответствующий слой ConvGRU присутствует, его каналы и латентные векторы учитываются при обновлении весов, что позволяет Декодеру корректно учитывать долгосрочные зависимости и глобальные признаки, сохранённые на этапах кодирования сигнала.
Поток Flow обновляется параллельно с Main Line, что обеспечивает синхронизацию локальной и глобальной информации при обучении и способствует точной реконструкции фазового состояния данных.
//--- Flow if(!flow || !flow.UpdateInputWeights(prev)) return false; prev = concat; }
После прохода через все слои, обновляются веса родительского класса, что завершает процесс корректировки.
if(!CNeuronSpikeActivation::updateInputWeights(norm)) return false; //--- return true; }
Такая организация гарантирует, что все внутренние параметры Декодера получают корректные сигналы для обучения, учитывают локальные колебания входного потока и глобальные тенденции, интегрированные через Энкодер.
В целом, метод updateInputWeights реализует многопоточный и иерархический механизм обновления параметров. Каждый блок получает точный сигнал для коррекции, градиенты аккумулируются и распределяются между компонентами.
Полный код CNeuronSTEFlowNetDecoder и всех его методов представлен во вложении.
Объект верхнего уровня
Следующим этапом мы переходим к построению интегрированного объекта верхнего уровня, который объединяет все ключевые компоненты модели в единую архитектуру. После тщательной разработки отдельных модулей (Энкодера, Декодера и Residual-блока) возникает необходимость создать структуру, способную не просто последовательно обрабатывать поток событий, но и синхронизировать работу всех внутренних компонентов. Такой объект позволяет объединить пространственно-временное кодирование, точное извлечение локальных признаков и прогнозирование динамики данных в одном согласованном механизме.
Объект верхнего уровня CNeuronSTEFlowNet выступает интегратором всех ключевых компонентов модели. Он наследует базовую функциональность от CNeuronSTEFlowNetDecoder, что позволяет отказаться от создания внутреннего компонента Декодера. А его функционал реализовать средствами родительского класса.
class CNeuronSTEFlowNet : public CNeuronSTEFlowNetDecoder { //--- protected: CNeuronSTEFlowNetEncoder cSTEEncoder; CNeuronSTEFlowNetResidualBlock cSTEResidualBlock; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSTEFlowNet(void) {}; ~CNeuronSTEFlowNet(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &chanels[], uint &units[], uint group_size, uint groups, uint heads, uint dimension_k, uint stack_size, ENUM_OPTIMIZATION optimization_type, uint batch); //-- virtual int Type(void) override const { return defNeuronSTEFlowNet; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void TrainMode(bool flag) override; virtual bool Clear(void) override; };
Внутри объекта объявляется cSTEEncoder, который обеспечивает пространственно-временное кодирование входного потока событий, и cSTEResidualBlock, поддерживающий стабильное и точное извлечение локальных признаков и микродвижений. Такой подход позволяет одновременно фиксировать детальные закономерности на коротких временных интервалах и глобальные тенденции, накопленные Энкодером.
Инициализация объекта верхнего уровня STE-FlowNet начинается с проверки соответствия размеров массивов каналов и последовательностей, чтобы исключить несоответствия в конфигурации.
bool CNeuronSTEFlowNet::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &chanels[], uint &units[], uint group_size, uint groups, uint heads, uint dimension_k, uint stack_size, ENUM_OPTIMIZATION optimization_type, uint batch) { if(chanels.Size() != units.Size()) return false;
Для корректной работы декодера создаются обратные массивы каналов и размеров последовательностей, которые формируются путем реверсирования исходных массивов. При этом первый элемент массива каналов удваивается, чтобы учесть конкатенированные выходы Residual-блока и обеспечить согласованное соединение с Декодером.
uint rev_chanels[], rev_units[]; if(ArrayResize(rev_chanels, chanels.Size()) < 0 || ArrayResize(rev_units, units. Size()) < 0) return false; uint total = chanels.Size(); for(uint i = 0; i < total; i++) rev_chanels[total - i - 1] = chanels[i]; rev_chanels[0] *= 2; //--- for(uint i = 0; i < total; i++) rev_units[total - i - 1] = units[i];
Далее осуществляется инициализация каждого ключевого компонента. Энкодер создается с использованием исходных массивов каналов и размеров последовательностей, с заданными параметрами группировки, голов внимания и размерности латентного пространства.
if(!cSTEEncoder.Init(0, 0, open_cl, chanels, units, group_size, groups, heads, dimension_k, stack_size, optimization_type, batch)) return false;
После успешного запуска Энкодера инициализируется Residual-блок, который обрабатывает результаты Энкодера и формирует устойчивое представление динамики.
if(!cSTEResidualBlock.Init(0, 1, open_cl, chanels[total - 1], units[total - 1], group_size, groups, optimization, iBatch)) return false;
Последним шагом инициализируется Декодер с использованием обратных массивов каналов и размеров последовательностей. Он получает указатель на объект Энкодера, что позволяет использовать латентные состояния для построения точных промежуточных представлений потока событий.
rev_chanels[0] = cSTEResidualBlock.Neurons() / rev_units[0]; if(!CNeuronSTEFlowNetDecoder::Init(numOutputs, myIndex, open_cl, rev_chanels, rev_units, group_size, groups, heads, dimension_k, cSTEEncoder.AsObject(), optimization_type, batch)) return false; //--- return true; }
После успешного выполнения всех шагов, объект готов к работе, обеспечивая интеграцию Энкодера, Декодера и Residual-блока в единую, скоординированную модель обработки пространственно-временных данных.
Прямой проход STE-FlowNet организован как последовательная передача данных через все ключевые компоненты модели. Сначала входной сигнал подается в Энкодер, который формирует латентное представление анализируемых событий, аккумулируя пространственно-временную информацию и извлекая значимые признаки.
bool CNeuronSTEFlowNet::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cSTEEncoder.FeedForward(NeuronOCL)) return false;
Затем результат работы Энкодера передается в Residual-блок, который усиливает выделенные признаки и сохраняет критические локальные детали. Этот блок обеспечивает устойчивость модели к шумам и повышает точность последующих вычислений, формируя основу для корректного декодирования.
if(!cSTEResidualBlock.FeedForward(cSTEEncoder.AsObject())) return false;
Наконец, обновленные данные поступают в Декодер, который на основе латентного состояния Энкодера и выходов Residual-блока строит прогноз потока событий.
return CNeuronSTEFlowNetDecoder::feedForward(cSTEResidualBlock.AsObject());
}
Такой поэтапный подход обеспечивает надежную обработку каждого события. Учитывает локальные микродвижения и глобальные тенденции, создавая согласованное и информативное представление динамики системы. В финансовом контексте это аналогично последовательной обработке анализируемых сделок и изменений цен для построения прогноза движения рынка с высокой детализацией и адаптивностью.
В итоге, CNeuronSTEFlowNet представляет собой полностью интегрированный объект, способный последовательно анализировать поток событий, фиксировать критические микродвижения, учитывать глобальные закономерности и динамически корректировать свои параметры для построения надежных прогнозов. В контексте финансовых рынков это означает возможность одновременно отслеживать мгновенные колебания цен и долгосрочные тренды, создавая адаптивную и высокоточную модель анализа для стратегического принятия решений и управления рисками.
Полный код класса и всех его методов представлен во вложении к статье.
Создание объекта верхнего уровня открывает новые возможности. Теперь все ранее реализованные модули объединяются в единую структуру и могут быть интегрированы в модели буквально как один слой. Такой подход значительно упрощает построение сложных нейросетевых архитектур и делает их интеграцию в торговые стратегии максимально удобной и наглядной.
Сегодня мы не будем углубляться в подробное описание архитектуры каждой обучаемой модели, поскольку основной фокус. Все детали реализации и структуры можно подробно изучить во вложении к статье.
Тестирование
Мы подошли к завершающему этапу работы с фреймворком STE-FlowNet — оценке эффективности реализованных подходов. Но прежде необходимо обучить нашего трейдера.
Первый этап — офлайн-обучение на исторических данных валютной пары EURUSD таймфрейм H1 за период с Января 2024 по Июнь 2025 года. Этот временной отрезок стал полноценной тренировочной площадкой. Модель училась распознавать исторические паттерны, анализировать динамику цен и объёмы сделок, выявлять взаимосвязи между ключевыми признаками. STE-FlowNet формировал собственную интуицию трейдера, объединяя её с точной стратегической оценкой, постепенно нарабатывая способность прогнозировать движение рынка и оценивать риск каждой потенциальной сделки.
Следующий этап — тонкая онлайн-настройка в тестере стратегий MetaTrader 5 — переносит обучение в более реалистичные условия. Свеча за свечой модель обрабатывает потоки данных. Здесь STE-FlowNet осваивает динамику рынка, учится сохранять устойчивость на фоне шумовых колебаний, корректировать действия при низкой ликвидности и мгновенно реагировать на резкие ценовые всплески. Базовая структура, сформированная на исторических данных, остаётся неизменной, но модель учится гибко реагировать на текущую рыночную обстановку, минимизируя риск переобучения и повышая точность прогнозов даже в нестабильной среде.
Финальная проверка проводилась на полностью новых данных за Июль—Сентябрь2025 года. Все параметры, полученные на предыдущих этапах, загружались без изменений, что обеспечивало честную оценку способности STE-FlowNet к обобщению.

Результаты тестирования на исторических данных выглядят убедительно и демонстрируют стабильность стратегии. Начнем с финансовых показателей. При начальном депозите $100 чистая прибыль составила $54,02, что соответствует общему росту капитала более чем на 50% за тестируемый период. Валовая прибыль достигла $119,57, при этом валовые убытки составили $65,55, что дает коэффициент прибыльности (Profit Factor) 1,82. Этот показатель отражает, что стратегия генерирует почти в два раза больше дохода по сравнению с убытками, что считается достойным результатом для торговых алгоритмов. Recovery Factor1,31 указывает на способность стратегии восстанавливаться после просадок.
Просадки капитала находятся на приемлемом уровне. Максимальная просадка по Эквити составила 28,57%, что является разумным для активного алгоритмического подхода на валютном рынке. При этом абсолютная просадка баланса была 25,13%, демонстрируя способность стратегии эффективно удерживает позиции и не приводит к резкому падению капитала.
Результаты по сделкам также показывают сбалансированность стратегии. Всего было совершено 50 сделок, из которых 62% оказались прибыльными. Процент выигрышных коротких-позиций составил 63,16%, а длинных — 61,29%, что говорит о корректной работе алгоритма как в нисходящем, так и в восходящем тренде. Средний профит на сделку $3,86, а средний убыток $3,45 — соотношение положительных и отрицательных сделок хорошо сбалансировано. Наибольшая прибыль в одной сделке составила $15,03, а наибольший убыток — $9,96.
В целом, тесты подтверждают, что STE-FlowNet успешно обучается на исторических данных, правильно реагирует на рыночные сигналы и демонстрирует стабильный рост капитала. Стратегия способна сочетать точность прогнозов с контролем риска. Однако перед использованием её в реальном торговом процессе необходимо более тщательное и всестороннее тестирование.
Заключение
В данной статье мы подробно рассмотрели архитектуру и ключевые компоненты фреймворка STE-FlowNet, демонстрируя его гибкость и адаптивность к различным задачам анализа данных. Проведённые тесты подтвердили высокую эффективность реализованных решений, показывая стабильность и точность работы даже на сложных временных рядах. Особое внимание уделено уникальной способности фреймворка интегрировать многопоточную обработку и рекуррентные структуры, что обеспечивает ускорение вычислений и более глубокое моделирование зависимости данных. Представленный подход открывает новые возможности для практического применения в трейдинге и смежных областях, позволяя создавать стратегически значимые прогнозы с минимальными ресурсными затратами.
Ссылки
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Study.mq5 | Советник | Советник офлайн обучения моделей |
| 2 | StudyOnline.mq5 | Советник | Советник онлайн обучения моделей |
| 3 | Test.mq5 | Советник | Советник для тестирования модели |
| 4 | Trajectory.mqh | Библиотека класса | Структура описания состояния системы и архитектуры моделей |
| 5 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
| 6 | NeuroNet.cl | Библиотека | Библиотека кода OpenCL-программы |
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Таблицы в парадигме MVC на MQL5: настраиваемые и сортируемые столбцы таблицы
От новичка до эксперта: Создание анимированного советника для новостей в MQL5 (X) — Представление графика с несколькими символами для торговли на новостях
От новичка до эксперта: Анимированный советник News Headline с использованием MQL5 (XI) - Корреляция при торговле на новостях
Осваиваем JSON: Разработка пользовательского JSON-ридера с нуля на MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования