preview
Нейросети в трейдинге: Диффузионная генерация торговых планов (Планировщик)

Нейросети в трейдинге: Диффузионная генерация торговых планов (Планировщик)

MetaTrader 5 — Торговые системы |
61 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Введение

Запланированный донабор позиции может стать невыполнимым ещё до исполнения. Сейчас свободной маржи достаточно, но движение цены против сделки уменьшит средства счёта. Прежний объём потребует пересмотра. Сокращение позиции на раннем шаге изменит условия последующих действий. Поэтому проверять нужно весь план и соответствующие состояния счёта.

Для решения этой задачи мы продолжаем торговую адаптацию фреймворка Diffusion Predictive Control with Constraints — DPCC. В авторском методе коррекция включена в обратный диффузионный процесс. После добавления шума предложение проверяется по динамике системы и ограничениям. Исправленная траектория поступает на следующий шаг генерации. В нашей адаптации генерируются торговые действия, а состояния счёта рассчитываются по ценовым сценариям.

В первой статье мы рассмотрели принципы DPCC и подготовили два OpenCL-кернела сценарного расчёта. Во второй разработали семь кернелов для поиска поправок и проверки их последствий. В третьей объединили эти операции в два MQL5-класса. CNeuronScenarioRolloutOCL рассчитывает финансовые последствия плана и выявляет нарушения. CNeuronPlanProjectionOCL использует полученную диагностику для коррекции последовательности действий. Таким образом, мы подготовили средства проверки и исправления уже предложенного плана.

Рассмотрим кадровый режим CNeuronPlanDenoiserOCL. Он обрабатывает нормированные действия с общими весами и сохраняет результаты шагов. Затем разберём действующий цикл CNeuronDPCCOCL. В нём используется прежний FeedForward. Кадровый интерфейс пока не подключён.

Покажем, как предложенный физический план после локальной проверки переводится обратно в нормированные координаты для следующего уточнения. Исходный счёт, сценарии и ограничения остаются общими для кандидатов. Сравним режимы без проектора, с его вызовом в конце и после каждого обратного шага. Итоговая проверка одинакова во всех трёх случаях.

Планы, прошедшие сценарный фильтр, сравним по спектральной оценке G. Это критерий фиксированной последовательности, а не оценка действия с продолжением политики через Q1/Q2. Здесь рассмотрим устройство режимов, а не результаты их торговых испытаний.

Из выбранного плана исполняется только первое действие. На новом баре планирование повторяется с фактического состояния счёта. Здесь разберём прямые вычисления. Подготовку данных, обучение и торговые испытания полной модели оставим следующему этапу.


Генерация последовательности торговых действий

Для обработки зашумлённых торговых планов предназначен CNeuronPlanDenoiserOCL. Класс наследуется от CNeuronUShapeAttention, но кадровый режим имеет собственную инициализацию и рабочие буферы. Один кадр обрабатывает пакет последовательностей и выдаёт прогноз шума той же формы, что и нормированные действия. Сам по себе этот интерфейс не выполняет финансовую проверку и пока не вызывается составным слоем на его рабочих обратных шагах.

В кадровом режиме условие генерации подключается через BindFrameCondition. Метод задаёт форму пакета с полями счёта, рыночными признаками Z/logU и весами Pi. В текущей версии отдельной ветви подключения только Z нет. После выделения памяти геометрия условия, допуск для весов сценариев и параметры нормирования остаются фиксированными.

Начнём с организации данных. Обучаемые преобразования, общий контекст и результаты отдельных обратных шагов хранятся раздельно. В объявлении класса для этого выделены следующие поля:

   CNeuronDPCCFrameAffineOCL *m_frame_affine[21];
   CBufferFloat              *m_frame_common[24];
   CBufferFloat              *m_frame_tapes[];
   CBufferFloat              *m_frame_backward[43];
   CBufferFloat              *m_frame_accumulated[21];
   CBufferFloat              *m_frame_work[5];

Массив m_frame_affine содержит 21 линейный блок с общими для кандидатов и кадров весами, m_frame_common хранит общие данные решения, а m_frame_tapes — результаты отдельных кадров. Буферы m_frame_backward и m_frame_accumulated нужны для обратного прохода и накопления градиентов. Новые копии сети для каждого шага не создаются.

Форму кадрового графа задаёт InitFrameShape. Метод разрешён только до создания прежнего графа и требует уже установленного контекста OpenCL. В начале проверяем размеры плана и совместимость параметров внимания:

   if(m_frame_mode || m_capacity != 0 || m_phase != 0 || m_configured || m_failed || !OpenCL ||
      horizon == 0 || rows == 0 || channels != 6 || heads == 0 || width == 0 ||
      width % heads != 0 || max_frames == 0 || max_frames > 64 ||
      (ulong)horizon * rows > (ulong)INT_MAX / 6 ||
      (ulong)horizon * rows > (ulong)UINT_MAX / sizeof(float) / MathMax(width, (uint)6))
      return(FrameFail(DPCC_CONTRACT_MISMATCH));
   const uint dimension = width / heads;
   if(dimension < 4 || dimension > 64 || (dimension & (dimension - 1)) != 0)
      return(FrameFail(DPCC_CONTRACT_MISMATCH));
   m_config.shape.KeyDimension = dimension;
   string device;
   if(!ResourceAdmission(OpenCL, device))
      return(FrameFail(DPCC_CONTRACT_MISMATCH));

horizon задаёт число действий, rows — вместимость пакета. Шесть каналов соответствуют объёму, TP и SL для покупки и продажи. Ширина width должна делиться на число голов внимания heads. Размер головы допускает 4, 8, 16, 32 или 64, число кадров max_frames — от 1 до 64. Это ограничения реализации, не оптимальные параметры торговли. Проверки произведений защищают от переполнения.

ResourceAdmission проверяет возможности устройства. После этого сохраняются размеры и устанавливается m_frame_mode. Обучаемые блоки будут созданы после задания формы рыночного условия.

Форму рыночного условия задаёт BindFrameCondition. Метод получает число сценариев, переменных, прогнозных шагов и латентную размерность. Отдельно передаются масштабы и смещения для 109 полей контекста. Это фиксированный состав текущего условного интерфейса, а не универсальная характеристика всех торговых моделей. Масштабы должны быть положительными и конечными, смещения — конечными. Горизонт прогноза не может быть короче торгового плана. При повторной привязке проверяются геометрия, допуск для весов и параметры нормирования.

При первом вызове сохраняем форму и рассчитываем размер входного пакета:

      m_frame_k = scenarios;
      m_frame_v = variables;
      m_frame_market_horizon = market_horizon;
      m_frame_dimension = latent_dimension;
      m_frame_condition_count = (uint)scalars + 109 + scenarios;
      m_frame_probability_tolerance = probability_tolerance;
      for(uint i = 0; i < 109; i++)
        {
         m_frame_profile[i] = bias[i];
         m_frame_profile[109 + i] = scale[i];
        }
      if(!FrameAllocate())
         return(false);
      m_frame_bound = true;
      if(!FrameOwned())
         return(FrameFail(DPCC_CONTRACT_MISMATCH, true));

В начале входного пакета расположены 109 полей контекста. За ними следуют рыночные токены Z/logU, затем — исходные веса сценариев Pi. Величина scalars предварительно рассчитывается как произведение числа сценариев, переменных, прогнозных шагов и ширины токена. Последняя равна латентной размерности плюс один компонент неопределённости. Смещения и масштабы сохраняются в двух последовательных половинах m_frame_profile. Это нормирование условия генерации, а не торговых действий.

После фиксации контекста FrameAllocate создаёт вычислительные блоки и буферы. Размеры сначала рассчитываются в ulong и проверяются относительно допустимых границ. Основной выход получает форму полного пакета планов. Для его создания вызывается базовая инициализация, а линейные преобразования создаются отдельно:

   if(!CNeuronBaseOCL::Init(0, 0, OpenCL, (uint)(lh * 6), ADAM, 1))
      return(FrameFail(DPCC_OPENCL_ERROR, true));
   activation = None;
   bTrain = false;
   for(uint i = 0; i < 21; i++)
     {
      m_frame_affine[i] = new CNeuronDPCCFrameAffineOCL;
      if(!m_frame_affine[i])
         return(FrameFail(DPCC_OPENCL_ERROR, true));
      if(!m_frame_affine[i].Init(0, i, OpenCL, m_frame_input[i], m_frame_input[i],
                               m_frame_output[i], m_frame_affine_rows[i], 1, ADAM, 1))
         return(FrameFail(DPCC_OPENCL_ERROR, true));
      m_frame_affine[i].SetActivationFunction(None);
      m_frame_affine[i].TrainMode(false);
     }

Здесь lh — произведение вместимости пакета и горизонта. Входная и выходная ширина каждого блока определены массивами m_frame_input и m_frame_output. Число обрабатываемых строк хранится в m_frame_affine_rows. Поэтому одна матрица применяется ко всем соответствующим элементам пакета. Число кандидатов изменяет объём промежуточных результатов, но не число наборов весов. Активации выполняются отдельными операциями в основном графе.

Для каждого кадра выделяется 43 буфера. Первые 26 хранят входы и результаты составных операций. Остальные 17 сохраняют выходы линейных блоков с индексами от 4 до 20. Доступ к конкретному буферу организован через FrameTape:

CBufferFloat *CNeuronPlanDenoiserOCL::FrameTape(const uint slot, const uint item) const
  {
   if(slot >= m_frame_max_frames || item >= 43 || ArraySize(m_frame_tapes) != (int)(43 * m_frame_max_frames))
      return(NULL);
   return(m_frame_tapes[slot * 43 + item]);
  }

Таким образом, индекс slot выбирает обратный шаг, а item — промежуточный результат внутри него. Данные предыдущего шага не заменяются выходом следующего. Общими остаются только параметры преобразований и специально выделенные рабочие области.

Один процесс генерации открывается методом BeginUnroll. Он проверяет готовность объекта и запрещает повторное начало при незавершённом расчёте. Существенна проверка числа кадров:

   if(!m_frame_mode || !m_frame_bound || m_failed || !FrameOwned() ||
      decision_id == 0 || frames != m_frame_max_frames || rows == 0 || rows > m_frame_rows ||
      m_frame_market_id == 0 || m_frame_market_weights == 0)
      return(FrameFail(DPCC_CONTRACT_MISMATCH));
   ArrayInitialize(m_frame_slots, 0);
   m_frame_decision = decision_id;
   m_frame_count = frames;
   m_frame_active_rows = rows;
   m_frame_next = 0;
   m_frame_current = 0;
   m_frame_backward_next = 0;
   m_frame_backward_count = 0;
   m_frame_training = need_backward;

В этой реализации значение frames должно совпадать с m_frame_max_frames. Уменьшить число обратных шагов при открытии расчёта нельзя. Число передаваемых строк может быть меньше вместимости пакета; оно хранится в m_frame_active_rows. Признак need_backward включает кадровый учебный режим независимо от обычного bTrain. В рассматриваемом прямом проходе он равен false.

Публичный ForwardFrame вызывает FrameForward и отмечает неудачный вызов как ошибку кадра. В начале метод FrameForward проверяет порядок шагов и формы внешних буферов:

   const uint active_count = m_frame_active_rows * m_frame_horizon * 6;
   if(!m_frame_mode || !m_frame_live || m_frame_update_started || m_frame_failed || m_failed || !FrameOwned() ||
      slot >= m_frame_count || slot != m_frame_next || j != m_frame_max_frames - slot ||
      m_frame_slots[slot] != 0 || !ValidBuffer(plan, active_count) ||
      !ValidBuffer(condition, m_frame_condition_count) || !ValidBuffer(epsilon_out, active_count))
      return(FrameFail(DPCC_CONTRACT_MISMATCH));

Первому кадру с индексом ноль соответствует диффузионный шаг J. Затем индекс кадра увеличивается, а j уменьшается. Повторная запись в использованный кадр отклоняется. Дополнительно проверяется отсутствие пересечений между входным планом, условием и выходом шума. Внешние буферы также не должны совпадать с внутренними рабочими областями денойзера.

При первом кадре условие генерации рассчитывается методом FrameCondition. На последующих шагах используется сохранённый результат, но только после проверки исходного пакета:

   if(m_frame_cache_valid)
     {
      if(!FrameSameCondition(condition))
         return(false);
     }
   else if(!FrameCondition(condition))
      return(false);

FrameSameCondition сверяет идентификаторы решения, состояния Market и его весов, а также контекст OpenCL. Затем DPCCDecoderSnapshotCheck сравнивает пакет с сохранённой копией. Поэтому одинакового размера или адреса буфера недостаточно.

Внутри FrameCondition кернел DPCCDenoiserFrameCondition разбирает входной пакет. Подготовленные поля контекста поступают в m_frame_common[0], рыночные токены — в m_frame_common[1], а веса сценариев — в m_frame_common[2]. Затем выполняются три линейных преобразования:

   if(!FrameAffine(0, m_frame_common[0]))
      return(false);
   if(!FrameAffine(1, m_frame_common[1]))
      return(false);
   if(!FrameAffine(2, m_frame_common[1]))
      return(false);

Блок с индексом ноль формирует запрос по состоянию счёта и техническому контексту. Следующие два блока создают ключи и значения рыночного внимания. После упаковки данных FrameAttention обрабатывает каждый сценарий отдельно. Число групп равно произведению числа сценариев и голов. Результаты объединяются с исходными весами Pi, проходят четвёртое линейное преобразование и сохраняются в m_frame_common[14]. Это готовое условие генерации для всех кандидатов и обратных шагов текущего решения.

После подготовки условия входной план сохраняется в первом буфере кадра. Если передан неполный пакет, остальная рабочая область заполняется нулями. Вместимость графа при этом не уменьшается. Эти нули — служебное дополнение, а не торговые команды и не маска допустимости; само заполнение не доказывает экономии вычислений.

Отдельно сформируем кодировку диффузионного шага:

   float encoding[];
   string reason;
   if(!DPCCBuildStepPositionalEncoding(j, m_frame_max_frames, encoding, reason))
      return(FrameFail(DPCC_CONTRACT_MISMATCH));
   CBufferFloat *step = FrameTape(slot, 1);
   for(uint i = 0; i < 16; i++)
      if(!step.Update((int)i, encoding[i]))
         return(FrameFail(DPCC_OPENCL_ERROR));
   if(!step.BufferWrite())
      return(FrameFail(DPCC_OPENCL_ERROR));

Полученные 16 компонентов принадлежат конкретному кадру. Они отличаются от позиционной кодировки будущих действий. Последняя рассчитывается при выделении памяти и хранится в m_frame_common[13]. Таким образом, положение действия на горизонте и номер уточнения плана представлены разными данными.

Начальный блок графа расширяет шесть компонентов действия до внутренней ширины. Кодировка шага преобразуется до той же размерности. Затем последовательно добавляем условие генерации, представление диффузионного шага и позиционную кодировку:

   if(!FrameAffine(4, saved_plan))
      return(false);
   if(!FrameAffine(5, step))
      return(false);
   const uint rows = m_frame_rows * m_frame_horizon;
   if(!FrameBroadcast(m_frame_common[14], m_frame_affine[4].getOutput(), FrameTape(slot, 2),
                      m_frame_width, rows))
      return(false);
   if(!FrameBroadcast(m_frame_affine[5].getOutput(), FrameTape(slot, 2), FrameTape(slot, 3),
                      m_frame_width, rows))
      return(false);
   if(!FrameBroadcast(m_frame_common[13], FrameTape(slot, 3), FrameTape(slot, 4),
                      m_frame_horizon * m_frame_width, m_frame_rows))
      return(false);

Условие и кодировка шага одинаковы для всех элементов пакета. Позиционная последовательность повторяется для каждого кандидата. Результат сохраняется в буфере кадра с индексом 4. Именно его получает первый блок внимания по торговому горизонту.

Внимание выполняется через FrameSelf. Метод создаёт запросы, ключи и значения тремя линейными блоками. Кернел DPCCDenoiserSelfLayout переводит их в раскладку для MHFlashAttention. Число независимых групп равно m_frame_rows × m_frame_heads. Поэтому действия одного плана взаимодействуют между собой, но не смешиваются с действиями другого кандидата. Временная маска отключена: уточнение охватывает всю предложенную последовательность.

После внимания выполним блок обработки признаков с остаточной связью:

   if(!FrameSelf(0, 4, 6, m_frame_horizon, 5, 10))
      return(false);
   if(!FrameAffine(10, FrameTape(slot, 10)))
      return(false);
   if(!FrameActivate(m_frame_affine[10].getOutput(), FrameTape(slot, 11), TANH))
      return(false);
   if(!FrameAffine(11, FrameTape(slot, 11)))
      return(false);
   if(!FrameSum(FrameTape(slot, 10), m_frame_affine[11].getOutput(), FrameTape(slot, 12)))
      return(false);

Линейный блок 10 расширяет признаки до удвоенной ширины. После TANH блок 11 возвращает исходную размерность. FrameSum складывает полученную поправку с выходом внимания. Результат в буфере 12 сохраним также для последующей пропускающей связи.

Далее перейдём к сокращённому горизонту. FramePair подготавливает соседние элементы к попарному объединению. При нечётной длине добавляется нулевое дополнение. Линейный блок 12 преобразует объединённые признаки обратно к внутренней ширине:

   if(!FramePair(FrameTape(slot, 12), FrameTape(slot, 13), 0))
      return(false);
   if(!FrameAffine(12, FrameTape(slot, 13)))
      return(false);
   if(!FrameSelf(1, 34, 13, (m_frame_horizon + 1) / 2, 14, 19))
      return(false);
   if(!FrameAffine(17, FrameTape(slot, 19)))
      return(false);
   if(!FrameActivate(m_frame_affine[17].getOutput(), FrameTape(slot, 20), TANH))
      return(false);
   if(!FrameAffine(18, FrameTape(slot, 20)))
      return(false);
   if(!FrameSum(FrameTape(slot, 19), m_frame_affine[18].getOutput(), FrameTape(slot, 21)))
      return(false);

Длина последовательности теперь равна округлённой вверх половине горизонта. На этом уровне работают собственные блоки внимания и обработки признаков. Их параметры хранятся отдельно от параметров первого уровня.

Число 34 в вызове FrameSelf — индекс сохранённого выхода линейного блока 12. Метод FrameAffine записывает такие результаты по правилу 26 + block − 4. Следующая операция читает данные своего кадра, а не изменяемый выход дочернего объекта.

Для восстановления горизонта блок 19 расширяет признаки каждого укрупнённого элемента. Обратный вызов FramePair возвращает исходную последовательность и удаляет техническое дополнение. Затем складываем восстановленные признаки с сохранённым результатом первого уровня:

   if(!FrameAffine(19, FrameTape(slot, 21)))
      return(false);
   if(!FramePair(m_frame_affine[19].getOutput(), FrameTape(slot, 22), 1))
      return(false);

   //--- D6 applies TANH after its saved skip sum and publishes epsilon only after admission.
   if(!FrameSum(FrameTape(slot, 22), FrameTape(slot, 12), FrameTape(slot, 23)))
      return(false);
   if(!FrameActivate(FrameTape(slot, 23), FrameTape(slot, 24), TANH))
      return(false);
   if(!FrameAffine(20, FrameTape(slot, 24)))
      return(false);

Последний линейный блок возвращает шесть компонентов для каждого действия. Ограничивающая выходная активация отсутствует. TANH применяется к скрытому представлению перед этим преобразованием, а не к предсказанному шуму. Поэтому выход нельзя трактовать как шесть уже допустимых торговых команд.

Завершим кадр сохранением результата. Полный пакет остаётся во внутренней памяти. Во внешний epsilon_out копируется только запрошенное число строк:

   if(!FrameCopy(m_frame_affine[20].getOutput(), FrameTape(slot, 25), m_frame_tape_size[25]))
      return(false);
   if(!FrameCopy(FrameTape(slot, 25), Output, m_frame_tape_size[25]))
      return(false);
   if(!FrameCopy(FrameTape(slot, 25), epsilon_out, active_count))
      return(false);
   m_frame_slots[slot] = 1;
   m_frame_next++;
   m_public_valid = true;
   m_status = DPCC_OK;
   return(true);

Только после успешного копирования кадр получает признак завершённого прямого прохода. Счётчик m_frame_next разрешает обработку следующего шага. Статус DPCC_OK здесь подтверждает выполнение денойзера. Он не означает допустимости торгового плана. Проверка ограничений ещё не выполнялась.

После всех кадров завершим расчёт через ReleaseUnroll. Метод проверяет полноту последовательности и состояние каждого кадра. Для прямого режима ожидается признак 1 во всех использованных слотах. Затем сбрасываются идентификаторы текущего решения и признаки действительности кэша. Для прерывания до записи обновлённых параметров предусмотрен отдельный AbortUnroll. Это позволяет отличать нормальное завершение от отмены незаконченного расчёта.

BackwardFrame вызывается для кадров в обратном порядке и передаёт расчёт в FrameBackward. Накопление и применение градиентов организуют ZeroAccumulatedGradients и ApplyAccumulatedGradients.


Планирование с коррекцией на обратных шагах

Прогноза шума недостаточно для торгового плана. Нужно уточнить действия, рассчитать их финансовые последствия и при необходимости применить локальную поправку. Этим управляет CNeuronDPCCOCL, но пока через прежний интерфейс денойзера. Разберём именно действующий маршрут.

Начнём с создания внутренних объектов. В рассматриваемой реализации за это отвечает InitializeChildren:

   m_decoder = new CNeuronScenarioDecoderOCL(); m_denoiser = new CNeuronPlanDenoiserOCL();
   m_rollout = new CNeuronScenarioRolloutOCL(); m_projector = new CNeuronPlanProjectionOCL();
   m_normalizer = new CNeuronBatchNormOCL(); m_denorm = new CNeuronRevINDenormOCL();
   if(!m_decoder || !m_denoiser || !m_rollout || !m_projector || !m_normalizer || !m_denorm)
      return(Fail(DPCC_OPENCL_ERROR, true));

Здесь предусмотрены отдельные экземпляры декодера, денойзера, сценарного расчётчика и базового проектора. Средства нормирования нужны для восстановления ценовых величин. Все дочерние объекты работают в одном контексте OpenCL. Денойзер и проектор обрабатывают пакет планов. Расчётчик формирует денежные результаты для пар план–сценарий.

Проектор связывается с тем же расчётчиком, который позднее проверяет окончательные планы. Это связывание выполняется при конфигурации:

   SDPCCConfig denoiser_config = config; denoiser_config.shape.Depth = 1;
   if(!m_decoder.Configure(config,decoder) || !m_denoiser.Configure(denoiser_config,model,training) ||
      !m_rollout.Configure(config) || !m_projector.Configure(config) || !m_projector.BindRollout(m_rollout))
      return(Fail(DPCC_CONTRACT_MISMATCH,true));

Поиск поправок и итоговая проверка используют один алгоритм финансового перехода. Но сопоставимы лишь расчёты с одинаковыми снимком счёта, сценариями, календарём, лимитами и условиями исполнения.

Обозначим число планов через M, горизонт через H, число сценариев через K. Для промежуточных данных выделим отдельные рабочие области. Полный пакет содержит M × H × 6 значений. Его текущую версию хранит выход m_plan_source. Следующее предложение помещается в буфер DPCC_CW_NEXT_PLAN. Выбранная последовательность занимает H × 6 элементов в DPCC_CW_SELECTED. Основной выход верхнего слоя содержит только шесть компонентов первого действия.

Денежные результаты имеют форму M × K, статусы и признаки допуска — по M элементов. G — дисконтированная сумма приращений средств в валюте счёта. При γ = 1 она равна изменению средств за горизонт, включая переоценку открытой позиции. При меньшем γ поздние приращения имеют меньший вес. Положительная оценка не заменяет проверку ограничений.

Метод Geometry рассчитывает размеры рабочих областей и проверяет соответствие пакета числу кандидатов. После этого AllocateOwned создаёт буферы и объект, предоставляющий текущий план дочерним слоям:

   m_decoder_source = new CNeuronBaseOCL(); m_plan_source = new CNeuronBaseOCL();
   if(!m_decoder_source || !m_plan_source ||
      !m_decoder_source.Init(0, 0, OpenCL, (uint)m_c_layout.lengths[0], ADAM, 1) ||
      !m_plan_source.Init(0, 0, OpenCL, m_shape.M * m_shape.H * 6, ADAM, 1))
      return(Fail(DPCC_OPENCL_ERROR, true));
   m_decoder_source.SetActivationFunction(None); m_plan_source.SetActivationFunction(None);
   m_decoder_source.TrainMode(false); m_plan_source.TrainMode(false);

m_plan_source передаёт данные дочерним слоям, а не генерирует действия. На разных стадиях в нём находятся нормированный или физический план. Цель позиции определяется разностью BuyVolume − SellVolume; нулевая цель задаёт закрытие, если действие допущено к исполнению. Разворот включает закрытие и открытие, а TP/SL задаются относительными расстояниями по финансовому контракту.

При первичной сборке нужно настроить профиль и перенести подготовленные параметры через ImportPreparedComponents. Метод проверяет доноров, копирует параметры и устанавливает обязательный для прямого прохода m_imported. Он использует прежнюю конфигурацию денойзера.

В начале метод feedForward закрывает доступ к прежнему результату и проверяет состояние объекта:

   m_public_valid=false;
   if(m_failed || m_busy || m_transfer_pending || !m_configured || !m_imported || !ValidateOwned())
      return(Fail(DPCC_CONTRACT_MISMATCH));
   m_busy=true; m_status=DPCC_OK;
   bool success=PrepareDecision(source,context) && InitializeNoise();

Признак m_busy исключает повторный вход во время незавершённого расчёта, m_transfer_pending не позволяет использовать объект в процессе переноса между контекстами OpenCL. Метод ValidateOwned проверяет внутренние объекты и принадлежащие им буферы. После этих проверок подготавливаем снимок решения и начальный шум.

PrepareDecision получает основной вход с 13 признаками состояния счёта и дополнительный контекст. В этой версии верхнего слоя дополнительный пакет содержит латентный прогноз, параметры восстановления цен, финансовые данные и заранее подготовленные шумы. Для сценарного расчётчика из него формируется отдельный финансовый пакет.

Входные данные копируются в собственные буферы до начала генерации:

   m_snapshot_staged=false; m_snapshot_device_hash=0;
   if(!SnapshotCompositeContext(inputs[0],m_work[DPCC_CW_ACCOUNT13],m_work[DPCC_CW_FLAGS],
         m_work[DPCC_CW_SCALAR],13) ||
      !SnapshotCompositeContext(context,m_work[DPCC_CW_CONTEXT],m_work[DPCC_CW_FLAGS],
         m_work[DPCC_CW_SCALAR],(uint)m_c_layout.total) || !BindSnapshotTimes()) return(false);

После копирования очищаем промежуточные данные, декодируем сценарии и восстанавливаем цены. PackCompositeR собирает финансовый пакет. PackCompositeCondition формирует условие денойзера длиной m_tokens + 61, отличное от кадрового пакета BindFrameCondition. Подготовка выполняется один раз перед обратными шагами. Это ещё одно различие двух показанных интерфейсов.

Все кандидаты начинают проверку с одного снимка счёта. Рассчитанные состояния отдельного плана не становятся исходными данными следующего.

После подготовки контекста инициализируем планы шумом. InitializeNoise вызывает InitializeCompositeNoise и обнуляет накопительную диагностику:

bool CNeuronDPCCOCL::InitializeNoise(void)
  {
   // Canonical preloaded C.Noise[m,J] only. Host indexed-noise helper owns
   // SHA/key generation; there is no second device random stream or readback.
   return(InitializeCompositeNoise(m_plan_source.getOutput()) && UpdateProjectionSummary(0));
  }

Шум поступает из входного пакета; второго случайного потока слой не создаёт. DPCCCompositeInitialNoise читает участок с индексом J для каждого кандидата. Все исходные строки получают активность, но итоговая допустимость пока равна нулю.

Обработку одного обратного шага организует ReverseStep:

bool CNeuronDPCCOCL::ReverseStep(const uint j)
  {
   if(!SetCompositeStep(j,m_shape.J,m_work[DPCC_CW_CONDITION],m_tokens)) return(false);
   if(!m_denoiser.SetContextIdentity(m_snapshot.decision_key,m_decision_serial,
         (ulong)m_work[DPCC_CW_CONTEXT].GetIndex()+1))
      return(Fail(m_denoiser.LastStatus()));
   // Exactly one real shared, frozen, L=M denoiser invocation for this j.
   if(!m_denoiser.FeedForward(m_plan_source,m_work[DPCC_CW_CONDITION]))
      return(Fail(m_denoiser.LastStatus()==DPCC_OK?DPCC_OPENCL_ERROR:m_denoiser.LastStatus()));
   return(CompositeReverseArithmetic(m_plan_source.getOutput(),m_denoiser.getOutput(),m_work[DPCC_CW_NEXT_PLAN],j));
  }

Обновляется кодировка шага, затем денойзеру передаётся идентичность контекста. FeedForward вызывается для полного пакета фиксированной вместимости M. Даже после деактивации отдельных кандидатов этот вызов сам по себе не сокращает размер пакета денойзера. Предсказанный шум и текущий план поступают в CompositeReverseArithmetic; новое предложение записывается отдельно от входа.

Здесь используется прежний FeedForward. Методы BeginUnroll и ForwardFrame к этому маршруту не подключены.

Арифметика обратного шага выполняется кернелом DPCCCompositeReverse. Для активной строки сначала рассчитывается среднее следующего предложения:

         const float root_variance = sqrt(1.0f - alpha_bar), root_alpha = sqrt(1.0f - beta);
         const float product = beta * eps, correction = product / root_variance;
         const float numerator = current - correction, mu = numerator / root_alpha;

Здесь current — текущая нормированная координата действия, а eps — предсказанный шум. Коэффициенты поступают из сохранённых массивов m_beta, m_alpha_bar и m_sigma. До запуска проверяются номер шага, размеры массивов и допустимые значения коэффициентов. Сам кернел дополнительно контролирует конечность промежуточных результатов.

При j > 1 к среднему добавляется случайная составляющая из участка шума с индексом j − 1 для текущего кандидата. На последнем шаге sigma должна быть нулевой, а участок шума с индексом ноль не читается. ScheduleValid проверяет это условие вместе с согласованностью накопленного произведения коэффициентов и дисперсии обратного перехода. Поэтому массивы расписания нельзя передать как три независимые последовательности.

Следующий этап зависит от выбранного режима. Для коррекции нужно перейти из нормированных координат в торговые единицы. Эту операцию выполняет CompositeActionAffine. Направление ноль соответствует преобразованию A = b + s·u. Направление один возвращает нормированные значения по формуле u = (A − b)/s.

Шесть смещений и масштабов берутся из финансового профиля:

   float reals[12];
   for(uint channel = 0; channel < 6; channel++)
     {
      reals[channel] = m_config.action_bias[channel];
      reals[channel + 6] = m_config.action_scale[channel];
      if(!MathIsValidNumber(reals[channel]) || !MathIsValidNumber(reals[channel + 6]) ||
         reals[channel + 6] <= 0)
         return(Fail(DPCC_INVALID_INPUT));
     }

Преобразования действий используют неизменные масштабы и не меняют лимиты. Денойзер не ограничивает амплитуду выходного шума. Проверка конечности отсекает NaN и бесконечность, но большие конечные значения требуют проверки торговых границ и сценарного расчёта.

Теперь рассмотрим центральный цикл feedForward:

   const uint count=m_shape.M*m_shape.H*6;
   for(uint j=m_shape.J;success && j>0;j--)
     {
      success=ReverseStep(j);
      if(success && m_config.mode==DPCC_P2)
         success=CompositeActionAffine(m_work[DPCC_CW_NEXT_PLAN],m_plan_source.getOutput(),0) &&
                 ProjectCurrent() && MergeProjectionMask();
      if(success) success=CopyBufferRaw(m_work[DPCC_CW_NEXT_PLAN],m_plan_source.getOutput(),count);
     }

После ReverseStep в DPCC_CW_NEXT_PLAN находится нормированное предложение. В DPCC_P2 оно переводится в торговые единицы и передаётся проектору. Результат снова нормируется, а маска исключает кандидатов, не прошедших коррекцию.

Последнее копирование передаёт пакет денойзеру для следующего шага. Принятые поправки влияют на дальнейшую генерацию только сохранившихся кандидатов.

Вызов подготовленного проектора сосредоточен в ProjectCurrent:

bool CNeuronDPCCOCL::ProjectCurrent(void)
  {
   // Current source is physical MH6 on entry, borrowed by the real projector.
   if(!m_projector.FeedForward(m_plan_source,m_work[DPCC_CW_ROLLOUT_CONTEXT]))
      return(Fail(m_projector.LastStatus()==DPCC_OK?DPCC_OPENCL_ERROR:m_projector.LastStatus()));
   return(UpdateProjectionSummary(1));
  }

Здесь не создаётся новый механизм поиска. Используется CNeuronPlanProjectionOCL, разобранный ранее. Он получает физический пакет действий и общий финансовый контекст. Сценарные расчёты выполняются через связанный m_rollout. После завершения вызова верхний слой обновляет накопительную диагностику.

Возврат результата организован отдельным методом:

bool CNeuronDPCCOCL::MergeProjectionMask(void)
  {
   if(!CompositeActionAffine(m_projector.getOutput(),m_work[DPCC_CW_NEXT_PLAN],1)) return(false);
   return(MergeCompositeProjectionMask(m_work[DPCC_CW_NEXT_PLAN]));
  }

Сначала выход проектора возвращается в нормированные координаты. Затем MergeCompositeProjectionMask согласует статусы с маской активности. Найденная поправка и исключение кандидата — разные результаты.

Статус 0 допускает кандидата к продолжению. Код 2 означает неуспешный поиск либо служебное состояние неактивного слота. Код 1 также принимается композитным фильтром как отказ, хотя в показанной штатной публикации проектора приведены прежде всего 0 и 2. Подробную причину определяют по диагностике. При статусах 1 или 2 активная строка исключается:

      if(active == 1.0f && projected != 0.0f)
        {
         rollout_context[active_offset + m] = 0.0f;
         candidate_status[m] = projected;
        }

Индекс и причина исключения сохраняются. Рабочие нули — служебное заполнение, а не команда закрытия позиции: активность строки уже равна нулю.

Исключение строки не сдвигает шумы остальных кандидатов. Ошибки входных данных или арифметики прекращают общий расчёт. Их нельзя подменять плохой оценкой отдельного плана.

UpdateProjectionSummary формирует по 12 полей на план. Поля 0–1 накапливают число проверок и итераций, 2–9 — последнюю диагностику проектора, 10–11 — итоговые признаки активности и допуска. Это сводка, а не история всех предложений. В DPCC_P2 бюджет каждого вызова проектора отделён от числа обратных шагов J.

В режиме DPCC_P1 цикл проходит без промежуточных поправок. Проектор включается только после его завершения:

   if(success && m_config.mode==DPCC_P1)
      success=CompositeActionAffine(m_plan_source.getOutput(),m_work[DPCC_CW_NEXT_PLAN],0) &&
              CopyBufferRaw(m_work[DPCC_CW_NEXT_PLAN],m_plan_source.getOutput(),count) &&
              ProjectCurrent() && MergeProjectionMask() &&
              CopyBufferRaw(m_work[DPCC_CW_NEXT_PLAN],m_plan_source.getOutput(),count);

В DPCC_P0 проектор не вызывается. DPCC_P1 вызывает его один раз, а DPCC_P2 — J раз на уровне пакета при полном проходе. Но отдельный кандидат может выбыть раньше: число вызовов не равно числу его успешных поправок. Системная ошибка прерывает цикл.

SetProjectionMode запрещён во время расчёта и аннулирует прежний снимок. Веса и алгоритм проектора не меняются. Следующий проход требует нового согласованного снимка.

После генерации выполняется общий для всех режимов FinalRollout. Он снова переводит окончательный нормированный пакет в торговые единицы и проверяет именно полученный массив:

bool CNeuronDPCCOCL::FinalRollout(void)
  {
   const uint count=m_shape.M*m_shape.H*6;
   if(!CompositeActionAffine(m_plan_source.getOutput(),m_work[DPCC_CW_NEXT_PLAN],0) ||
      !CopyBufferRaw(m_work[DPCC_CW_NEXT_PLAN],m_plan_source.getOutput(),count))
      return(Fail(m_status==DPCC_OK?DPCC_OPENCL_ERROR:m_status));
   if(!m_rollout.FeedForward(m_plan_source,m_work[DPCC_CW_ROLLOUT_CONTEXT]))
      return(Fail(m_rollout.LastStatus()==DPCC_OK?DPCC_OPENCL_ERROR:m_rollout.LastStatus()));
   // Preserve actual financial returns before native FQF reads owner buffers.
   if(!CopyBufferRaw(m_rollout.getOutput(),m_work[DPCC_CW_RETURNS],m_shape.M*m_shape.K) ||
      !SnapshotCompositeContext(m_rollout.GetValidity(),m_work[DPCC_CW_VALIDITY],m_work[DPCC_CW_FLAGS],
                                m_work[DPCC_CW_SCALAR],m_shape.M))
      return(Fail(m_status==DPCC_OK?DPCC_OPENCL_ERROR:m_status));
   return(true);
  }

На этом этапе поправки уже не ищутся. Метод сохраняет денежные результаты и признаки допустимости. После его завершения m_plan_source содержит физические планы. Именно из этого массива позднее выбирается последовательность.

После обратного преобразования локальная поправка участвует в следующем шаге генерации. Итоговый физический пакет затем проверяется заново через FinalRollout. Прежняя допустимость не переносится автоматически, а промежуточная коррекция не даёт гарантии итогового результата.

PadAndValidateDistributions допускает кандидата к ранжированию только при трёх условиях: активность равна 1, статус проектора — 0, итоговая сценарная допустимость — 1. Успех вычислений этих условий не заменяет. Кернел DPCCCompositePad проверяет их совместно:

   const float a=R[activeOffset+m],s=status[m],v=endvalid[m];
   if(!isfinite(a)||(a!=0&&a!=1)||!isfinite(s)||(s!=0&&s!=1&&s!=2)||
      !isfinite(v)||(v!=0&&v!=1)) error=3;
   const uint valid=(a==1&&s==0&&v==1);

Веса сценариев проверяются на конечность, неотрицательность и положительную суммарную массу. Для спектральных кернелов распределение дополняется до длины — степени двойки; новые элементы имеют нулевую массу. Нейтральность такого дополнения — требование к FQF_MonotoneRearrange и FQF_SRMForward, а не следствие самого заполнения нулями. Её нужно подтверждать сравнением оценки до и после дополнения; результаты такого испытания здесь не приводятся.

RankExternalDistributions использует пары денежных результатов G и масс сценариев. Параметр risk_type выбирает MEAN, CVAR, MEAN_CVAR, EXPONENTIAL или DUAL_POWER. risk_parameter задаёт долю нижнего хвоста для CVAR/MEAN_CVAR (0 < α ≤ 1), параметр EXPONENTIAL (λ ≥ 0) или DUAL_POWER (ν ≥ 1). mean_weight определяет долю среднего результата в MEAN_CVAR от 0 до 1. Разные настройки могут изменить выбор плана. Сортировку и оценку выполняют FQF_MonotoneRearrange и FQF_SRMForward без обучаемой FQF-головы:

   if(!OpenCL.Execute(def_k_FQF_MonotoneRearrange,2,offset,global,local)||
      !OpenCL.SetArgumentBuffer(def_k_FQF_SRMForward,0,m_work[DPCC_CW_SORTED_SUPPORT].GetIndex())||
      !OpenCL.SetArgumentBuffer(def_k_FQF_SRMForward,1,m_work[DPCC_CW_SORTED_MASS].GetIndex())||
      !OpenCL.SetArgumentBuffer(def_k_FQF_SRMForward,2,m_work[DPCC_CW_SCORES].GetIndex())||
      !OpenCL.SetArgument(def_k_FQF_SRMForward,3,m_config.risk_type)||
      !OpenCL.SetArgument(def_k_FQF_SRMForward,4,m_config.risk_parameter)||
      !OpenCL.SetArgument(def_k_FQF_SRMForward,5,m_config.mean_weight)||
      !OpenCL.Execute(def_k_FQF_SRMForward,2,offset,global,local))
      return(Fail(DPCC_OPENCL_ERROR));

Спектральная оценка относится к фиксированному плану на заданном горизонте. Она учитывает массы сценариев, но не доказывает их соответствие истинным вероятностям будущего рынка. Q1/Q2 оценивали бы первое действие с продолжением политики. Это другой критерий даже при одинаковой мере риска.

PublishDecision передаёт кернелу физические планы, маску, оценки и диагностику. Выбирается максимальная оценка среди допущенных строк. При точном равенстве сохраняется меньший индекс. Числовая проверка завершается до копирования результата.

После выбора полная последовательность сохраняется отдельно. В основной выход копируются только первые шесть компонентов:

   if(error||winner<0)
      return;
   for(uint i=0;i<H*6;i++)
      selected[i]=plans[(uint)winner*H*6+i];
   // DATA03 [BuyVolume,BuyTP,BuySL,SellVolume,SellTP,SellSL], first h=0.
   for(uint i=0;i<6;i++)
      output[i]=selected[i];

Если допустимого кандидата нет, кернел сообщает отказ, а не публикует торговую команду. Прикладная программа должна определить действия с открытой позицией отдельно; служебные нули исполнять нельзя. Для передачи статуса MQL5 считывает компактную запись:

   float receipt[4];
   if(!OpenCL.BufferRead(m_work[DPCC_CW_FLAGS].GetIndex(),receipt,0,0,4))
      return(Fail(DPCC_OPENCL_ERROR));
   if(!MathIsValidNumber(receipt[0])||(receipt[0]!=DPCC_OK&&receipt[0]!=DPCC_NO_FEASIBLE_CANDIDATE))
      return(Fail(DPCC_NUMERIC_ERROR));
   m_status=(ENUM_DPCC_STATUS)(int)receipt[0];
   // Computed business refusal exposes diagnostics only, never a selected plan.
   m_public_valid=true;
   return(true);

Возврат значения true при DPCC_NO_FEASIBLE_CANDIDATE означает завершение вычислений с отказом. Флаг m_public_valid делает актуальными статус и диагностику, но не торговую команду. Исполнение требует HasValidDecision, который по контракту истинен только при DPCC_OK. Служебный ноль не означает разрешённое закрытие.

В конце feedForward все завершающие операции соединяются в одну последовательность:

   if(success) success=FinalRollout() && PadAndValidateDistributions() && UpdateProjectionSummary(2) &&
                       RankExternalDistributions() && PublishDecision();
   m_busy=false;
   if(!success) {m_snapshot_valid=false;return(Fail(m_status==DPCC_OK?DPCC_OPENCL_ERROR:m_status));}
   // CandidatePlans remains physical MH6; only exact OK yields a chosen plan.
   return(true);

После вычислительной ошибки снимок и публичный результат теряют действительность. Рабочие буферы не откатываются, повторный проход требует нового согласованного снимка. При необратимой ошибке объект пересоздаётся. Штатный отказ без допустимого кандидата тоже не разрешает использовать старое действие.

В DPCC_P2 принятые поправки возвращаются в нормированные координаты перед следующим уточнением. Отклонённые кандидаты не участвуют в отборе. Итоговая проверка остаётся отдельной стадией.


Заключение

Мы рассмотрели кадровый денойзер с сохранением результатов обратных шагов и действующий цикл CNeuronDPCCOCL. Пока это разные вычислительные пути. Их соединение потребует согласовать формат условий и проверить результаты.

В P2 проектор вызывается после каждого обратного шага с учётом уже добавленного шума; отдельные кандидаты могут выбывать раньше. В P1 вызов выполняется в конце, в P0 отсутствует. Независимо от режима обязательна новая итоговая сценарная проверка. Вызов проектора ещё не означает, что план удалось исправить.

Мы отделили завершение вычислений от наличия торгового решения. Статус отказа нельзя подменять нулевой торговой командой. Локальный поиск не гарантирует ближайший допустимый план, а сценарная проверка — соблюдение лимитов при любом будущем движении рынка. Сравнение эффективности P0/P1/P2, чувствительности к прогнозу и устойчивости расчётов требует отдельных испытаний.

Верхний слой ранжирует результаты G по выбранной спектральной мере. Подключение кадрового денойзера и другие способы оценки остаются дальнейшими задачами адаптации DPCC. Обучение и торговые испытания здесь не рассматриваются.


Ссылки


Файлы проекта

# Имя Тип Описание
1 Trajectory.mqh Общий модуль Общие параметры, описание моделей, подготовка данных и диагностика
2 StudyForecast.mq5 Советник Подготовка и проверка сценарного декодера с восстановлением масштаба цен
3 Study.mq5 Советник Обучение денойзера и сборка комплекта DPCC
4 StudyOnline.mq5 Советник Последовательный сбор и проверка траекторий в тестере
5 Test.mq5 Советник Проверка и исполнение модели DPCC с фиксированными весами
6 NeuroNet.mqh Библиотека классов Библиотека классов нейронных сетей
7 NeuroNet_Attention.mqh Модуль библиотеки Кадровый режим денойзера CNeuronPlanDenoiserOCL
8 NeuroNet_AgentsRL.mqh Модуль библиотеки Сценарный расчётчик и проектор торговых планов
9 NeuroNet.cl OpenCL-программа OpenCL-кернелы библиотеки нейронных сетей

Исходные файлы доступны в репозитории проекта.

Прикрепленные файлы |
MQL5.zip (7654.59 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV Фильтрация новостей с помощью экономического календаря MetaTrader 5 и резервного варианта на основе CSV
В статье представлен автономный модуль фильтрации новостей для MetaTrader 5, созданный на основе API экономического календаря платформы. Модуль реализует сопоставление торговых символов с валютами, торговые паузы перед событиями и после них, а также необязательное сокращение объёма позиции в дни с событиями высокой значимости, с резервным вариантом на основе CSV для Тестера стратегий. Демонстрационный советник и панель мониторинга на графике в реальном времени показывают интеграцию и проверку как в реальной торговой среде, так и в условиях бэктеста.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Альтернатива среднему арифметическому и стандартному отклонению: Библиотека робастной статистики для индикаторов MQL5 Альтернатива среднему арифметическому и стандартному отклонению: Библиотека робастной статистики для индикаторов MQL5
Ценовые выбросы искажают показания индикаторов, основанных на среднем арифметическом и стандартном отклонении. В статье представлена библиотека робастной статистики на языке MQL5 (RobustStats.mqh), реализующая вычисление медианы, медианное абсолютное отклонение (MAD) с коэффициентом масштабирования 1,4826 и наклона Тейла — Сена, а также представлены три индикатора для непосредственной замены полос Боллинджера, канала линейной регрессии и осциллятора z-оценки. Совместное отображение индикаторов для сравнения и измерение точки разрушения на EURUSD показывают, как робастные инструменты сохраняют форму, когда одиночный скачок цены сдвигает классические.