Нейросети в трейдинге: Поиск устойчивых закономерностей в разнородных рыночных данных (Окончание)
Введение
В предыдущих публикациях мы последовательно адаптировали ключевые идеи INFNet к практическим условиям алгоритмической торговли. Было показано, что речь идёт о фундаментальном подходе — управлении потоками информации между разнородными сигналами.
В классических моделях анализа рынка основная проблема проявляется довольно быстро: либо модель стремится учесть все возможные взаимосвязи и упирается в вычислительную сложность, либо упрощает структуру и теряет значимую часть сигнала. INFNet предлагает компромисс, который на практике оказывается куда более зрелым: не увеличивать число взаимодействий, а структурировать их. Введённое разделение на типы токенов — последовательные, контекстные и сценарные — позволяет разнести источники информации по ролям и избежать преждевременного смешивания сигналов. Это, в свою очередь, снижает шум и повышает устойчивость модели на нестационарных данных рынка.
Отдельного внимания заслуживает механизм hub-токенов, а также связанный с ним цикл агрегации и распространения информации. Вместо прямого взаимодействия всех со всеми формируется управляемый центр, через который проходит глобальный контекст. Такой подход снижает вычислительную сложность до линейной и сохраняет структуру сигнала. Глобальная информация не теряется в процессе сжатия — она возвращается обратно к токенам через управляемый broadcast, где учитывается локальное состояние каждого элемента. В условиях рынка, где слабые сигналы часто важнее сильных, но запаздывающих, это даёт заметное преимущество.
На предыдущих этапах была заложена практическая основа реализации. Мы сформировали механизм генерации токенов, адаптировали обработку последовательных и контекстных признаков, реализовали узлы агрегации и настроили передачу сигналов через Broadcast Gated Unit. Отдельный акцент был сделан на формировании общего embedding-пространства, в котором различные типы признаков могут взаимодействовать без потери семантики. Архитектура перестала быть набором разрозненных компонентов и приобрела очертания целостной системы, пусть пока ещё и на уровне отдельных модулей.
Однако до настоящего момента все элементы существовали скорее как хорошо подогнанные детали, чем как единый механизм. Это типичная стадия любой инженерной разработки: компоненты уже работают, но система ещё не дышит как целое. Именно этот разрыв мы и закрываем в текущей статье.
Задача данного этапа предельно конкретна. Мы создаём объект верхнего уровня, который объединяет все ранее реализованные компоненты в единый вычислительный конвейер. Это не формальная сборка, а структурирование потока данных: от входных признаков — через локальную обработку, агрегацию, распространение контекста — к формированию итогового представления и прогнозу. Важно не просто связать блоки. Нужно обеспечить согласованное движение сигнала между ними и сохранить управляемость и интерпретируемость модели.
После сборки конвейера модель впервые будет рассмотрена как рабочий инструмент. Мы проведём тестирование на реальных исторических данных, чтобы оценить поведение модели в условиях рыночной динамики.

