Нейросети в трейдинге: Когнитивная инерция в анализе финансовых рынков (модуль временной согласованности)
Введение
Финансовый рынок редко ломает торговую модель одной свечой. Чаще он действует тоньше: даёт понятный импульс, затем короткий откат и снова возвращается к прежнему направлению. Для трейдера это обычный рыночный шум. Для модели без устойчивого внутреннего состояния такой участок легко превращается в череду взаимоисключающих сигналов.
В этом и состоит главный интерес CogDriver в контексте финансовых рынков. Изначально фреймворк был предложен для автономного вождения. Там агент видит очередной кадр дорожной сцены, но не может строить движение как набор независимых реакций. Если система уже начала манёвр, она должна понимать, продолжается прежний сценарий или среда действительно изменилась.
Авторы CogDriver называют это когнитивной инерцией. Идея проста: агенту нужна способность сохранять рабочее намерение во времени. Не цепляться за него любой ценой и не сглаживать решения механически, а удерживать связную линию рассуждения, пока новые данные не дают оснований её пересмотреть.
В автономном вождении отсутствие такой связности проявляется как Decision Jitter: модель то продолжает движение, то резко меняет решение, то возвращается к прежнему плану. На рынке похожая проблема проявляется в торговом контуре. Модель открывает позицию, закрывает её на локальном шуме, затем снова входит почти в том же направлении. В отчёте Strategy Tester это превращается в лишние сделки, преждевременные выходы, рост издержек и ухудшение торговой траектории.
С этой постановки задачи мы начали предыдущую статью. Мы не пытались буквально перенести автомобильную архитектуру на финансовый рынок. У рынка нет полос движения, физических объектов и ego-motion автомобиля. Зато есть собственная система координат: цена, волатильность, объём, локальные экстремумы, диапазоны, состояние позиции и риск. Поэтому адаптация CogDriver свелась не к копированию модулей, а к поиску их функциональных аналогов.
В авторской работе CogDriver опирается на два направления. Первое связано с данными: CogDriver-Data формирует обучающий материал, где действия агента описаны как причинно связанная история. Второе связано с архитектурой: CogDriver-Agent поддерживает устойчивое внутреннее состояние через временную память и механизм согласования текущей сцены с накопленным контекстом. Для финансовых рынков оба направления важны, но начинать нужно было с нижнего слоя. Без корректного представления рыночной сцены дальнейшая память быстро превращается в склад шума.
В первой статье основное внимание было уделено подготовке рыночного состояния. Мы ввели MarketStateDensity как рыночный аналог плотностной карты ситуации. Обычный временной ряд показывает последовательность значений. MarketStateDensity позволяет смотреть шире: на распределение состояний, концентрацию движения, положение цены относительно локального диапазона, структуру волатильности и устойчивость текущего режима. Это уже не одна свеча и не набор индикаторов, а компактная карта рыночной сцены.
Однако один снимок всё ещё остаётся снимком. Поэтому следующим шагом стало накопление таких представлений в стек. Последний бар сам по себе часто обманчив. В составе стека он получает контекст: то, что на одиночном срезе похоже на разворот, в динамике может оказаться обычной коррекцией; а слабый откат после импульса — началом смены режима. Для CogDriver это принципиально. Когнитивная инерция рождается не из последней точки, а из связи между состояниями.
Практическим результатом первой статьи стал класс CNeuronCogDriverData, реализующий нижний слой будущего фреймворка в инфраструктуре MQL5 и OpenCL. Мы не выносим подготовку данных во внешний препроцессор. Блок остаётся частью вычислительного графа: через него можно проводить прямой проход, обратное распространение ошибки, обновление обучаемых параметров и сохранение состояния модели.
Логика работы CNeuronCogDriverData такова: нормализация входных признаков, обновление стека состояний, построение MarketStateDensity, расчёт аффинной коррекции, обновление стека квантильных представлений и извлечение признаков финальным свёрточным блоком.
На выходе мы получаем подготовленное представление динамики рыночных состояний. Это первый практический шаг к финансовой версии CogDriver: модель получила рабочую память восприятия и основу для временного согласования.
Нижний слой ещё не делает систему полноценной реализацией CogDriver: он описывает, как рынок пришёл к текущему состоянию, но не определяет, как согласовать текущую сцену с накопленной внутренней гипотезой модели. Для торгового агента это критично. Если каждый новый бар полностью переписывает внутреннее состояние, мы возвращаемся к реактивной модели. Если прошлое состояние сохраняется без проверки, модель начинает тащить старый сценарий против рынка.
Значит, между подготовленной рыночной сценой и торговым решением нужен слой временного согласования. В оригинальном CogDriver эту роль выполняет Temporal Coherence Module. Он переносит исторические запросы в текущую систему координат, уточняет их с учётом нового состояния и объединяет прошлый контекст с текущим наблюдением. В финансовой адаптации нужен тот же принцип, но на рыночном материале.
Задача следующего модуля — связать выход CNeuronCogDriverData с внутренней памятью агента и превратить накопленный стек состояний в управляемую рабочую память. Нижний слой уже подготовил плотностное представление рынка. Теперь модель должна не просто хранить историю, а переоценивать её относительно текущего состояния и использовать только релевантный контекст.
Для трейдера это особенно понятно. Последняя свеча часто выглядит убедительно, но она не всегда хороший ориентир. Сильный бар после серии слабых движений может быть ловушкой. Небольшой откат после устойчивого импульса может быть обычной паузой. Расширение волатильности может подтвердить пробой, а может разрушить прежний сценарий. Разницу нельзя надёжно увидеть по одному срезу. Её нужно оценивать через динамику состояния.
Мы продолжаем работу с того места, где остановились ранее. У нас уже есть нижний слой данных CogDriver. Теперь переходим от памяти восприятия к памяти рассуждения. Первая статья подготовила рынок к анализу. В этой статье мы научим модель удерживать смысл этого анализа во времени и адаптировать его к текущей ситуации. Именно здесь CogDriver начинает превращаться из идеи когнитивной инерции в рабочий вычислительный контур для финансовых рынков.

Адаптация архитектуры модуля TCM
После подготовки рыночной сцены возникает следующий вопрос: как сохранить её смысл во времени? Сам по себе стек состояний ещё не делает модель устойчивой. Он только даёт материал для анализа. Дальше нужен механизм, который сопоставит текущую картину с накопленной памятью и определит, какие прошлые состояния действительно помогают понять рынок сейчас.
В оригинальном CogDriver эту роль выполняет Temporal Coherence Module. Он согласует текущую дорожную сцену с исторической памятью агента. Для автономного вождения это естественно: автомобиль движется, окружающие объекты смещаются, а прошлые запросы нужно привести к новой системе координат. Только после этого их можно использовать как временный контекст.
На финансовом рынке такой геометрии нет. Цена не движется по дороге, волатильность не является объектом в соседней полосе, а объём нельзя перенести из прошлого кадра через физическое смещение. Поэтому прямое копирование TCM не сработает. Но сама идея остаётся ценной: текущее состояние нельзя анализировать изолированно. Его нужно согласовать с памятью прошлых состояний.
Так появляется рыночный аналог TCM. Он принимает подготовленное представление рынка, выделяет текущий запрос состояния и обращается к памяти ранее накопленных запросов. При этом память не должна быть простой очередью последних наблюдений. Рынок быстро меняет режимы, и ближайшее прошлое не всегда оказывается самым полезным.
Обычное временное окно устроено слишком прямолинейно. Последний бар получает максимальную близость просто потому, что он последний, а более старые состояния постепенно уходят из поля зрения. Для сглаживания это допустимо. Для CogDriver — недостаточно. Нам нужно не просто хранить историю по времени, а переоценивать её относительно текущего рынка.
Отсюда появляется ключевая идея рыночного TCM: память должна быть ранжируемой. Текущий запрос сравнивается с историческими запросами, и каждый элемент получает оценку полезности. Эта оценка не говорит, хороший перед нами торговый сигнал или плохой. Она отвечает на более низкоуровневый вопрос: помогает ли данный фрагмент прошлой рыночной сцены интерпретировать текущую ситуацию.
TCM не должен выбирать торговое действие. Он не решает, покупать или продавать, не оценивает прибыльность позиции и не строит окончательный план. Его место ниже. Он формирует временно согласованное представление рынка, которое затем может быть передано в Actor-Critic, прогнозную голову или другой верхний модуль.
Такое разделение делает архитектуру управляемой. Если на уровне временной памяти сразу решать торговую задачу, модуль начнёт смешивать слишком разные функции: хранение состояния, оценку контекста, выбор действия и объяснение результата. Это путь к шумной модели. Поэтому роль TCM уже и чище: согласовать текущую рыночную сцену с релевантной памятью.
В оригинальном CogDriver память работает как rolling bank наиболее значимых запросов. Для дорожной сцены это оправдано. Один и тот же объект может наблюдаться в нескольких последовательных кадрах, и его повторное присутствие помогает не потерять трек. На рынке такая логика может дать перекос: один режим начнёт многократно попадать в память и усиливать сам себя.
Поэтому в финансовой адаптации память обновляется без технического дублирования. Новый запрос добавляется как новое состояние. Старые элементы не копируются поверх прежней памяти, а конкурируют за сохранение. Если память заполнена, вытесняется наименее релевантный элемент. Так мы сохраняем ограниченный набор прошлых состояний и не позволяем одному удачному фрагменту занять весь контекст.
Это отличие кажется техническим, но для рынка оно принципиально. Финансовые временные ряды часто возвращаются к похожим состояниям. Однако похожесть не равна тождественности. Два импульса могут быть близки по форме, но один возникает после накопления, а другой — после истощения тренда. Если память механически размножает похожие фрагменты, внимание модели становится уже, а не шире.
Ещё одно отклонение от оригинального алгоритма связано с формой данных. После блока подготовки рынка мы работаем не с одним общим вектором, а с набором переменных. Цена, волатильность, объём, диапазон и производные признаки несут разную информацию. Их полезная временная глубина тоже различается: одной переменной важен ближайший откат, другой — более старая зона накопления, третьей — изменение амплитуды движения.
Поэтому память целесообразно вести не только по времени, но и по переменным. Такая схема позволяет каждой группе признаков иметь собственную историю релевантных состояний. Переменные не конкурируют за один общий банк памяти. Это особенно важно для финансовых данных, где слабый по масштабу признак может быть сильным по смыслу. Например, изменение волатильности нередко предупреждает о смене режима раньше, чем цена покажет явный разворот.
В такой архитектуре текущий запрос сначала обращается к своей временной памяти. Каждая переменная ищет в прошлом состояния, которые уточняют её собственную линию. После этого переменные можно согласовать между собой. Сначала мы уточняем отдельные каналы по их истории, затем собираем общую рыночную картину.
Для извлечения контекста из памяти естественно использовать механизм внимания. Текущий запрос выступает как вопрос к прошлому. Исторические запросы становятся ключами и значениями. Если память пуста, модель опирается только на текущий запрос. Если память заполнена, внимание выбирает не ближайшее состояние, а наиболее совместимые фрагменты прошлого.
Но память не должна иметь безусловный приоритет. Рынок может резко изменить режим, и в такие моменты прошлый контекст становится опасным. Он полезен только до тех пор, пока согласуется с текущей сценой. Поэтому после извлечения контекста нужен механизм смешивания текущего состояния и памяти. Gate определяет, насколько сильно использовать исторический контекст.
Так TCM становится не сглаживающим фильтром, а механизмом проверки. Фильтр стремится сделать траекторию плавнее. TCM должен сделать её осмысленнее. Он не обязан подавлять каждое резкое изменение: иногда именно резкое изменение и есть главный сигнал. Поэтому память используется через оценку релевантности и управляемое смешивание, а не через простое усреднение.
Отдельного внимания требует возраст элементов памяти. В оригинальной задаче прошлые запросы можно выравнивать через движение автомобиля и пространственную трансформацию. В финансовой задаче такого выравнивания нет, но есть фактор давности. Один и тот же паттерн, возникший один бар назад и двадцать баров назад, не должен восприниматься одинаково.
Возрастная кодировка позволяет памяти хранить не только похожесть состояния, но и его временную дистанцию. Недавний всплеск волатильности может быть прямым продолжением текущего движения. Старый всплеск может быть полезной аналогией, но уже не должен иметь тот же вес. Так вместо пространственного выравнивания оригинального CogDriver мы используем временную маркировку рыночных состояний.
Важным правилом становится порядок обновления памяти. Текущий запрос не должен участвовать в Attention как элемент собственной истории. Иначе возникает самоподтверждение: модель сформировала состояние, поместила его в память и тут же извлекла как контекст. Для обучения это слишком лёгкий путь. Историческая память должна быть действительно исторической.
Отложенный принцип делает архитектуру чище. В текущем проходе модель использует только прошлые запросы. Затем она сохраняет текущий запрос как кандидат для будущей памяти. На следующем шаге он становится историческим элементом и участвует в сопоставлении с новым рынком. Память отражает последовательность состояний без подмешивания текущего запроса в собственный контекст.
Обучение такого блока тоже нужно ограничить. Память между шагами лучше рассматривать как состояние, а не как набор обучаемых параметров. Градиент проходит через формирование текущего запроса, Attention и Gate. Но операции управления памятью — ранжирование, выбор слота, возраст и маска валидности — не требуют дифференцирования. Мы не протаскиваем градиент через всю историю рынка, а учим модель правильно использовать доступный контекст здесь и сейчас.
В адаптированном варианте TCM становится второй ступенью финансового CogDriver: он превращает плотностную карту и стек состояний в рабочую временную память, выделяя текущий запрос, сопоставляя его с историей, оценивая релевантность прошлых состояний, извлекая контекст и смешивая его с текущим представлением.
Такой модуль не делает модель умнее сам по себе. Он делает её менее слепой к собственной истории. На рынке часто проигрывает система, которая каждый новый сигнал видит слишком отдельно от предыдущего. Адаптированный TCM должен закрыть этот разрыв и подготовить более устойчивое состояние для последующего торгового решения.
Алгоритмы OpenCL-программы
После определения общей логики модуля переходим к отдельным операциям на стороне OpenCL. Здесь уже не повторяем архитектуру TCM. Нас интересуют точечные вычислительные задачи: как оценить релевантность элементов памяти, выбрать слот замещения, обновить возраст и записать новый Query без нарушения причинности.
Первый специализированный кернел — CogDriverRankTCMMeanFlashScore. Он выполняется после Flash-Attention и считает средний Score элементов памяти. Кернел не меняет память. Его задача — оценить, насколько каждый слот был полезен при обработке текущего Query.
__kernel void CogDriverRankTCMMeanFlashScore(__global const float* query, __global const float* memory_kv, __global const float* valid, __global float* out_score, __global const float* logsumexp, const int memory_count, const int variables, const int heads, const int head_dim ) { const int slot = get_global_id(0); const int variable = get_global_id(1); if(slot >= memory_count || variable >= variables || heads <= 0 || head_dim <= 0) return;
Пространство выполнения организовано по слотам памяти и переменным. Каждый поток отвечает за одну пару [slot, variable]. Сначала проверяются границы и валидность слота. Если элемент памяти невалиден, его Score устанавливается в MIN_VALUE. Так пустые слоты не конкурируют с реальными историческими состояниями.
const int state_shift = RCtoFlat(slot, variable, memory_count, variables, 0); if(valid[state_shift] < 0.5f) { out_score[state_shift] = MIN_VALUE; return; }
Для валидного слота кернел проходит по всем головам внимания данной переменной. Такая схема сохраняет per-variable логику памяти: каждая переменная оценивает собственные исторические состояния и не конкурирует с другими переменными в общем банке.
float score = 0.0f; const int total_heads = variables * heads; for(int head = 0; head < heads; head++) { const int effective_head = variable * heads + head; const int shift_q = RCtoFlat(effective_head, 0, total_heads, head_dim, 0); const int shift_k = RCtoFlat(effective_head, 0, 2 * total_heads, head_dim, slot); const int shift_lse = RCtoFlat(0, effective_head, 1, total_heads, 0); const float head_lse = IsNaNOrInf(logsumexp[shift_lse], 0.0f); float dotv = 0.0f; for(int d = 0; d < head_dim; d++) dotv += IsNaNOrInf(query[shift_q + d], 0.0f) * IsNaNOrInf(memory_kv[shift_k + d], 0.0f); const float raw_score = IsNaNOrInf(dotv / sqrt((float)head_dim), MIN_VALUE); score += IsNaNOrInf(exp(clamp(raw_score - head_lse, -120.0f, 0.0f)), 0.0f); } out_score[state_shift] = IsNaNOrInf(score / (float)heads, 0.0f); }
Для каждой головы вычисляется скалярное произведение текущего Query и соответствующего ключа памяти. Ключ берётся из memory_kv, где исторический Query уже приведён к форме, пригодной для внимания. Если используется кодировка возраста состояния, она уже учтена в этом буфере. Поэтому оценка отражает не только близость активаций, но и временную маркировку состояния.
Полученное скалярное произведение делится на корень из размерности вектора, как в стандартном механизме внимания. Затем используется значение logsumexp, рассчитанное внутри Flash-Attention.
После прохода по всем головам значения усредняются. Итоговый Score показывает, какую среднюю долю внимания получил слот памяти по головам своей переменной. Это служебная оценка. Она не является торговым сигналом и не говорит, был ли фрагмент рынка прибыльным. Она отвечает только на вопрос, участвовал ли этот элемент памяти в интерпретации текущего состояния.
После расчёта Score переходим ко второму специализированному кернелу — CogDriverRankTCMInsertPending. Его задача — записать новый Query в память, используя обновлённые оценки релевантности. Название можно понимать как вставку подготовленного Query, отложенного до завершения работы внимания.
__kernel void CogDriverRankTCMInsertPending(__global float* memory, __global float* age, __global float* score, __global float* valid, __global const float* last_query, const int memory_len, const int query_dim, const int variables ) { const int variable = get_global_id(0); const int dim = get_global_id(1); const int lid = get_local_id(1); const int lsize = get_local_size(1); if(variable >= variables || dim >= query_dim || memory_len <= 0 || query_dim <= 0) return;
Кернел получает буферы памяти, возраста, Score, маски валидности и Query для вставки. Пространство выполнения организовано по двум измерениям: переменная и компонент Query. Каждая переменная обрабатывает собственную строку памяти, а рабочие элементы внутри группы распределяют между собой компоненты вектора и слоты памяти.
Сначала каждый work-item просматривает часть слотов памяти своей переменной. Проход выполняется с шагом local_size, поэтому вся рабочая группа совместно покрывает полный диапазон памяти.
__local int invalid_slot[LOCAL_ARRAY_SIZE]; __local int min_slot[LOCAL_ARRAY_SIZE]; __local int min_age[LOCAL_ARRAY_SIZE]; __local float min_score[LOCAL_ARRAY_SIZE]; __local int insert_slot[1]; int local_invalid = memory_len; int local_min_slot = -1; int local_min_age = 0; float local_min_score = MAX_VALUE; //--- each work-item scans a part of one variable memory row int step = min((int)LOCAL_ARRAY_SIZE, (int)lsize); if(lid < step) { for(int slot = lid; slot < memory_len; slot += step) { const int state_shift = RCtoFlat(slot, variable, memory_len, variables, 0); if(valid[state_shift] < 0.5f) { if(slot < local_invalid) local_invalid = slot; continue; }
Для каждого слота поток проверяет маску valid. Если слот ещё не заполнен, он рассматривается как кандидат на вставку. Приоритет получает свободный слот с минимальным индексом. Это позволяет частично заполненной памяти расти без преждевременного вытеснения накопленных состояний.
Если слот валиден, поток читает его Score и проверяет значение на NaN и Inf. Среди валидных элементов выбирается слот с минимальным Score. При равенстве дополнительно учитывается возраст элемента. Это делает выбор детерминированным и упрощает отладку.
const float sc = IsNaNOrInf(score[state_shift], MIN_VALUE); const int t_age = (int)IsNaNOrInf(age[state_shift], 0.0f); age[state_shift] = (float)(t_age + 1); if(local_min_slot < 0 || sc < local_min_score || (sc == local_min_score && t_age > local_min_age)) { local_min_slot = slot; local_min_score = sc; local_min_age = t_age; } } invalid_slot[lid] = local_invalid; min_slot[lid] = local_min_slot; min_score[lid] = local_min_score; min_age[lid] = local_min_age; } BarrierLoc
Промежуточные кандидаты записываются в локальную память рабочей группы. После барьера один поток выполняет редукцию. Сначала он ищет лучший свободный слот. Если память ещё не заполнена, новый Query записывается туда.
//--- reduce to one replacement slot per variable if(lid == 0) { int best_invalid = memory_len; int best_min_slot = -1; int best_min_age = 0; float best_min_score = MAX_VALUE; for(int i = 0; i < lsize; i++) { if(invalid_slot[i] < best_invalid) best_invalid = invalid_slot[i]; if(min_slot[i] >= 0 && (best_min_slot < 0 || min_score[i] < best_min_score || (min_score[i] == best_min_score && min_age[i] < best_min_age))) { best_min_slot = min_slot[i]; best_min_score = min_score[i]; best_min_age = min_age[i]; } } insert_slot[0] = (best_invalid < memory_len ? best_invalid : best_min_slot); } BarrierLoc
Если свободных слотов нет, выбирается валидный слот с минимальной средней оценкой внимания.
После выбора целевого слота рабочая группа параллельно записывает компоненты Query в выбранный слот памяти. Каждый поток отвечает за свою компоненту вектора. Значения проходят через защиту от NaN и Inf, чтобы некорректные активации не разрушили дальнейшие вычисления.
const int target_slot = insert_slot[0]; if(target_slot < 0) return; //--- write the delayed query to the replacement slot memory[RCtoFlat(variable, dim, variables, query_dim, target_slot)] = IsNaNOrInf(last_query[RCtoFlat(variable, dim, variables, query_dim, 0)], 0.0f); if(lid == 0) { const int state_shift = RCtoFlat(target_slot, variable, memory_len, variables, 0); age[state_shift] = 1.0f; score[state_shift] = 0.0f; valid[state_shift] = 1.0f; } }
Один поток группы завершает служебное обновление: помечает слот как валидный и сбрасывает его Score до начального значения.
В итоге два кернела работают как связанная пара. CogDriverRankTCMMeanFlashScore измеряет участие памяти в текущем Attention. CogDriverRankTCMInsertPending использует эту оценку для замены слота и записи нового Query. Такое разделение удобно для отладки: сначала можно проверить Score после Flash-Attention, а затем отдельно проверить выбор слота, редукцию и запись Query.
Построение объекта TCM
Для реализации адаптированного модуля временной согласованности на стороне основной программы создаётся объект CNeuronCogDriverRankTCM, унаследованный от базового слоя OpenCLCNeuronBaseOCL. Наследование позволяет встроить модуль в существующий нейросетевой фреймворк без изменения общей логики построения моделей. Снаружи это обычный слой: он получает входной буфер, выполняет прямой проход, передаёт результат дальше и участвует в обратном распространении ошибки. Внутри он содержит собственную память рыночных состояний и несколько связанных вычислительных контуров.
class CNeuronCogDriverRankTCM : public CNeuronBaseOCL { protected: uint iInputDim; uint iQueryDim; uint iVariables; uint iHeads; uint iHeadDim; uint iTotalQueryDim; uint iTotalHeads; uint iMemoryLen; uint iMemoryCount; bool bLastQueryValid; CNeuronSpikeConvBlock cQueryProjection; CNeuronBaseOCL cGateInput; CNeuronBaseOCL cGateProjection; CNeuronBaseOCL cMemoryContext; CNeuronBaseOCL cSelfContext; CBufferFloat cMemoryQuery; CBufferFloat cMemoryAge; CBufferFloat cMemoryScore; CBufferFloat cMemoryValid; CBufferFloat cLastQuery; CBufferFloat cMemoryKV; CBufferFloat cMemoryKVGradient; CBufferFloat cSelfKV; CBufferFloat cSelfKVGradient; CBufferFloat cLogSumExp; CBufferFloat cSelfLogSumExp; CBufferFloat cQueryGateGradient; CBufferFloat cGateQueryGradient; CBufferFloat cGateContextGradient; //--- virtual bool InitStateBuffers(void); virtual bool InsertPendingQuery(void); virtual bool BuildMemoryKV(void); virtual bool FlashAttentionForward(CBufferFloat *query, CBufferFloat *out); virtual bool FlashAttentionBackward(CBufferFloat *query, CBufferFloat *query_gr, CBufferFloat *out, CBufferFloat *out_gr); virtual bool BuildSelfKV(CBufferFloat *source); virtual bool SelfAttentionForward(CBufferFloat *query, CBufferFloat *out); virtual bool SelfAttentionBackward(CBufferFloat *query, CBufferFloat *query_gr, CBufferFloat *out, CBufferFloat *out_gr); virtual bool MeanFlashScores(CBufferFloat *query); virtual bool StoreLastQuery(CBufferFloat *query); virtual bool CopyBuffer(CBufferFloat *source, CBufferFloat *target, uint dimension); virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronCogDriverRankTCM(void) : iInputDim(0), iQueryDim(0), iVariables(0), iHeads(0), iHeadDim(0), iTotalQueryDim(0), iTotalHeads(0), iMemoryLen(0), iMemoryCount(0), bLastQueryValid(false) {}; ~CNeuronCogDriverRankTCM(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint input_dim, uint query_dim, uint variables, uint memory_len, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronCogDriverRankTCM; } virtual uint MemoryCount(void) const { return iMemoryCount; } virtual bool LastQueryValid(void) const { return bLastQueryValid; } //--- 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 TrainMode(bool flag) override; virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual bool Clear(void) override; };
Объект не принимает торговых решений. Он не выбирает направление позиции и не оценивает прибыльность сделки. Его задача ниже: получить подготовленное представление рынка, сформировать текущий Query, обратиться к памяти ранее сохранённых состояний, извлечь временной контекст и вернуть уточнённое представление. Это представление уже может использоваться выше — в прогнозной голове, Actor-Critic или другом управляющем блоке.
Внутри класса выделяются несколько логических групп данных. Первая отвечает за размерности: параметры входа, размер Query, количество переменных, число голов внимания, размер одной головы, общий размер Query и общее число голов. Это необходимо, потому что вход рассматривается не как единый вектор, а как набор переменных, каждая из которых имеет собственное представление и раскладывается на головы внимания.
Вторая группа связана с памятью. Объект хранит максимальную длину памяти и текущее количество заполненных элементов. Это позволяет корректно работать на начальных шагах, когда память ещё не заполнена. Флаг валидности последнего Query защищает от обращения к несуществующей истории и предотвращает загрязнение памяти на старте.
Отдельное место занимают внутренние вычислительные блоки. Для формирования Query используется свёрточная проекция, которая лучше подходит к структурированному выходу CogDriverData, чем простое линейное преобразование. Отдельные OpenCL-объекты отвечают за подготовку Gate, хранение Memory Context и Self Context. Такое разделение делает архитектуру прозрачной: временной контекст, согласование переменных и управляемое смешивание не смешиваются в одном буфере.
Инициализация объекта выполняется методом Init. Он получает размерности, количество переменных, длину памяти, число голов, тип оптимизации и параметры OpenCL-контекста. На этом этапе проверяется согласованность параметров. Все размерности должны быть положительными, а размер Query должен делиться на число голов без остатка. Иначе невозможно корректно определить размер одной головы и запустить механизмы внимания.
bool CNeuronCogDriverRankTCM::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint input_dim, uint query_dim, uint variables, uint memory_len, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch) { if(input_dim == 0 || query_dim == 0 || variables == 0 || memory_len == 0 || heads == 0 || query_dim % heads != 0) ReturnFalse; uint total_query = variables * query_dim; if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, total_query, optimization_type, batch)) ReturnFalse;
После проверки рассчитываются производные величины — общий размер Query и общее число голов внимания. Эти значения используются при инициализации базового слоя и внутренних структур. Затем заполняются поля объекта, обнуляются счётчики памяти и сбрасывается флаг валидности последнего Query. Так формируется корректное начальное состояние.
iInputDim = input_dim; iQueryDim = query_dim; iVariables = variables; iHeads = heads; iHeadDim = query_dim / heads; iTotalQueryDim = total_query; iTotalHeads = variables * heads; iMemoryLen = memory_len; iMemoryCount = 0; bLastQueryValid = false;
Далее настраиваются внутренние блоки. Проекция преобразует входное представление в пространство Query без дополнительной нелинейности.
if(!cQueryProjection.Init(0, 0, OpenCL, input_dim, input_dim, query_dim, variables, 1, optimization, iBatch)) ReturnFalse; cQueryProjection.SetActivationFunction(None);
Блоки Gate подготавливают объединённое представление текущего состояния и контекста, а затем формируют управляющий коэффициент с сигмоидальной активацией.
if(!cGateInput.Init(total_query, 1, OpenCL, 2 * total_query, optimization, iBatch)) ReturnFalse; cGateInput.SetActivationFunction(None); if(!cGateProjection.Init(0, 2, OpenCL, total_query, optimization, iBatch)) ReturnFalse; cGateProjection.SetActivationFunction(SIGMOID);
Контекстные блоки используются как промежуточные буферы без дополнительных преобразований.
if(!cMemoryContext.Init(0, 3, OpenCL, total_query, optimization, iBatch)) ReturnFalse; cMemoryContext.SetActivationFunction(None); if(!cSelfContext.Init(0, 4, OpenCL, total_query, optimization, iBatch)) ReturnFalse; cSelfContext.SetActivationFunction(None);
После этого выделяются буферы состояния: память Query, возраст, Score, маски валидности, структуры для внимания и градиентов.
if(!InitStateBuffers()) ReturnFalse; //--- return Clear(); }
Инициализация завершается очисткой всех буферов. Память считается пустой, все элементы невалидны, счётчики обнулены. Это гарантирует корректную работу первого прохода без обращения к неинициализированной истории.
При любой ошибке инициализация прерывается. Использовать слой в неконсистентном состоянии нельзя.
Прямой проход реализуется методом feedForward. Он собирает все элементы объекта в единую последовательность вычислений.
bool CNeuronCogDriverRankTCM::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || NeuronOCL.Neurons() < int(iInputDim * iVariables)) ReturnFalse;
На вход поступает представление рынка от предыдущего слоя. Первым шагом выполняется отложенное обновление памяти: если на предыдущем шаге был сформирован Query, он записывается в память только сейчас. Так текущий Query не участвует в механизме внимания как элемент собственной памяти.
//--- Step 1: deferred memory mutation from the previous pass if(!InsertPendingQuery()) ReturnFalse;
Затем из входного состояния формируется новый Query. Он используется как запрос к уже накопленной памяти.
//--- Step 2: current query if(!cQueryProjection.FeedForward(NeuronOCL)) ReturnFalse; CBufferFloat *query = cQueryProjection.getOutput(); if(!query) ReturnFalse;
Если память пуста, слой корректно вырождается: выполняется только Self-Attention между переменными, результат возвращается на выход, а Query откладывается для записи на следующем шаге.
//--- Step 3-5: attention, score, and fusion if(iMemoryCount == 0) { if(!SelfAttentionForward(query, Output)) ReturnFalse; if(!StoreLastQuery(query)) ReturnFalse; return true; }
Если память содержит данные, сначала формируются ключи и значения для её элементов. При необходимости добавляется возрастная кодировка.
if(!BuildMemoryKV())
ReturnFalse;
Затем выполняется Flash-Attention. На выходе получается Memory Context — представление релевантных исторических состояний.
if(!FlashAttentionForward(query, cMemoryContext.getOutput()))
ReturnFalse;
После этого выполняется Self-Attention, который согласует переменные между собой уже с учётом извлечённого временного контекста. Так формируется целостное представление текущей рыночной сцены.
if(!SelfAttentionForward(cMemoryContext.getOutput(), cSelfContext.getOutput()))
ReturnFalse;
Далее рассчитывается Score элементов памяти — оценка их влияния на текущее состояние системы. Эти значения сохраняются и используются при следующем обновлении памяти для выбора элемента на замену.
if(!MeanFlashScores(query))
ReturnFalse;
Финальный этап — Gated Fusion. Текущий Query и контекст объединяются, формируется управляющий коэффициент, затем выполняется управляемое смешивание. В зависимости от ситуации модель может больше опираться либо на память, либо на текущее состояние.
if(!Concat(query, cSelfContext.getOutput(), cGateInput.getOutput(), (int)iTotalQueryDim, (int)iTotalQueryDim, 1)) ReturnFalse; if(!cGateProjection.FeedForward(cGateInput.AsObject())) ReturnFalse; if(!GateElementMult(cSelfContext.getOutput(), query, cGateProjection.getOutput(), Output)) ReturnFalse;
Результат записывается в выходной буфер слоя. Это уже не просто текущее состояние рынка, а временно согласованное представление с учётом релевантного исторического контекста. В конце текущий Query сохраняется как отложенный и будет записан в память только на следующем шаге. Такой механизм сохраняет причинность и исключает самоподтверждение состояний.
//--- Current query is retained only for the next forward pass. if(!StoreLastQuery(query)) ReturnFalse; //--- return true; }
В итоге прямой проход завершает обновление памяти предыдущего шага, формирует новый Query, извлекает контекст, согласует переменные, оценивает память и формирует итоговое состояние, откладывая запись текущего состояния до следующего обращения.
CNeuronCogDriverRankTCM представляет собой контейнер вычислительного контура TCM. Снаружи это один слой, внутри — система с собственной памятью.
Заключение
В этой статье мы продолжили адаптацию фреймворка CogDriver к финансовым рынкам и перешли от подготовки рыночной сцены к слою временной согласованности. Теперь модель не просто видит текущее состояние рынка, а сопоставляет его с памятью ранее накопленных состояний.
Ключевым результатом стала адаптация идеи Temporal Coherence Module под природу финансовых временных рядов. Вместо пространственного согласования дорожной сцены мы используем рыночную Query-память. Прошлые состояния не лежат в ней пассивным архивом. Они оцениваются через механизм внимания, получают Score и участвуют в формировании уточнённого состояния только тогда, когда действительно помогают интерпретировать текущий рынок.
Отдельное внимание уделено причинности вычислений. Текущий Query не используется как элемент собственной памяти. Сначала он обращается к накопленным состояниям, затем по результатам внимания оценивается полезность памяти, и только после этого Query откладывается для последующего обновления. Такой порядок защищает модель от самоподтверждения.
Практическая часть была сосредоточена на OpenCL-реализации точечных операций и построении объекта CNeuronCogDriverRankTCM. Мы рассмотрели кернелы расчёта среднего Score и обновления памяти, а затем собрали их в слой, совместимый с существующим нейросетевым фреймворком. В результате получен модуль, который принимает подготовленное состояние рынка, извлекает релевантный исторический контекст и формирует временно согласованное представление для следующих блоков модели.
О качестве торговых решений говорить рано. Это задача следующих этапов. Сейчас важнее другое: архитектура CogDriver получила второй рабочий контур. После памяти восприятия появился слой памяти состояния. Именно он должен стать основой для торгового агента, который принимает решение не по последней свече, а по развитию рыночной ситуации во времени.
Ссылки
- CogDriver: Integrating Cognitive Inertia for Temporally Coherent Planning in Autonomous Driving
- Другие статьи серии
Программы, используемые в статье
| # | Имя | Тип | Описание |
|---|---|---|---|
| 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 не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Рыночные секреты Ларри Уильямса (Часть 10): Автоматизация паттернов разворота Smash Day
Разработка динамического мультивалютного советника (Часть 8): Ротация капитала в зависимости от времени суток
От начального до среднего уровня: Произвольный доступ (I)
Рыночные секреты Ларри Уильямса (Часть 9): Торговые паттерны для получения прибыли
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования