
Нейросети в трейдинге: Мультимодальный агент, дополненный инструментами (Окончание)
Введение
В предыдущей статье мы познакомились с фреймворком FinAgent, который представляет собой передовой инструмент для анализа данных и поддержки принятия решений на финансовых рынках. Его разработка направлена на создание эффективного механизма формирования торговых стратегий и минимизации рисков в условиях сложной и динамичной рыночной среды. Архитектура FinAgent состоит из пяти взаимосвязанных модулей, каждый из которых выполняет специализированные функции для обеспечения общей адаптивности системы.
Модуль анализа рынка обеспечивает извлечение и обработку данных из разнородных источников, включая ценовые графики, рыночные новости и отчёты. Здесь осуществляется поиск устойчивых паттернов, которые могут быть использованы для прогнозирования динамики цен.
Модули рефлексии играют важную роль в процессе адаптации и обучения модели. Модуль низкоуровневый рефлексии анализирует взаимозависимости между текущими рыночными сигналами, повышая точность кратковременных прогнозов. Высокоуровневый модуль, напротив, работает с долгосрочными трендами, включая исторические данные и результаты предыдущих торговых решений, с целью корректировки используемой стратегии на основе накопленного опыта.
Модуль памяти обеспечивает долговременное хранение больших объёмов рыночных данных. Применение современных технологий векторного сходства позволяет минимизировать влияние шума и улучшить точность поиска информации, что особенно важно для построения долгосрочных стратегий и выявления сложных взаимосвязей.
Центральное звено системы — модуль принятия решений, который интегрирует результаты работы всех остальных компонентов. На основе текущей и исторической информации он формирует оптимальные торговые рекомендации. А благодаря возможности интеграции экспертных знаний и традиционных индикаторов, модуль способен генерировать сбалансированные и обоснованные рекомендации.
Авторская визуализация фреймворка FinAgent представлена ниже.
В предыдущей статье мы начали реализацию подходов, предложенных авторами фреймворка FinAgent, средствами MQL5. Были представлены алгоритмы для модулей низкоуровневой и высокоуровневой рефлексии, реализованные в виде объектов CNeuronLowLevelReflection и CNeuronHighLevelReflection. Эти модули осуществляют анализ рыночных сигналов, истории принятых торговых решений и фактически достигнутых финансовых результатов, что позволяет адаптировать политику поведения агента к изменяющимся условиям рынка. Они также предоставляют возможность гибко реагировать на динамические изменения трендов и выявлять ключевые закономерности в данных.
Особенностью нашей реализации является интеграция блоков памяти непосредственно в объекты рефлексии. Этот подход отличается от авторской архитектуры фреймворка, в которой память для всех информационных потоков была выделена в отдельный модуль. Интеграция блоков памяти упрощает построение информационных потоков взаимодействия между различными компонентами фреймворка.
В продолжение начатой работы, мы рассмотрим реализацию нескольких важных модулей, каждый из которых играет уникальную роль в общей архитектуре системы:
- Модуль анализа рынка предназначен для обработки данных из доступного разнообразия источников, включая финансовые отчёты, новости и биржевые котировки. Он приводит мультимодальные данные к единому формату и выделяет устойчивые паттерны, которые могут быть использованы для прогнозирования будущей динамики рынка.
- Дополнительные инструменты, основанные на априорных знаниях, которые обеспечивают поддержку анализа и принятия решений на основе исторических закономерностей, статистических данных и экспертных оценок. В то же время они позволяют дать логическую интерпретацию принятых решений.
- Система поддержки принятия решений объединяет результаты работы всех модулей для формирования адаптивной и оптимальной торговой стратегии. Эта система предоставляет рекомендации по действиям в реальном времени, что позволит трейдерам и аналитикам оперативно реагировать на изменения рыночной конъюнктуры и принимать более взвешенные решения.
Модуль анализа рынка играет центральную роль в системе, так как он отвечает за предварительную обработку и унификацию данных. Это особенно важно для выявления скрытых закономерностей, которые трудно обнаружить при традиционном подходе к анализу данных. Авторы фреймворка FinAgent использовали большие языковые модели (LLM) для извлечения ключевых аспектов данных и их сжатия. В своей реализации мы отказались от использования LLM и сделали акцент на специализированные модели для анализа временных рядов, которые обеспечивают высокую точность и производительность. В рамках данной серии статей было представлено несколько фреймворков анализа и прогнозирования многомерных временных рядов. И здесь можно использовать любой из них. При подготовке статьи наш выбор пал на трансформер с сегментированным вниманием, который реализован в виде класса CNeuronPSformer.
Однако, это не единственный вариант. Более того, фреймворк FinAgent предлагает использование мультимодальных исходных данных. Это позволяет нам не только экспериментировать с различными алгоритмами представления и анализа временных рядов, но комбинировать их. Такой подход существенно расширяет возможности системы, обеспечивая более детализированное понимание рыночных процессов и способствуя разработке высокоэффективных, адаптивных стратегий.
Модуль дополнительных инструментов выполняет функцию интеграции априорных знаний об анализируемой среде в общую архитектуру модели. Этот компонент обеспечивает возможность генерировать аналитические сигналы на основе классических индикаторов, таких как скользящие средние, осцилляторы и индикаторы объёма, которые уже зарекомендовали себя в практике алгоритмического трейдинга. Однако модуль не ограничивается использованием только стандартных инструментов.
Кроме того, генерация сигналов с использованием четких правил, на основе показателей технических индикаторов, улучшает интерпретируемость принятых моделью решений, а также способствует повышению их надежности и эффективности. Что является ключевым фактором для стратегического планирования и управления рисками.
Модуль дополнительных инструментов
Формирование модуля генерации сигналов на основе показателей классических индикаторов в рамках нейронной модели представляет собой гораздо более сложную задачу, чем может показаться на первый взгляд. Основная трудность заключается не в интерпретации сигналов, а в оценке показателей, поступающих на вход объекта.
В классических стратегиях описание сигналов напрямую зависит от фактических показаний индикаторов. Однако, их показатели часто формируют значения в совершенно несвязанных и несопоставимых распределениях, что создаёт значительные препятствия для построения моделей. Этот фактор серьёзно снижает эффективность их обучения, так как алгоритмы вынуждены адаптироваться к анализу гетерогенных данных. Это приводит к увеличению времени обработки, снижению точности прогнозов и прочим негативным последствиям. Поэтому, ранее мы приняли решение использовать исключительно нормализованные данные в наших моделях.
Процесс нормализации исходных данных позволяет привести все анализируемые признаки к единому, сопоставимому масштабу, что, в свою очередь, существенно улучшает качество обучения моделей. Этот подход минимизирует риски появления искажений, связанных с разными единицами измерения показателей или их изменчивостью во времени. Важным преимуществом нормализации является возможность более глубокого анализа данных, поскольку в таком виде они становятся более предсказуемыми для алгоритмов машинного обучения.
Однако следует отметить, что нормализация значительно усложняет процесс генерации сигналов классических стратегий. Эти стратегии изначально разрабатывались для работы с "сырыми" данными, и предполагают использование фиксированных пороговых значений для интерпретации индикаторов. В процессе нормализации данные преобразуются, что приводит к неопределенному смещению пороговых значений. Более того, нормализация делает невозможным формирование сигнала по пересечению двух линий классического индикатора, так как нет гарантии синхронного смещения значений анализируемых линий. Как следствие —) искажение генерируемых сигналов или их отсутствие. Все это приводит нас к необходимости разработки новых подходов к интерпретации показателей индикаторов.
И здесь, как мне кажется, было найдено простое и, в то же время, концептуально обоснованное решение. Суть его заключается в том, что в процессе нормализации все анализируемые признаки приводятся к нулевому среднему и единичной дисперсии. В результате этого процесса каждая величина становится сопоставимой с другими, и может быть интерпретирована как своеобразный осциллятор. Такой подход предоставляет универсальную схему интерпретации сигналов: значения более "0" рассматриваются в качестве сигналов на покупку, а значения ниже "0" — на продажу. Допускается так же внедрение пороговых значений, что даёт возможность формирования "коридоров" для фильтрации слабых или неоднозначных сигналов. Это позволяет минимизировать количество ложных срабатываний, повышает точность анализа и способствует принятию более обоснованных решений.
Мы также допускаем вероятность инверсных сигналов для некоторых анализируемых признаков. Решения этой проблемы возможно путем использования обучаемых параметров, которые адаптируются к историческим данным.
Применение предложенного подхода создаёт основу для построения моделей, которые могут эффективно адаптироваться к меняющимся условиям и генерировать более точные и достоверные сигналы.
Реализацию предложенного подхода к формированию сигналов мы начинаем с построения кернела MoreLessEqual на стороне OpenCL-программы. В данном случае был реализован наиболее простой алгоритм с фиксированным пороговым значением.
В параметрах данного кернела получаем указатели на 2 буфера данных одинакового размера. Один из них содержит исходные данные, а во второй мы будем записывать сигналы в виде одного из 3 числовых значений:
- -1 — продажа;
- 0 — отсутствие сигнала;
- 1— покупка.
__kernel void MoreLessEqual(__global const float * input, __global float * output) { const size_t i = get_global_id(0); const float value = IsNaNOrInf(input[i], 0); float result = 0;
В теле кернела мы идентифицируем текущий поток операций и сразу считываем из буфера исходных данных соответствующее значение в локальную переменную. Обязательным шагом является проверка корректности полученного значения: все данные, которые не проходят валидацию, автоматически заменяются на "0" для предотвращения ошибок на последующих этапах обработки.
Тут же вводится локальная переменная, предназначенная для сохранения промежуточных результатов. Изначально этой переменной присваивается значение, указывающее на отсутствие какого-либо сигнала.
Следующим шагом мы проверяем абсолютное значение анализируемой переменной. Для генерации сигнала оно должно превышать заданное пороговое значение.
if(fabs(value) > 1.2e-7) { if(value > 0) result = 1; else result = -1; } output[i] = result; }
Положительные значения выше порогового значения формируют сигнал на покупку, а отрицательные — на продажу. Соответствующий флаг мы записываем в локальную переменную. И перед завершением работы кернела, переносим полученный флаг в буфер результатов.
Описанный выше алгоритм представляет собой последовательную процедуру прямого прохода, при которой данные обрабатываются без применения обучаемых параметров. Этот метод базируется на строго детерминированных вычислениях, направленных на минимизацию затрат вычислительных ресурсов и предотвращение избыточной сложности, что особенно важно в контексте обработки больших объёмов информации. Важно отметить, что распределение градиента ошибки в данном информационном потоке не предусмотрено. Ведь нам важно выявить устойчивые сигналы на основе показателей индикаторов, а не "подогнать" их под желаемый результат. Это делает алгоритм особенно привлекательным для систем, требующих высокой скорости и точности обработки.
После построения алгоритма на стороне OpenCL-программы нам необходимо организовать обслуживание и вызов представленного выше кернела на стороне основной программы. Для выполнения данного функционала, мы создадим новый объект CNeuronMoreLessEqual, структура которого представлена ниже.
class CNeuronMoreLessEqual : public CNeuronBaseOCL { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override {return true; } public: CNeuronMoreLessEqual(void) {}; ~CNeuronMoreLessEqual(void) {}; };
Представленная структура нового объекта необычайно проста. В ней даже отсутствует метод инициализации объекта. Практически, весь функционал выполняется средствами родительского класса. Мы лишь переопределяем методы прямого и обратного прохода.
В методе прямого прохода осуществляется лишь передача указателей на буферы данных в параметры выше представленного кернела с последующей постановкой его в очередь выполнения.
bool CNeuronMoreLessEqual::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!OpenCL || !NeuronOCL) return false; uint global_work_offset[1] = { 0 }; uint global_work_size[1] = { Neurons() }; ResetLastError(); const int kernel = def_k_MoreLessEqual; if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_inputs, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_outputs, getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } //--- if(!OpenCL.Execute(kernel, 1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), GetLastError(), __LINE__); return false; } //--- return true; }
Функционал методов обратного прохода, на первый взгляд, может показаться неочевидным, учитывая сделанное ранее указание на отсутствие обучаемых параметров и отказ от распределения градиентов ошибки. Однако следует подчеркнуть, что в структуре построения нейронных сетей данные методы являются обязательными для всех слоев. В противном случае будет вызван одноимённый метод родительского класса, который может функционировать некорректно в условиях нашей архитектуры. Чтобы избежать подобных проблем, метод обновления обучаемых параметров переопределяется заглушкой, возвращающей значение true.
Что касается отказа от распределения градиентов ошибки, логически он аналогичен передаче нулевых значений. Таким образом, в методе распределения градиентов мы просто обнуляем соответствующий буфер в объекте исходных данных, что обеспечивает корректную работу модели и минимизирует риск возникновения ошибок.
bool CNeuronMoreLessEqual::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getGradient()) return false; return NeuronOCL.getGradient().Fill(0); }
На этом мы завершаем работу с объектом модуля дополнительных инструментов. С полным кодом класса CNeuronMoreLessEqual и всех его методов вы можете ознакомиться во вложении.
На данном этапе мы практически полностью рассмотрели реализацию всех ключевых модулей фреймворка FinAgent. Остается рассмотреть модуль принятия решений, который играет роль центрального звена в общей архитектуре. Этот модуль обеспечивает синтез информации, поступающей от множества информационных потоков, число которых превышает два. И нами было принято решение об интеграции модуля принятия решений в комплексный объект фреймворка, без выделения в отдельную сущность. Что позволило улучшить взаимодействие всех компонентов системы.
Построение фреймворка FinAgent
И вот пришел момент объединить ранее созданные отдельные модули в единую комплексную структуру фреймворка FinAgent, чтобы обеспечить их интеграцию и синергетическое взаимодействие. Модули различной функциональной направленности объединяются для достижения общей цели — создания эффективной и гибкой системы для анализа сложных рыночных данных и разработки стратегий, учитывающих динамику и особенности финансового рынка. Данный функционал выполняется новым объектом CNeuronFinAgent, структура которого представлена ниже.
class CNeuronFinAgent : public CNeuronRelativeCrossAttention { protected: CNeuronTransposeOCL cTransposeState; CNeuronLowLevelReflection cLowLevelReflection[2]; CNeuronHighLevelReflection cHighLevelReflection; CNeuronMoreLessEqual cTools; CNeuronPSformer cMarketIntelligence; CNeuronMemory cMarketMemory; CNeuronRelativeCrossAttention cCrossLowLevel; CNeuronRelativeCrossAttention cMarketToLowLevel; CNeuronRelativeCrossAttention cMarketToTools; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; public: CNeuronFinAgent(void) {}; ~CNeuronFinAgent(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint account_descr, uint nactions, uint segments, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronFinAgent; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool Clear(void) override; };
В представленной структуре мы видим привычный набор переопределяемых методов и ряд внутренних объектов, среди которых легко найти объекты реализованных нами модулей фреймворка FinAgent. О построении информационных потоков их взаимодействия мы поговорим в процессе обсуждения алгоритмов реализации методов данного класса.
Все внутренние объекты объявлены статично, что позволяет нам оставить пустыми конструктор и деструктор создаваемого класса. Инициализация всех объявленных и унаследованных объектов осуществляется в методе Init. В параметрах метода мы получаем ряд констант, позволяющих однозначно определить архитектуру создаваемого объекта.
bool CNeuronFinAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint account_descr, uint nactions, uint segments, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronRelativeCrossAttention::Init(numOutputs, myIndex, open_cl, 3, window_key, nactions / 3, heads, window, units_count, optimization_type, batch)) return false;
Забегая немного вперед, скажем, что наш блок принятия решений будет состоять из нескольких последовательных блоков кросс-внимания. Последний мы реализуем средствами родительского объекта, в качестве которого мы не случайно использовали класс относительного кросс-внимания CNeuronRelativeCrossAttention.
На выходе нашей реализации фреймворка FinAgent мы ожидаем получить представление тензора действий агента в виде матрицы, строки которой являются векторами описания отдельных действий. При этом операции покупки и продажи представлены отдельными строками этой матрицы. Каждая операция описывается 3 параметрами: объем сделки и два торговых уровня (стоп-лосс и тейк-профит). Следовательно, наша матрица действий агента будет содержать 3 столбца.
Поэтому, при вызове метода инициализации родительского класса, мы указываем окно анализа данных по основной магистрали равным 3, а количество элементов анализируемой последовательности — в 3 раза меньше полученного в параметрах размера вектора для описания действий агента. Это позволяет модели оценить эффективность каждой отдельно взятой операции в контексте второго потока информации, по которому планируется передавать обработанную информацию о состоянии окружающей среды. Что и выражается в переносе соответствующих параметров.
После успешного выполнения операций метода инициализации родительского класса, мы переходим к подготовке вновь объявленных внутренних объектов. И начинаем мы эту работу с инициализации компонентов модуля анализа рынка. В нашем случае он представлен двумя объектами: трансформером с сегментированным вниманием для поиска устойчивых паттернов в многомерном временном ряде исходных данных и блоком памяти.
int index = 0; if(!cMarketIntelligence.Init(0, index, OpenCL, window, units_count, segments, 0.2f, optimization, iBatch)) return false; index++; if(!cMarketMemory.Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch)) return false;
С целью всестороннего анализа состояния окружающей среды, в своей работе мы используем 2 модуля низкоуровневой рефлексии, которые работают параллельно с тензором исходных данных, представленным в разных проекциях. Для получения второй проекции анализируемых данных, мы применяем объект транспонирования.
index++; if(!cTransposeState.Init(0, index, OpenCL, units_count, window, optimization, iBatch)) return false;
Затем инициализируем 2 объекта низкоуровневой рефлексии. Об анализе ими данных в различной проекции свидетельствует перестановка размерностей окна и длины последовательности анализируемого тензора.
index++; if(!cLowLevelReflection[0].Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch)) return false; index++; if(!cLowLevelReflection[1].Init(0, index, OpenCL, units_count, window_key, window, heads, optimization, iBatch)) return false;
В первом случае, мы анализируем многомерный временной ряд, где каждый временной шаг представлен вектором данных, и сравниваем эти векторы для выявления взаимосвязей между ними. Во втором случае, сопоставляем отдельные унитарные последовательности, с целью поиска зависимостей и закономерностей в их динамике.
Затем инициализируем модуль высокоуровневой рефлексии, в котором посмотрим на последние действия агента в контексте изменения рыночной ситуации и финансовых результатов.
index++; if(!cHighLevelReflection.Init(0, index, OpenCL, window, window_key, units_count, heads, account_descr, nactions, optimization, iBatch)) return false;
И тут же мы подготовим объект модуля дополнительных инструментов.
index++; if(!cTools.Init(0, index, OpenCL, window * units_count, optimization, iBatch)) return false; cTools.SetActivationFunction(None);
Результаты работы всех вышеинициализированных модулей собираются в модуле принятия решений, который, как было сказано выше, состоит из нескольких последовательных блоков кросс-внимания. На первом этапе интегрируем информацию, полученную от двух модулей низкоуровневой рефлексии.
index++; if(!cCrossLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, units_count, window, optimization, iBatch)) return false;
Далее, мы обогащаем результаты работы модуля анализа рынка информацией, полученной от модулей низкоуровневой рефлексии.
index++; if(!cMarketToLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count, optimization, iBatch)) return false;
И добавим немного априорных знаний.
index++; if(!cMarketToTools.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count, optimization, iBatch)) return false; //--- return true; }
Напомню, что последний слой модуля принятия решений мы уже инициализировали ранее. Он представлен родительским объектом.
После успешной инициализации всех вложенных объектов, мы возвращаем логический результат выполнения операций вызывающей программе и завершаем работу метода.
Следующим этапом нашей работы является построение алгоритма прямого прохода нашей реализации фреймворка FinAgent в рамках метода feedForward. В параметрах метода мы получаем указатели на 2 объекта исходных данных. Планируется, что первый несет информацию о текущем состоянии окружающей среды, а по второму информационному потоку передаются данные о состоянии счета и текущих финансовых результатах.
bool CNeuronFinAgent::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!cMarketIntelligence.FeedForward(NeuronOCL)) return false; if(!cMarketMemory.FeedForward(cMarketIntelligence.AsObject())) return false;
Информация об анализируемом состоянии окружающей среды проходит первичную обработку в модуле анализа рынка, включая поиск паттернов трансформером с сегментированным вниманием и выявления их устойчивых комбинаций в модуле памяти.
Затем информация о найденных паттернах в двух проекциях передается модулям низкоуровневой рефлексии для всестороннего анализа динамики рынка.
if(!cTransposeState.FeedForward(cMarketIntelligence.AsObject())) return false; if(!cLowLevelReflection[0].FeedForward(cMarketIntelligence.AsObject())) return false; if(!cLowLevelReflection[1].FeedForward(cTransposeState.AsObject())) return false;
Обратите внимание, что модули низкоуровневой рефлексии работают с паттернами, обнаруженными в текущем состоянии окружающей среды, без учета данных из блока памяти, работающего в магистрали модуля анализа рынка. Это обусловлено необходимостью анализа непосредственной реакции рынка на появление найденных паттернов, что позволяет более точно оценить текущие изменения и тенденции, не опираясь на историческую информацию.
Аналогичная ситуация и с модулем высокоуровневой рефлексии.
if(!cHighLevelReflection.FeedForward(cMarketIntelligence.AsObject(), SecondInput)) return false;
Напомню, что на вход модуля высокоуровневой рефлексии мы подаем информацию о текущем состоянии окружающей среды в виде результатов модуля анализа рынка и вектора финансовых результатов. Тензор предшествующих действий агента используется рекурсивно из буфера результатов модуля высокоуровневой рефлексии.
А вот модуль дополнительных инструментов работает непосредственно с исходными данными, так как ищет сигналы на основе априорных знаний в показателях анализируемых индикаторов.
if(!cTools.FeedForward(NeuronOCL)) return false;
Далее мы переходим к организации процессов модуля принятия решений. Вначале мы обогащаем результаты рефлексии многомерного временного ряда, интегрируя выявленные зависимости в динамике унитарных последовательностей. Это позволяет повысить точность анализа и улучшить понимание взаимодействий в системе, обеспечивая более глубокую и всестороннюю оценку анализируемого состояния окружающей среды.
if(!cCrossLowLevel.FeedForward(cLowLevelReflection[0].AsObject(), cLowLevelReflection[1].getOutput())) return false;
На следующем этапе мы интегрируем информацию, полученную в процессе низкоуровневой рефлексии, в представление устойчивых паттернов, полученное на выходе блока памяти, работающего в магистрали модуля анализа рынка. Это позволяет дополнительно уточнить и усилить уже выявленные закономерности, обеспечивая более точное и комплексное восприятие текущих динамик и взаимодействий в анализируемой системе.
if(!cMarketToLowLevel.FeedForward(cMarketMemory.AsObject(), cCrossLowLevel.getOutput())) return false;
Здесь важно подчеркнуть, что модули низкоуровневой рефлексии анализируют конкретное состояние окружающей среды, выявляя реакцию рынка на отдельные паттерны. Однако, среди рассматриваемых паттернов могут быть такие, которые встречаются редко, и оценка реакции рынка на них может быть нерепрезентативной. В таких случаях мы сохраняем информацию в памяти модуля низкоуровневой рефлексии, поскольку существует вероятность появления аналогичных паттернов в будущем. Что позволит собрать больше данных о реакции рынка на них.
Но мы не можем использовать неподтвержденную информацию для принятия решений. Поэтому, в модуле принятия решений мы опираемся только на устойчивые паттерны, запрашивая информацию о реакции рынка на них в блоке низкоуровневой рефлексии, для более точной и обоснованной оценки.
Далее мы дополняем результаты анализа рыночной ситуации априорными знаниями.
if(!cMarketToTools.FeedForward(cMarketToLowLevel.AsObject(), cTools.getOutput())) return false;
Обратите внимание, мы не добавили обучаемых параметров для интерпретации флагов, сформированных в модуле дополнительных инструментов, хотя такая необходимость обсуждалась ранее. Мы возлагаем этот функционал на параметры формирования сущностей Key и Value в модуле кросс-внимания. Таким образом, интерпретация флагов и их обработка интегрируются непосредственно в процесс кросс-внимания. Следовательно, явное добавление дополнительных обучаемых параметров для интерпретации результатов работы модуля дополнительных инструментов становится излишним.
В завершении работы метода прямого прохода, нам остается проанализировать результаты работы модуля высокоуровневой рефлексии в контексте выявленных устойчивых паттернов и реакции рынка на них. Эту операцию мы выполняем средствами родительского класса.
return CNeuronRelativeCrossAttention::feedForward(cHighLevelReflection.AsObject(), cMarketToTools.getOutput());
}
Логический результат выполнения операций возвращаем вызывающей программе и завершаем работу метода прямого прохода.
За построением метода прямого прохода следует организация процессов обратного прохода. В рамках данной статьи я предлагаю детально рассмотреть алгоритм построения метода распределения градиентов ошибки calcInputGradients, а метод оптимизации обучаемых параметров updateInputWeights можно оставить для самостоятельного изучения.
bool CNeuronFinAgent::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondGradient) return false;
В параметрах метода мы получаем указатели на те же объекты исходных данных, только на этот раз нам предстоит передать в них градиент ошибки в соответствии с влиянием исходных данных на итоговый результат работы модели. И в теле метода мы сразу проверяем актуальность полученных указателей, так как в противном случае выполнение дальнейших операций теряет смысл.
Как уы знаете, распределение градиентов ошибки полностью повторяет информационный поток прямого прохода, только в обратном направлении. Метод прямого прохода завершился вызовом одноименного метода родительского класса. Соответственно, метод распределения градиентов ошибки начинается с вызова метода родительского класса. В нем мы распределяем погрешность работы модели между модулем высокоуровневой рефлексии и предшествующим блоком кросс-внимания модуля принятия решений.
if(!CNeuronRelativeCrossAttention::calcInputGradients(cHighLevelReflection.AsObject(), cMarketToTools.getOutput(), cMarketToTools.getGradient(), (ENUM_ACTIVATION)cMarketToTools.Activation())) return false;
Далее мы последовательно проводим градиент ошибки через все блоки кросс-внимания модуля принятия решений, распределяя погрешности по всем информационным потокам фреймворка, в соответствии с их влиянием на результат работы модели.
if(!cMarketToLowLevel.calcHiddenGradients(cMarketToTools.AsObject(), cTools.getOutput(), cTools.getGradient(), (ENUM_ACTIVATION)cTools.Activation())) return false; //--- if(!cMarketMemory.calcHiddenGradients(cMarketToLowLevel.AsObject(), cCrossLowLevel.getOutput(), cCrossLowLevel.getGradient(), (ENUM_ACTIVATION)cCrossLowLevel.Activation())) return false;
Затем мы распределяем градиент ошибки по модулям низкоуровневой рефлексии.
if(!cLowLevelReflection[0].calcHiddenGradients(cCrossLowLevel.AsObject(), cLowLevelReflection[1].getOutput(), cLowLevelReflection[1].getGradient(), (ENUM_ACTIVATION)cLowLevelReflection[1].Activation())) return false; if(!cTransposeState.calcHiddenGradients(cLowLevelReflection[1].AsObject())) return false;
На данном этапе мы распределили градиент ошибки между всеми модулями нашего фреймворка. И теперь нам предстоит собрать данные со всех информационных потоков на уровне исходных данных. Напомню, что все модули рефлексии и блок памяти модуля анализа рынка работают с результатами предварительной обработки исходных данных в трансформере с сегментированным вниманием. Следовательно, мы сначала собираем градиент ошибки на уровне результатов указанного объекта.
Первым шагом будет передача градиента ошибки от блока памяти.
if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cMarketMemory.AsObject())) return false;
Затем, мы осуществим подмену указателя на буфер градиентов ошибки нашего объекта предварительной обработки исходных данных, что позволит нам сохранить вышеполученные значения.
CBufferFloat *temp = cMarketIntelligence.getGradient(); if(!cMarketIntelligence.SetGradient(cMarketIntelligence.getPrevOutput(), false) || !((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cHighLevelReflection.AsObject(), SecondInput, SecondGradient, SecondActivation) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false;
И вызовем метод распределения градиентов ошибки от модуля высокоуровневой рефлексии. После чего, в обязательном порядке, суммируем значения, полученные по двум информационным потокам.
Стоит отметить, что модуль высокоуровневой рефлексии работает с данными из двух информационных потоков. Таким образом, при распределении градиентов ошибки, через этот модуль одновременно передается погрешность как по основному потоку, так и по магистрали финансового результата. Это позволяет учитывать влияние ошибок на обе важнейшие составляющие анализа, что способствует более точной настройке системы.
Аналогичным образом осуществляется распределение градиента ошибки и по информационным потокам модулей низкоуровневой рефлексии. Однако, в отличие от модуля высокоуровневой рефлексии, эти модули работают только с одним источником исходных данных, что упрощает процесс распределения градиента ошибки.
if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cLowLevelReflection[0].AsObject()) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false; if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cTransposeState.AsObject()) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1) || !cMarketIntelligence.SetGradient(temp, false)) return false;
Не забываем, что после каждой итерации необходимо добавлять полученный градиент ошибки к ранее накопленным значениям. Это гарантирует, что мы корректно учитываем все погрешности работы модели. После обработки всех информационных потоков, важно вернуть указатели на буферы данных в исходное состояние.
Теперь нам остается передать градиент ошибки на уровень исходных данных магистрали основного информационного потока и завершить работу метода, вернув логический результат выполнения операций вызывающей программе.
if(!NeuronOCL.calcHiddenGradients(cMarketIntelligence.AsObject())) return false; //--- return true; }
Обратите внимание, что в алгоритме метода распределения градиентов ошибки не участвует модуль дополнительных инструментов. Как обсуждалось ранее, мы не планируем передачу градиентов ошибки по указанному информационному потоку. А очистка буфера градиентов ошибки объекта, предоставляющего исходные данные в данном случае даже вредна. Ведь тот же объект получает градиент ошибки по магистрали основного информационного потока.
На этом мы завершаем рассмотрение алгоритмов построения фреймворка FinAgent средствами MQL5. Полный код всех представленных объектов и их методов доступен во вложении для вашего ознакомления и дальнейшего использования. Там же представлен код всех программ и архитектура обучаемой модели, используемых при подготовке статьи. Все они практически без изменений были перенесены из статьи, посвященной построению агента с многоуровневой памятью. Изменения коснулись лишь архитектуры модели, где мы заменили один нейронный слой, интегрировав выше представленный фреймворк.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronFinAgent; //--- Windows { int temp[] = {BarDescr, AccountDescr, 2 * NActions, Segments}; //Window, Account description, N Actions, Segments if(ArrayCopy(descr.windows, temp) < int(temp.Size())) return false; } descr.count = HistoryBars; descr.window_out = 32; descr.step = 4; // Heads descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Архитектура остальных слоев модели была перенесена без изменений. А мы движемся дальше. И переходим к заключительному этапу нашей работы — оценке эффективности реализованных подходов на реальных исторических данных.
Тестирование
В последних двух статьях мы подробно рассмотрели фреймворк FinAgent. При этом было реализовано собственное видение подходов, предложенных его авторами. Мы адаптировали алгоритмы фреймворка в соответствии с нашими требованиями. И теперь подошли к решающему этапу — проверке эффективности реализованных решений на реальных исторических данных.
Следует особо отметить, что в процессе разработки мы внесли значительные изменения в алгоритмы, лежащие в основе фреймворка FinAgent. Эти модификации касаются ключевых аспектов работы модели. Поэтому, в данном случае, мы оцениваем не оригинальное решение, а нашу адаптированную версию.
Обучение модели проводилось с использованием исторических данных валютной пары EURUSD за 2023 год на таймфрейме H1. Все настройки индикаторов, используемых моделью, были оставлены на уровне значений по умолчанию, что позволило сосредоточиться на оценке самого алгоритма и его способности работать с исходными данными без дополнительных настроек.
Для начального этапа обучения была использована выборка данных, подготовленная в рамках предыдущих исследований. Мы применили алгоритм обучения с формированием "почти идеальных" целевых действий Агнета, который позволил нам обучать модель без необходимости постоянного обновления обучающей выборки. Однако, несмотря на успешную работу алгоритма в таком формате, мы считаем, что для повышения точности и охвата более широкого спектра состояний счета, регулярное обновление обучающей выборки было бы полезным дополнением.
После нескольких циклов обучения модель продемонстрировала стабильную прибыльность как на обучающих, так и на тестовых данных. Финальное тестирование было проведено на исторических данных за Январь 2024 года. При этом все параметры модели и анализируемых индикаторов были сохранены без изменений. Такой подход позволяет получить объективную оценку эффективности работы модели в условиях, максимально приближенных к реальному рынку. Результаты тестирования представлены ниже.
За период тестирования модель совершила 95 торговых операций, что значительно превышает результаты последних моделей за аналогичный период. Более 42% торговых операций было закрыто с прибылью. Но благодаря тому, что средняя прибыльная сделка в 1.5 раза превышает среднюю убыточную, в целом, за период тестирования, модель получила прибыль. Показатель профит-фактор был зафиксирован на уровне 1.09.
Примечательно, что основная прибыль была получена моделью в первой половине месяца, когда цена колебалась в довольно узком коридоре. А при формировании медвежьего тренда, линя баланса переходит в боковое движение. И даже наблюдается некоторая просадка.
Мое личное мнение, что причины такого поведения можно искать в алгоритмах модулей анализа рынка и дополнительных инструментов. Но это почва для дополнительных исследований.
Заключение
Мы познакомились с фреймворком FinAgent, который представляет собой передовое решение для комплексного анализа рыночной динамики и исторических данных. Авторы фреймворка объединяют текстовую и визуальную информацию, что позволяет значительно расширить возможности для принятия более обоснованных торговых решений. С помощью пяти ключевых компонентов, составляющих архитектуру фреймворка, FinAgent демонстрирует точность и высокую адаптивность, что особенно важно для торговли на финансовых рынках, где условия часто изменяются.
Особое внимание стоит уделить тому, что фреймворк не ограничивается одной лишь областью анализа, а предлагает широкий спектр инструментов, способных эффективно работать как с текстовыми, так и с графическими данными. Такой подход позволяет учитывать множество факторов, влияющих на рынок, и обеспечивает более глубокое понимание рыночных процессов. Эти особенности делают FinAgent перспективным инструментом для разработки торговых стратегий, которые могут адаптироваться под изменяющиеся рыночные условия и учитывать даже самые мелкие колебания на рынке.
В практической части нашей работы было реализовано собственное видение предложенных подходов средствами MQL5. Мы обучили модель, интегрировав в нее предложенные подходы, и провели ее тестирование на реальных исторических данных. Результаты тестирования показали возможность модели генерировать прибыль. Однако, доходность модели оказалась зависимой от рыночной ситуации. Так же, необходимо проведение дополнительных экспериментов с целью поиска путей для повышения адаптивности модели к динамически меняющимся условиям рынка.
Ссылки
- A Multimodal Foundation Agent for Financial Trading: Tool-Augmented, Diversified, and Generalist
- Другие статьи серии
Программы, используемые в статье
# | Имя | Тип | Описание |
---|---|---|---|
1 | Research.mq5 | Советник | Советник сбора примеров |
2 | ResearchRealORL.mq5 | Советник | Советник сбора примеров методом Real-ORL |
3 | Study.mq5 | Советник | Советник обучения моделей |
4 | Test.mq5 | Советник | Советник для тестирования модели |
5 | Trajectory.mqh | Библиотека класса | Структура описания состояния системы и архитектуры моделей |
6 | NeuroNet.mqh | Библиотека класса | Библиотека классов для создания нейронной сети |
7 | NeuroNet.cl | Библиотека | Библиотека кода OpenCL-программы |





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