Объект верхнего уровня
Логика предыдущего этапа подводит нас к вполне ожидаемому, но принципиально важному шагу. До этого момента мы последовательно строили архитектуру снизу вверх: прорабатывали отдельные механизмы, проверяли их устойчивость, согласовывали форматы данных. Каждый компонент был оправдан сам по себе. Однако рынок — среда непрерывная. Здесь не работают изолированные блоки. Здесь работает только система, в которой сигнал проходит весь путь без потери смысла.
Именно поэтому дальнейшее развитие невозможно без формализации верхнего уровня. Речь идёт не о декоративной оболочке или о классе, который просто вызывает всё по очереди. Такой подход быстро превращает архитектуру в хрупкую конструкцию, где любое изменение тянет за собой каскад ошибок. Задача ставится строже: определить объект, который будет управлять логикой прохождения данных через все компоненты.
Мы переходим от набора вычислительных примитивов к управляемому конвейеру обработки информации. На этом уровне нужно зафиксировать: порядок стадий, формат передачи данных, согласование размерностей и точки преобразования сигнала. Если на уровне отдельных блоков мы говорили о том, как обрабатывается информация, то теперь нас интересует, как она движется.
Верхнеуровневый объект в такой архитектуре выполняет сразу несколько функций. Во-первых, он инкапсулирует все ранее реализованные модули, устраняя необходимость прямого взаимодействия между ними. Это делает систему управляемой. Во-вторых, он задаёт строгую последовательность этапов: формирование токенов, локальная обработка, агрегация через hub-механизм, распространение контекста и финальное формирование представления. В-третьих, именно на этом уровне становится возможным контроль целостности данных — от входа до выхода.
Класс CNeuronINFNetBlock по своей роли ближе к узлу обработки потока, внутри которого последовательно реализуются все ключевые стадии: подготовка данных, локальная обработка, агрегация и управляемое распространение контекста. Важно понимать: именно такие блоки, соединённые в стек, и формируют поведение всей модели.
class CNeuronINFNetBlock : public CNeuronPerTokenSwiGLU { protected: uint iDimension; uint iSequenceUnits; uint iSequenceVar; uint iContextUnits; uint iScenariosUnits; CNeuronUniMixerTokenizer cPrepare; CLayer cSequence; CLayer cContext; CLayer cScenarios; CLayer cHUBs; //--- virtual bool INFNetBGU(CNeuronBaseOCL* scale_and_shift, CNeuronBaseOCL* sequence_in, CNeuronBaseOCL* sequence_out, CNeuronBaseOCL* context_in, CNeuronBaseOCL* context_out, CNeuronBaseOCL* scenarios_in, CNeuronBaseOCL* scenarios_out); virtual bool INFNetBGUGrad(CNeuronBaseOCL* scale_and_shift, CNeuronBaseOCL* sequence_in, CNeuronBaseOCL* sequence_out, CNeuronBaseOCL* context_in, CNeuronBaseOCL* context_out, CNeuronBaseOCL* scenarios_in, CNeuronBaseOCL* scenarios_out); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronINFNetBlock(void) {}; ~CNeuronINFNetBlock(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &dimensions[], uint units_s, uint heads, uint stack_size, uint layers, uint embed_size, uint candidates, uint topK, 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 defNeuronINFNetBlock; } virtual void SetOpenCL(COpenCLMy *obj) override; virtual void TrainMode(bool flag) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual bool Clear(void) override; };
Первое, что обращает на себя внимание — наследование от CNeuronPerTokenSwiGLU. Это решение задаёт тон всей реализации. Мы не строим логику с нуля, а опираемся на уже проверенный механизм. На практике это означает, что каждый токен обрабатывается независимо на базовом уровне.
Далее идёт группа параметров, которые задают геометрию пространства:
- iDimension — размерность embedding-пространства;
- iSequenceUnits, iContextUnits, iScenariosUnits — размерность соответствующих потоков;
- iSequenceVar — количество унитарных рядов в последовательных данных.
Это способ зафиксировать баланс между различными источниками информации. В рыночных задачах перекос в любую сторону быстро приводит к деградации: либо модель залипает в истории, либо переоценивает текущий контекст.
Ключевой архитектурный момент — разделение потоков:
- cSequence — обработка временных структур;
- cContext — агрегированные признаки среды;
- cScenarios — сценарные / целевые представления;
- cHUBs — центр агрегации.
Именно здесь проявляется исходная идея INFNet: разделить потоки, прежде чем их объединять. Каждый поток работает в своей логике, не мешая другим, и только затем информация сводится через hub-механизм.
Метод инициализации выполняет настройку всей системы координат, в которой будет жить модель. Именно в этом этапе определяется баланс сигналов, их относительная масса и то, как они будут конкурировать за внимание модели в процессе обучения.
bool CNeuronINFNetBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint &dimensions[], uint units_s, uint heads, uint stack_size, uint layers, uint embed_size, uint candidates, uint topK, ENUM_OPTIMIZATION optimization_type, uint batch) { if(dimensions.Size() <= units_s || layers < 1 || units_s < 1) ReturnFalse; iSequenceVar = units_s; iSequenceUnits = stack_size; iContextUnits = dimensions.Size() - units_s; iDimension = embed_size; iScenariosUnits = iSequenceVar + iContextUnits + 1; uint inside_embed_size = (embed_size + 1) / 2;
Первые проверки выглядят формально, но по сути они отсеивают ситуации, при которых модель теряет смысл ещё до запуска. В задачах финансовых рынков это особенно чувствительно. Если последовательная часть оказывается слишком короткой, модель перестаёт видеть динамику. Если контекст перегружен, она начинает верить фону сильнее, чем самому движению цены. Поэтому уже на уровне входных параметров фиксируется базовый баланс: что считать историей, что — состоянием среды, а что — задачей.
Сначала управление передаётся родительскому классу. В него передаётся сформированное объединённое пространство токенов: сценарные, последовательные и дополнительный служебный канал.
if(!CNeuronPerTokenSwiGLU::Init(numOutputs, myIndex, open_cl, iDimension, iScenariosUnits + iSequenceVar + 1, inside_embed_size, candidates, topK, optimization_type, batch)) ReturnFalse;
Это важный момент. Блок сразу настраивается на работу с полной картиной, а не с отдельными фрагментами. В терминах рынка это означает, что модель с самого начала ориентирована на совместную обработку истории, контекста и цели, а не на их разрозненное преобразование.
Следом инициализируется объект первичной подготовки данных cPrepare. Здесь происходит приведение исходных данных к единому формату.
//--- Prepare uint index = 0; if(!cPrepare.Init(0, index, OpenCL, dimensions, iSequenceVar + iContextUnits, iDimension, iSequenceVar + iContextUnits, candidates, topK, optimization, iBatch)) ReturnFalse;
На вход подаётся сырая структура, а на выходе формируется пространство фиксированной размерности iDimension, в котором есть только токены с согласованной семантикой. Причём сохраняется разделение на последовательную и контекстную часть, но уже в одном embedding-пространстве. Это как переход от разрозненных рыночных показателей к единой шкале, где цена, волатильность и производные величины становятся сопоставимыми.
После этого блоки очищаются и привязываются к OpenCL-контексту.
cSequence.Clear(); cContext.Clear(); cScenarios.Clear(); cHUBs.Clear(); cSequence.SetOpenCL(OpenCL); cContext.SetOpenCL(OpenCL); cScenarios.SetOpenCL(OpenCL); cHUBs.SetOpenCL(OpenCL);
Это технический шаг, но он подчёркивает важную особенность реализации: вся архитектура изначально рассчитана на параллельные вычисления. В условиях обработки временных рядов это не роскошь, а необходимость. Рынок не ждёт, пока модель додумает.
Далее начинается формирование потока анализа последовательностей. Сначала создаётся базовый линейный слой, который принимает эмбеддинги описания текущего рыночного состояния.
//--- Sequence index++; CNeuronBaseOCL* neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, iDimension * iSequenceVar, optimization, iBatch) || !cSequence.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None);
Затем данные укладываются в стек через CNeuronAddToStack, где формируется структурированное представление последовательности с учётом заданной глубины iSequenceUnits.
index++; CNeuronAddToStack* stack = new CNeuronAddToStack(); if(!stack || !stack.Init(0, index, OpenCL, iSequenceUnits, iDimension, iSequenceVar, optimization, iBatch) || !cSequence.Add(stack)) DeleteObjAndFalse(stack);
После этого следует серия базовых нейронных слоев без активации, в которые мы будем сохранять промежуточные данные после broadcast.
for(uint i = 0; i < layers; i++) { index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, stack.Neurons(), optimization, iBatch) || !cSequence.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None); }
Аналогично строится контекстный блок. Только в данном случае мы не накапливаем историю в стек.
//--- Context for(uint i = 0; i <= layers; i++) { index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, iDimension * iContextUnits, optimization, iBatch) || !cContext.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None); }
Сценарный блок вводится отдельно и сразу задаёт структуру объединения данных. На этом этапе формируется пространство, в котором уже можно говорить о задаче как таковой.
//--- Scenarios index++; CNeuronINFNetScenarios* scen = new CNeuronINFNetScenarios(); uint fields[] = {iSequenceVar, iContextUnits, 1}; if(!scen || !scen.Init(0, index, OpenCL, iDimension, fields, iDimension, inside_embed_size, candidates, topK, optimization, iBatch) || !cScenarios.Add(scen)) DeleteObjAndFalse(scen);
Дальнейшие базовые слои, как и в других модальностях, используются для хранения обновлённых данных.
for(uint i = 0; i < layers; i++) { index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, scen.Neurons(), optimization, iBatch) || !cScenarios.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None); }
После подготовки всех потоков инициализация переходит к ядру — блоку HUB. Сначала создаются два отдельных агрегатора: один для последовательности, другой для контекста.
//--- HUBs index++; CNeuronINFNetHUBSequence* hub = new CNeuronINFNetHUBSequence(); if(!hub || !hub.Init(0, index, OpenCL, iDimension, iSequenceUnits, iDimension, iSequenceVar, optimization, iBatch) || !cHUBs.Add(hub)) DeleteObjAndFalse(hub); hub = new CNeuronINFNetHUBSequence(); if(!hub || !hub.Init(0, index, OpenCL, iDimension, iContextUnits, iDimension, 1, optimization, iBatch) || !cHUBs.Add(hub)) DeleteObjAndFalse(hub);
Это разделение сохраняет дисциплину потоков и не допускает преждевременного смешивания сигналов. Затем вводится объединяющий слой, который собирает в единое пространство выход сценарного блока и агрегированные данные последовательной и контекстной составляющих.
index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, scen.Neurons() + iDimension * (iSequenceVar + 1), optimization, iBatch) || !cHUBs.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None);
Далее запускается основной цикл. Число итераций задаётся параметром метода. На каждом шаге последовательно создаются три блока кросс-внимания CNeuronDomainAwareAttention. Каждый из них работает со своим типом контекста: сначала последовательность, затем контекст, затем сценарии.
CNeuronDomainAwareAttention* att = NULL; CNeuronFieldAwareConv* conv = NULL; for(uint i = 0; i < layers; i++) { index++; att = new CNeuronDomainAwareAttention(); if(!att || !att.Init(0, index, OpenCL, iDimension, iScenariosUnits + iSequenceVar + 1, heads, iDimension, iSequenceUnits * iSequenceVar, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(att)) DeleteObjAndFalse(att); index++; att = new CNeuronDomainAwareAttention(); if(!att || !att.Init(0, index, OpenCL, iDimension, iScenariosUnits + iSequenceVar + 1, heads, iDimension, iContextUnits, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(att)) DeleteObjAndFalse(att); index++; att = new CNeuronDomainAwareAttention(); if(!att || !att.Init(0, index, OpenCL, iDimension, iScenariosUnits + iSequenceVar + 1, heads, iDimension, iScenariosUnits, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(att)) DeleteObjAndFalse(att);
Это не дублирование, а осознанное разнесение внимания по доменам. Модель не пытается сразу охватить всё, она поочерёдно извлекает информацию из разных источников.
Результаты этих трёх блоков объединяются в один вектор через линейный слой.
index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, 3 * att.Neurons(), optimization, iBatch) || !cHUBs.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None);
Далее включается CNeuronFieldAwareConv, который сжимает пространство и генерирует независимые коэффициенты масштабирования и смещения для каждого домена отдельно.
index++; conv = new CNeuronFieldAwareConv(); if(!conv || !conv.Init(0, index, OpenCL, 3 * iDimension, 2 * iDimension, iSequenceVar + iScenariosUnits + 1, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(conv)) DeleteObjAndFalse(conv);
Второй сверточный блок возвращает данные в агрегированное состояние. Здесь также используется Field-Aware подход.
index++; conv = new CNeuronFieldAwareConv(); if(!conv || !conv.Init(0, index, OpenCL, 3 * iDimension, iDimension, iSequenceVar + iScenariosUnits + 1, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(conv)) DeleteObjAndFalse(conv); }
Этот цикл повторяется, постепенно усиливая значимые зависимости и подавляя второстепенные. С каждой итерацией представление становится более согласованным. В терминах рынка это похоже на процесс фильтрации шума: сначала сигнал едва различим, но по мере обработки начинает проявляться структура.
После завершения цикла выполняется финальный проход через блоки внимания и свёртки, уже без повторений.
index++; att = new CNeuronDomainAwareAttention(); if(!att || !att.Init(0, index, OpenCL, iDimension, iScenariosUnits + iSequenceVar + 1, heads, iDimension, iSequenceUnits * iSequenceVar, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(att)) DeleteObjAndFalse(att); index++; att = new CNeuronDomainAwareAttention(); if(!att || !att.Init(0, index, OpenCL, iDimension, iScenariosUnits + iSequenceVar + 1, heads, iDimension, iContextUnits, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(att)) DeleteObjAndFalse(att); index++; att = new CNeuronDomainAwareAttention(); if(!att || !att.Init(0, index, OpenCL, iDimension, iScenariosUnits + iSequenceVar + 1, heads, iDimension, iScenariosUnits, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(att)) DeleteObjAndFalse(att); index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, 3 * att.Neurons(), optimization, iBatch) || !cHUBs.Add(neuron)) DeleteObjAndFalse(neuron); neuron.SetActivationFunction(None); index++; conv = new CNeuronFieldAwareConv(); if(!conv || !conv.Init(0, index, OpenCL, 3 * iDimension, iDimension, iSequenceVar + iScenariosUnits + 1, inside_embed_size, candidates, topK, optimization, iBatch) || !cHUBs.Add(conv)) DeleteObjAndFalse(conv); //--- return true; }
Это своего рода завершающий штрих, который фиксирует итоговое распределение информации. На выходе формируется компактное и согласованное представление, готовое к дальнейшему использованию в модели.
В коде эта последовательность выглядит громоздко. По сути же она реализует довольно строгую идею: сначала привести данные к единому виду, затем отдельно обработать разные типы сигналов, после чего аккуратно собрать их через контролируемый центр и несколько раз уточнить результат. В условиях финансовых рынков, где данные неоднородны, шумны и подвержены смене режимов, именно такая дисциплина обработки позволяет сохранить сигнал и не утонуть в случайных колебаниях.
В результате весь метод инициализации раскрывает важную мысль, которую легко упустить за техническими деталями. Мы не просто создаём слои и соединяем их между собой. Мы задаём маршрут, по которому будет двигаться информация.
После того как блок полностью собран, прямой проход начинает играть роль фактического прогона сигнала через всю архитектуру. Именно здесь становится видно, насколько корректно была выстроена логика потоков и не теряется ли информация по дороге.
bool CNeuronINFNetBlock::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cPrepare.FeedForward(NeuronOCL)) ReturnFalse;
Первым шагом осуществляется подготовка. Это момент, где сырые исходные данные окончательно преобразуются в токены. На выходе мы уже не имеем дело с ценой, индикаторами или производными признаками в их исходном виде. Перед нами единое embedding-пространство, где все сигналы приведены к общей шкале. Для финансовых рынков это критично: модель не должна помнить, в каких единицах измерялась волатильность или объём — она должна работать с согласованным представлением.
Сразу после этого происходит явное разделение потока. Общий тензор раскладывается на две части: последовательную и контекстную.
CNeuronBaseOCL* sequence = cSequence[0]; CNeuronBaseOCL* context = cContext[0]; if(!sequence || !context || !DeConcat(sequence.getOutput(), context.getOutput(), cPrepare.getOutput(), sequence.Neurons(), context.Neurons(), 1)) ReturnFalse;
Это тот редкий случай, когда архитектура не просто допускает разделение, а требует его на уровне вычислений. Последовательность продолжает нести информацию о динамике цены, а контекст — о состоянии среды. Если смешать их раньше времени, модель потеряет ориентиры.
Последовательный поток сразу проходит через следующий слой, добавляя новые токены в стек.
sequence = cSequence[1]; if(!sequence || !sequence.FeedForward(cSequence[0])) ReturnFalse;
Здесь он получает дополнительную структуризацию, фактически фиксируя форму временного сигнала. Параллельно сценарный блок генерирует сценарии из подготовленных токенов последовательности и контекст.
CNeuronBaseOCL* scen = cScenarios[0]; if(!scen || !scen.FeedForward(cPrepare.AsObject())) ReturnFalse;
Это важно: сценарии формируются из базового представления. Таким образом сохраняется независимость интерпретации.
Далее начинается работа HUB-блока. Сначала последовательность и контекст независимо проходят через свои агрегаторы. Это создаёт два центра локальной концентрации информации.
//--- HUBs CNeuronBaseOCL* hub = cHUBs[0]; if(!hub || !hub.FeedForward(sequence)) ReturnFalse; hub = cHUBs[1]; if(!hub || !hub.FeedForward(context)) ReturnFalse; hub = cHUBs[2];
После этого они объединяются вместе со сценарным представлением. В результате формируется единый узел, который содержит всю текущую информацию о состоянии системы.
if(!hub || !Concat(cHUBs[0].getOutput(), cHUBs[1].getOutput(), scen.getOutput(), hub.getOutput() , cHUBs[0].Neurons(), cHUBs[1].Neurons(), scen.Neurons(), 1)) ReturnFalse;
С этого момента начинается основной цикл обработки. На каждой итерации происходит строго повторяемая последовательность действий. Сначала три блока DomainAwareAttention извлекают информацию из разных источников. Один работает с последовательностью, другой с контекстом, третий со сценариями.
int layers = cContext.Total() - 1; for(int i = 0; i < layers; i++) { CNeuronBaseOCL* att_seq = cHUBs[i * 6 + 3]; if(!att_seq || !att_seq.FeedForward(hub, sequence.getOutput())) ReturnFalse; CNeuronBaseOCL* att_cont = cHUBs[i * 6 + 4]; if(!att_cont || !att_cont.FeedForward(hub, context.getOutput())) ReturnFalse; CNeuronBaseOCL* att_scen = cHUBs[i * 6 + 5]; if(!att_scen || !att_scen.FeedForward(hub, scen.getOutput())) ReturnFalse;
Важно, что во всех трёх случаях в качестве запроса используется текущее состояние HUB. Это означает, что внимание направляется не в целом, а относительно агрегированных данных. Модель не просто смотрит на данные, она задаёт им вопрос.
Полученные результаты объединяются в единое пространство. Здесь три разных представления складываются в общий вектор, но ещё без финальной интерпретации.
CNeuronBaseOCL* concat = cHUBs[i * 6 + 6]; if(!concat || !Concat(att_seq.getOutput(), att_cont.getOutput(), att_scen.getOutput(), concat.getOutput(), iDimension, iDimension, iDimension, iSequenceVar + iScenariosUnits + 1)) ReturnFalse;
Далее этот вектор проходит через слой scale_and_shift. По сути, это подготовка параметров для следующего ключевого шага — Broadcast Gated Unit.
CNeuronBaseOCL* scal_and_shift = cHUBs[i * 6 + 7]; if(!scal_and_shift || !scal_and_shift.FeedForward(concat)) ReturnFalse;
И вот здесь происходит то, ради чего строилась вся архитектура. Метод INFNetBGU принимает текущие состояния последовательности, контекста и сценариев, а также вычисленные параметры масштабирования и сдвига. На выходе формируются обновлённые версии всех трёх потоков.
sequence = cSequence[i + 2]; context = cContext[i + 1]; scen = cScenarios[i + 1]; if(!sequence || !context || !scen || !INFNetBGU(scal_and_shift, cSequence[i + 1], sequence, cContext[i], context, cScenarios[i], scen)) ReturnFalse;
Глобальный сигнал, собранный в HUB, не просто добавляется — он проходит через гейтинговый механизм, который решает, сколько информации внедрить в каждый токен. В условиях рынка это выглядит как адаптивная фильтрация: в одних ситуациях модель усиливает влияние контекста, в других — почти игнорирует его, полагаясь на чистую динамику цены.
После обновления потоков обновляется состояние HUB через очередной слой.
hub = cHUBs[i * 6 + 8]; if(!hub.FeedForward(concat)) ReturnFalse; }
Это замыкает итерацию. Новый HUB уже учитывает изменения, внесённые в последовательность, контекст и сценарии, и становится отправной точкой для следующего шага. Такой цикл повторяется несколько раз, постепенно уточняя представление.
Когда цикл завершается, выполняется финальный проход через блоки внимания и объединения. Здесь уже нет возврата к отдельным потокам. Происходит окончательная агрегация информации, после чего формируется итоговое состояние HUB. Это своего рода срез модели, в котором уже учтены все взаимодействия.
CNeuronBaseOCL* att_seq = cHUBs[layers * 6 + 3]; if(!att_seq || !att_seq.FeedForward(hub, sequence.getOutput())) ReturnFalse; CNeuronBaseOCL* att_cont = cHUBs[layers * 6 + 4]; if(!att_cont || !att_cont.FeedForward(hub, context.getOutput())) ReturnFalse; CNeuronBaseOCL* att_scen = cHUBs[layers * 6 + 5]; if(!att_scen || !att_scen.FeedForward(hub, scen.getOutput())) ReturnFalse; CNeuronBaseOCL* concat = cHUBs[layers * 6 + 6]; if(!concat || !Concat(att_seq.getOutput(), att_cont.getOutput(), att_scen.getOutput(), concat.getOutput(), iDimension, iDimension, iDimension, iSequenceVar + iScenariosUnits + 1)) ReturnFalse; hub = cHUBs[layers * 6 + 7]; if(!hub.FeedForward(concat)) ReturnFalse;
Завершается прямой передачей агрегированной информации родительскому классу.
if(!CNeuronPerTokenSwiGLU::feedForward(hub)) ReturnFalse; //--- return true; }
На этом этапе окончательно формируется выходное представление. Важно, что нелинейность применяется уже после всей сложной агрегации. Это означает, что она не искажает процесс взаимодействия сигналов, а лишь аккуратно трансформирует итог.
Если посмотреть на весь процесс целиком, становится видно, что сигнал проходит несколько стадий: сначала приведение к единому виду, затем разделение по ролям, после чего — многократная итеративная агрегация с обратной связью через HUB. Это не разовый расчёт, а последовательное уточнение. В терминах финансового рынка это похоже на процесс анализа, где первоначальная гипотеза многократно проверяется и корректируется по мере поступления новых данных.
Такая организация прямого прохода даёт ключевое преимущество. Модель не принимает решение на основе одного взгляда на данные. Она несколько раз пересматривает ситуацию, каждый раз с учётом уже накопленного контекста. Именно это позволяет ей работать в условиях шума, неполноты информации и постоянной смены рыночных режимов, не теряя при этом устойчивости.
Прямой проход — только половина механики. Он отвечает на вопрос, что модель думает сейчас. Но никак не приближает её к правильному ответу. Нужно организовать обратный поток: провести ошибку через всю архитектуру и распределить её между компонентами пропорционально вкладу. Именно это выполняет метод calcInputGradients. Причём делает в той же строгой логике потоков, что и прямой проход, только в обратном направлении.
bool CNeuronINFNetBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) ReturnFalse; //--- if(!CNeuronPerTokenSwiGLU::calcInputGradients(cHUBs[-1])) ReturnFalse;
Процесс начинается с верхнего уровня. Сначала градиент проходит родительский класс, который был последним этапом прямого прохода. Это естественная точка старта: мы берём итоговое представление и начинаем разматывать его назад. Важно, что обратное распространение здесь не просто передаёт ошибку, а уже на первом шаге учитывает структуру потока данных. То есть корректировка сразу адаптируется к тому, как именно формировался финальный выход.
Далее градиент попадает в финальный агрегированный HUB и разделяется обратно на три составляющих — последовательную, контекстную и сценарную.
int layers = cContext.Total() - 1; CNeuronBaseOCL* concat = cHUBs[-2]; if(!concat || !concat.CalcHiddenGradients(cHUBs[-1])) ReturnFalse; CNeuronBaseOCL* att_seq = cHUBs[-5]; CNeuronBaseOCL* att_cont = cHUBs[-4]; CNeuronBaseOCL* att_scen = cHUBs[-3]; if(!att_seq || !att_cont || !att_scen || !DeConcat(att_seq.getGradient(), att_cont.getGradient(), att_scen.getGradient(), concat.getGradient(), iDimension, iDimension, iDimension, iSequenceVar + iScenariosUnits + 1)) ReturnFalse;
Это зеркальное отражение прямого прохода: там мы объединяли сигналы, здесь — снова их разводим. В этом месте важно сохранить пропорции. Если ошибка будет распределена некорректно, один поток начнёт доминировать, а другой — деградировать.
После разделения начинается восстановление градиента HUB. Он пересчитывается последовательно для каждого типа данных: сначала через последовательность, затем через контекст и сценарии.
CNeuronBaseOCL* hub = cHUBs[-6]; CNeuronBaseOCL* sequence = cSequence[-1]; CNeuronBaseOCL* context = cContext[-1]; CNeuronBaseOCL* scen = cScenarios[-1]; CNeuronBaseOCL* scal_and_shift = NULL; //--- if(!hub || !sequence || !hub.CalcHiddenGradients(att_seq, sequence.getOutput(), sequence.getGradient(), (ENUM_ACTIVATION)sequence.Activation())) ReturnFalse; CBufferFloat* temp = hub.getGradient(); if(!hub.SetGradient(hub.getPrevOutput(), false) || !context || !hub.CalcHiddenGradients(att_cont, context.getOutput(), context.getGradient(), (ENUM_ACTIVATION)context.Activation()) || !SumAndNormalize(temp, hub.getGradient(), temp, iDimension, false, 0, 0, 0, 1)) ReturnFalse; if(!scen || !hub.CalcHiddenGradients(att_scen, scen.getOutput(), scen.getGradient(), (ENUM_ACTIVATION)scen.Activation()) || !SumAndNormalize(temp, hub.getGradient(), temp, iDimension, false, 0, 0, 0, 1) || !hub.SetGradient(temp, false)) ReturnFalse;
Причём после каждого шага градиенты суммируются. Это ключевой момент. В архитектуре с несколькими потоками все градиенты должны быть учтены в полном объёме.
Дальше начинается основной цикл обратного распространения, который идёт в обратном порядке относительно прямого прохода. На каждой итерации сначала обрабатывается блок INFNetBGUGrad. Это зеркальное отражение Broadcast Gated Unit. Если в прямом проходе глобальный контекст внедрялся в локальные токены, то здесь происходит обратное — ошибка распределяется от обновлённых токенов к их предыдущим состояниям и параметрам масштабирования.
for(int i = layers - 1; i >= 0; i--) { att_seq = cHUBs[i * 6 + 3]; att_cont = cHUBs[i * 6 + 4]; att_scen = cHUBs[i * 6 + 5]; concat = cHUBs[i * 6 + 6]; scal_and_shift = cHUBs[i * 6 + 7]; sequence = cSequence[i + 1]; context = cContext[i]; scen = cScenarios[i]; if(!sequence || !context || !scen || !INFNetBGUGrad(scal_and_shift, sequence, cSequence[i + 2], context, cContext[i + 1], scen, cScenarios[i + 1])) ReturnFalse;
По сути, модель понимает, какие именно изменения в глобальном сигнале повлияли на результат.
После этого градиент снова агрегируется с учётом состояния HUB. Здесь повторяется тот же принцип: аккуратное суммирование.
if(!concat || !concat.CalcHiddenGradients(scal_and_shift)) ReturnFalse; temp = concat.getGradient(); if(!concat.SetGradient(concat.getPrevOutput(), false) || !concat.CalcHiddenGradients(hub) || !SumAndNormalize(temp, concat.getGradient(), temp, iDimension, false, 0, 0, 0, 1) || !concat.SetGradient(temp, false)) ReturnFalse;
Архитектура не допускает резких перекосов даже на этапе обучения. Это особенно важно для рынков, где данные нестабильны, а редкие события могут давать сильные выбросы ошибки.
Затем градиент снова разделяется на три потока и начинается восстановление влияния каждого типа данных на состояние HUB.
hub = cHUBs[i * 6 + 2]; if(!att_seq || !att_cont || !att_scen || !DeConcat(att_seq.getGradient(), att_cont.getGradient(), att_scen.getGradient(), concat.getGradient(), iDimension, iDimension, iDimension, iSequenceVar + iScenariosUnits + 1)) ReturnFalse;
Для каждого из блоков вызывается метод CalcHiddenGradients, где учитывается не только сам градиент, но и соответствующий выходной сигнал и функция активации. Это делает обратное распространение контекстно-зависимым: модель корректирует параметры с учётом того, в каком режиме она работала на прямом проходе.
if(!hub || !sequence || !hub.CalcHiddenGradients(att_seq, sequence.getOutput(), sequence.getGradient(), (ENUM_ACTIVATION)sequence.Activation())) ReturnFalse; temp = hub.getGradient(); if(!hub.SetGradient(hub.getPrevOutput(), false) || !context || !hub.CalcHiddenGradients(att_cont, context.getOutput(), context.getGradient(), (ENUM_ACTIVATION)context.Activation()) || !SumAndNormalize(temp, hub.getGradient(), temp, iDimension, false, 0, 0, 0, 1)) ReturnFalse; if(!scen || !hub.CalcHiddenGradients(att_scen, scen.getOutput(), scen.getGradient(), (ENUM_ACTIVATION)scen.Activation()) || !SumAndNormalize(temp, hub.getGradient(), temp, iDimension, false, 0, 0, 0, 1) || !hub.SetGradient(temp, false)) ReturnFalse; }
После завершения всех итераций цикла градиент возвращается к начальному состоянию HUB. Здесь снова происходит разделение на последовательную и контекстную части, а сценарный поток аккуратно интегрируется. Это замыкает обратный путь внутри агрегирующего блока.
if(!cHUBs[0] || !cHUBs[1] || !DeConcat(cHUBs[0].getGradient(), cHUBs[1].getGradient(), scen.getPrevOutput(), hub.getGradient(), cHUBs[0].Neurons(), cHUBs[1].Neurons(), scen.Neurons(), 1)) ReturnFalse; if(!SumAndNormalize(scen.getGradient(), scen.getPrevOutput(),scen.getGradient(),iDimension,false,0,0,0,1)) ReturnFalse; temp=context.getGradient(); if(!context.SetGradient(context.getPrevOutput(),false) || !context.CalcHiddenGradients(cHUBs[1]) || !SumAndNormalize(temp,context.getGradient(),temp,iDimension,false,0,0,0,1) || !context.SetGradient(temp,false)) ReturnFalse; temp=sequence.getGradient(); if(!sequence.SetGradient(sequence.getPrevOutput(),false) || !sequence.CalcHiddenGradients(cHUBs[0]) || !SumAndNormalize(temp,sequence.getGradient(),temp,iDimension,false,0,0,0,1) || !sequence.SetGradient(temp,false)) ReturnFalse; sequence=cSequence[0]; if(!sequence || !sequence.CalcHiddenGradients(cSequence[1])) ReturnFalse;
Далее градиенты последовательно проходят через контекстный и последовательный потоки. Причём и здесь используется схема сброс → пересчёт → суммирование. Сначала градиент временно сохраняется, затем пересчитывается через соответствующий слой, после чего объединяется с предыдущим значением. Это позволяет сохранить вклад разных путей распространения ошибки.
Когда градиент доходит до самого начала последовательного блока, он проходит через первый слой cSequence[0], восстанавливая влияние исходного представления последовательности. Параллельно сценарный поток передаёт градиент в `cPrepare`, что возвращает нас к точке формирования токенов.
if(!cPrepare.CalcHiddenGradients(scen)) ReturnFalse; if(!Concat(sequence.getGradient(), context.getGradient(), cPrepare.getPrevOutput(), sequence.Neurons(), context.Neurons(), 1)) ReturnFalse; if(cPrepare.Activation()!=None) if(!DeActivation(cPrepare.getOutput(),cPrepare.getPrevOutput(),cPrepare.getPrevOutput(), cPrepare.Activation())) ReturnFalse; if(!SumAndNormalize(cPrepare.getGradient(),cPrepare.getPrevOutput(),cPrepare.getGradient(), iDimension,false,0,0,0,1)) ReturnFalse;
На завершающем этапе градиенты последовательности и контекста объединяются обратно в единый буфер. Это полностью симметрично прямому проходу. После этого при необходимости применяется обратная функция активации, и градиенты суммируются. Таким образом восстанавливается корректный масштаб ошибки на уровне входных токенов.
Последний шаг — передача градиента во внешний объект NeuronOCL.
if(!NeuronOCL.CalcHiddenGradients(cPrepare.AsObject())) ReturnFalse; //--- return true; }
Это уже граница блока. Всё, что происходило внутри, было направлено на то, чтобы довести ошибку до этой точки в корректном виде, не потеряв структуру и не исказив вклад отдельных компонентов.
Если посмотреть на весь процесс целиком, становится очевидно, что обратное распространение здесь продолжение той же философии, что и прямой проход. Ошибка движется по тем же каналам, что и сигнал, но в обратном направлении. Она также агрегируется и разделяется. В контексте финансовых рынков это даёт важное преимущество: модель учится на структурированном сигнале, который уже прошёл через фильтры архитектуры. Это делает обучение более устойчивым и снижает вероятность того, что модель начнёт подстраиваться под случайные колебания вместо реальных закономерностей.
Тестирование
В этой реализации обучение INFNet идёт поэтапно. Модель переходит от статичных исторических данных к работе с живым потоком котировок. На первом этапе используется офлайн-обучение на архиве EURUSD H1 за 2025 год. Здесь рынок ещё зафиксирован, а модель анализирует его как целостную последовательность наблюдений. Ценовые движения преобразуются в токенизированное представление и проходят через архитектуру INFNet, где формируются устойчивые зависимости между различными состояниями рынка — импульсами, консолидациями и сменами режимов.
На этом этапе модель не просто подгоняет параметры под историю, а выстраивает внутреннюю структуру переходов между рыночными состояниями. По сути, формируется базовая карта поведения рынка, в которой фиксируются повторяемые закономерности: реакция на импульсы, поведение после флетов, изменение структуры контекста при переходе режимов. Это создаёт фундамент, на котором будет строиться дальнейшая адаптация.
Следующий этап — онлайн-обучение в тестере MetaTrader 5 — переводит систему в принципиально иной режим. Рынок перестаёт быть фиксированным набором данных и становится непрерывным потоком. Каждый новый бар не только проходит через уже обученную архитектуру, но и уточняет её внутренние представления. Последовательность обновляется через sequence-поток, контекст адаптируется к текущему состоянию рынка, а сценарные зависимости постепенно перестраиваются под новые условия. Обучение здесь приобретает характер непрерывной синхронизации модели с живым рынком.
Финальный этап тестирования на данных января–марта 2026 года служит проверкой устойчивости всей архитектуры. Модель сталкивается с новыми рыночными режимами, которые могут существенно отличаться от обучающего периода.


Тестирование показало противоречивый, но показательный результат. С одной стороны, стратегия вышла в плюс: при стартовом депозите $100.0 итоговый баланс составил около $210.05, то есть модель действительно извлекает прибыль из рыночного потока. Однако эта прибыль формируется через высокую турбулентность результата.
В процессе торговли система провела 132 сделки с почти паритетным распределением: около 52% прибыльных и 48% убыточных. Это указывает на то, что модель работает на слабом статистическом преимуществе. Profit factor на уровне 1.17 подтверждает это — стратегия находится на границе прибыльности, где любое ухудшение условий может легко обнулить edge.
Главный проблемный участок — риск-профиль. Максимальная просадка превышает 50% как по балансу, так и по эквити. Это означает, что в отдельные периоды модель теряет половину капитала, прежде чем восстанавливается. В практическом трейдинге это уже не технический нюанс, а критический фактор устойчивости. Дополнительно обращает на себя внимание неравномерность сделок: отдельные убыточные позиции оказывают чрезмерное влияние на итоговый результат, что говорит о недостаточно жёстком контроле риска.
Система демонстрирует ключевое свойство INFNet: она находит слабую структуру в данных и превращает её в положительное ожидание. Но при текущей конфигурации эта структура ещё не стабилизирована: прибыль есть, но она куплена высокой волатильностью эквити и глубокими просадками. Иными словами, модель уже умеет зарабатывать, но пока не умеет делать это спокойно и предсказуемо.
Заключение
Архитектура INFNet в данной работе прошла полный цикл: от концептуального описания потоков информации до реализации единого вычислительного конвейера и его тестирования на исторических рыночных данных. Важно, что система перестала быть набором отдельных модулей и приобрела целостную структуру, в которой последовательность, контекст и сценарные зависимости обрабатываются как взаимосвязанные, но формально разделённые потоки. Такой подход позволил сохранить управляемость модели при росте сложности и обеспечить линейную организацию ключевых операций.
Практическая реализация показала, что архитектура действительно способна извлекать устойчивый сигнал из рыночных данных. На тестовом периоде модель демонстрирует положительную итоговую доходность, подтверждая наличие статистического преимущества. Однако одновременно проявляется и обратная сторона: высокая чувствительность к режимам рынка и значительные просадки указывают на то, что текущая конфигурация ещё не обладает достаточной устойчивостью с точки зрения риск-профиля. Иными словами, сигнал найден, но его стабильность пока остаётся ограниченной.
С инженерной точки зрения результат ожидаем для систем этого класса. INFNet не является статическим предиктором — это адаптивная структура, в которой качество результата напрямую зависит от баланса между потоками информации, глубиной агрегации и механизмами обратного распространения ошибки. В текущей реализации этот баланс уже сформирован на уровне работоспособности, но требует дальнейшей калибровки, особенно в части управления риском и подавления экстремальных отклонений.
Таким образом, можно зафиксировать ключевой результат работы: создана и реализована целостная версия INFNet, способная работать в условиях реального рыночного потока и извлекать из него положительное математическое ожидание.
Ссылки
- Aggregate and Broadcast: Scalable and Efficient Feature Interaction for Recommender Systems
- Другие статьи серии
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 1 | Study.mq5 | Советник | Советник офлайн-обучения моделей |
| 2 | StudyOnline.mq5 | Советник | Советник онлайн-обучения моделей |
| 3 | Test.mq5 | Советник | Советник для тестирования модели |
| 4 | Trajectory.mqh | Библиотека класса | Структура описания состояния системы и архитектуры моделей |
| 5 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
| 6 | NeuroNet.cl | Библиотека | Библиотека кода OpenCL-программы |
Проект представлен на forge.mql5.io/dng.
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Создание самооптимизирующихся советников на MQL5 (Часть 17): Ансамблевый интеллект
Повторное использование нарушенных ордер-блоков в качестве блоков смягчения (SMC)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования