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

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

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

Введение

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

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

В предыдущей статье мы подготовили основу этой проверки. Кернел DPCCRollout рассчитывает изменения средств, маржи и позиции, а DPCCRolloutReduce объединяет проверки по сценариям. Полученная диагностика показывает шаг и фазу нарушения. Теперь от обнаружения проблемы перейдём к поиску поправки. В примере с донабором попробуем уменьшить ранее заданный объём и снова рассчитать последствия всего плана.

В практической части рассмотрим семь новых OpenCL-кернелов. Они сохраняют исходный план, готовят варианты торговых действий, проверяют их последствия и публикуют диагностику. Полный порядок вызовов должна обеспечить MQL5-обвязка. Слово Projection в названиях примитивов сохраняет программный контракт. Однако рассмотренный алгоритм — ограниченный локальный поиск поправки. Он не вычисляет ближайшую допустимую траекторию состояний и действий в смысле исходного DPCC. Поиск может закончиться с нарушениями, поэтому результат необходимо читать вместе со статусами.


Алгоритмы OpenCL-примитивов

Будем обрабатывать пакет из M планов длиной H будущих действий. Каждое действие содержит шесть компонентов: объём, параметры TP и SL покупки, затем те же величины для продажи. Разность объёмов задаёт целевую нетто-позицию. Коррекция сохранит форму пакета и горизонт H независимо от числа попыток поиска.

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

На всех этапах используем тот же исходный счёт, сценарии и условия исполнения. Корректность расчёта зависит и от размеров буферов, масок активности и порядка зависимых запусков. Схема показывает связи примитивов. Подготовку данных и полный порядок запусков должна обеспечивать MQL5-обвязка. Её устройство оставим за рамками этой статьи.

Кернел DPCCProjectionInit

Для сравнения поправок прежде всего нужен неизменный исходный план. Поэтому DPCCProjectionInit читает последовательности из plans и создаёт две копии: original для сравнения и current для изменений. Из context он получает маску активности. Стадия, счётчики и оценки каждого плана хранятся в отдельной записи controls. Исходное предложение останется точкой отсчёта даже после нескольких принятых поправок.

Это позволяет обрабатывать планы независимо. Записи управления отведём 24 элемента, сводной оценке и итоговой диагностике — по 12. Для одной попытки зарезервируем девять слотов — отдельных мест под варианты плана. Размеры записей и индексы полей закрепим константами для последующего обмена данными.

#define DPCC_PC_STRIDE       24
#define DPCC_PS_STRIDE       12
#define DPCC_PD_STRIDE       12
#define DPCC_PROPOSALS        9
#define DPCC_PC_ACTIVE        0
#define DPCC_PC_STAGE         1
#define DPCC_PC_EVAL          2
#define DPCC_PC_ITER          3
#define DPCC_PC_COUNT         4
#define DPCC_PC_CURSOR        5
#define DPCC_PC_H             6
#define DPCC_PC_PHASE         7
#define DPCC_PC_K             8
#define DPCC_PC_CONSTRAINT    9
#define DPCC_PC_SOURCE       10
#define DPCC_PC_FEASIBLE     11
#define DPCC_PC_VMAX         12
#define DPCC_PC_VSUM         13
#define DPCC_PC_DISTANCE     14
#define DPCC_PC_IDENTITY     15
#define DPCC_PC_LAST_P       16
#define DPCC_PC_STATUS       17
#define DPCC_PC_SYSTEM       18
#define DPCC_PC_PROPOSED     19
#define DPCC_PC_VERIFIED     20
#define DPCC_PC_ORIGINAL_OK  21

Один рабочий элемент OpenCL обслуживает один план. После проверки индекса m обнулим запись управления. Код 2 задаётся по умолчанию: сценарная допустимость ещё не подтверждена. Он не означает, что поиск уже исчерпан. Причину результата позднее уточнят активность, стадия, признак выполненной проверки и системный статус. Координатам нарушения и номеру последней поправки присвоим −1. Эти небольшие целые значения точно представимы в float.

__kernel void DPCCProjectionInit(__global const float *plans,
                                  __global const float *context,
                                  __global float *original,
                                  __global float *current,
                                  __global float *base_active,
                                  __global float *final_active,
                                  __global float *controls,
                                  uint M, uint H, uint off_R_active, uint plan_offset)
  {
   const uint m = (uint)get_global_id(0);
   if(m >= M)
      return;
   __global float *control = controls + (size_t)m * DPCC_PC_STRIDE;
   for(int i = 0; i < DPCC_PC_STRIDE; i++)
      control[i] = 0.0f;
   control[DPCC_PC_STATUS] = 2.0f;
   control[DPCC_PC_LAST_P] = -1.0f;
   for(int i = DPCC_PC_H; i <= DPCC_PC_SOURCE; i++)
      control[i] = -1.0f;
   const float active = context[(size_t)off_R_active + m];
   if(!DPCCBoolean(active))
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
   else
     {
      control[DPCC_PC_ACTIVE] = active;
      control[DPCC_PC_STAGE] = active == 1.0f ? 1.0f : 0.0f;
     }

Участие плана определим по маске со смещением off_R_active в context. Функция DPCCBoolean принимает 0 и 1: корректный ноль оставляет план неактивным на стадии 0, единица переводит на стадию 1. Другое значение даёт DPCC_STATUS_INVALID и исключает вход из сценарной оценки.

Обе копии сохраним для каждого плана, в том числе неактивного, через DPCCProjectionCopy. В показанной реализации функция переносит 32-битные слова через указатели на uint, без арифметического преобразования чисел. Способ выбран для сохранения исходного представления, включая знак нуля. Это потребуется при возврате допустимого плана без изменений.

void DPCCProjectionCopy(__global const float *source, __global float *destination,
                         size_t source_offset, size_t destination_offset, size_t count)
  {
   __global const uint *source_bits = (__global const uint *)source;
   __global uint *destination_bits = (__global uint *)destination;
   for(size_t i = 0; i < count; i++)
      destination_bits[destination_offset + i] = source_bits[source_offset + i];
  }

Начало копируемого плана находится по адресу m × H × 6, длина равна H × 6 элементов. Только вход plans дополнительно смещён на plan_offset. В original и current первый план начинается с нуля. Смещения выражены в элементах буфера, без перевода в байты.

   const size_t row = (size_t)m * H * 6;
   DPCCProjectionCopy(plans, original, (size_t)plan_offset + row, row, (size_t)H * 6);
   DPCCProjectionCopy(plans, current, (size_t)plan_offset + row, row, (size_t)H * 6);
   for(uint h = 0; h < H; h++)
     {
      for(int a = 0; a < 6; a++)
         if(!isfinite(original[row + (size_t)h * 6 + a]))
            control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
      if(control[DPCC_PC_SYSTEM] == 0.0f &&
         !isfinite(original[row + (size_t)h * 6] - original[row + (size_t)h * 6 + 3]))
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
     }
   base_active[m] = control[DPCC_PC_SYSTEM] == 0.0f ? control[DPCC_PC_ACTIVE] : 0.0f;
   final_active[m] = 0.0f;
  }

Перед финансовым расчётом проверим конечность компонентов. Некорректный вход получит DPCC_STATUS_INVALID, переполнение разности конечных объёмов — DPCC_STATUS_NUMERIC. Проверка охватывает и неактивные планы. По её результату base_active включит только активные записи без системной ошибки. Маску final_active пока обнулим: сейчас нужна первичная оценка, а заключительная понадобится позднее. Следующий этап определяется полем DPCC_PC_STAGE.

Кернел DPCCProjectionSummary

После сценарного расчёта каждому плану соответствует несколько историй счёта. В одной донабор проходит без нарушения, в другой приводит к нехватке свободной маржи. Кернел DPCCProjectionSummary формирует сводку: признак сценарной допустимости, два показателя нарушений и координаты первого нарушения. Отклонение от original дополнит сводку величиной коррекции.

Такая сводка нужна и исходному плану, и его поправкам. Число слотов на план зададим параметром P: 1 для обычной оценки, 9 для предложений. Один рабочий элемент обработает один из M × P слотов. Целочисленное деление slot на P укажет исходный план, остаток — номер предложения. При расчёте расстояния это сопоставление вернёт любую поправку к исходной последовательности своего плана.

__kernel void DPCCProjectionSummary(__global const float *plans,
                                    __global const float *original,
                                    __global const float *active,
                                    __global const float *returns,
                                    __global const float *states,
                                    __global const float *violations,
                                    __global float *summaries,
                                    __global const float *action_scale,
                                    uint M, uint K, uint H, uint P)
  {
   const size_t slot = get_global_id(0);
   if(P == 0 || slot >= (size_t)M * P)
      return;
   __global float *summary = summaries + slot * DPCC_PS_STRIDE;
   for(int i = 0; i < DPCC_PS_STRIDE; i++)
      summary[i] = 0.0f;
   for(int i = 4; i <= 8; i++)
      summary[i] = -1.0f;
   summary[10] = (float)(slot % P);
   const float enabled = active[slot];
   if(!DPCCBoolean(enabled) || K == 0 || K > 64 || (P != 1 && P != DPCC_PROPOSALS))
     {
      summary[9] = (float)DPCC_STATUS_INVALID;
      return;
     }
   summary[11] = enabled;
   if(enabled == 0.0f)
      return;
   for(int a = 0; a < 6; a++)
      if(!isfinite(action_scale[a]) || action_scale[a] <= 0.0f)
        {
         summary[9] = (float)DPCC_STATUS_INVALID;
         return;
        }

Обнулим сводку и присвоим −1 координатам пока не найденного нарушения. Поля 10 и 11 записи summary сохранят номер предложения и активность. Неактивный вариант завершит обработку без чтения истории. Кернел поддерживает от 1 до 64 ценовых сценариев (K). Верхняя граница относится к реализации, а не к методу DPCC. При выборе числа сценариев она требует отдельного обоснования и проверки. Для активного варианта проверим шесть масштабов action_scale: они должны быть положительными и конечными.

Активный вариант проверяется на всех K сценариях. Индекс pair указывает на returns, восемь полей violations и H + 1 состояний states по 24 компонента. Проверка конечности охватывает и промежуточные состояния счёта. Один рабочий элемент просматривает всю эту историю своего слота. Поэтому с ростом K и H увеличивается его нагрузка. Влияние на скорость требует отдельных измерений.

   summary[0] = 1.0f;
   for(uint k = 0; k < K; k++)
     {
      const size_t pair = slot * K + k;
      __global const float *violation = violations + pair * 8;
      for(int i = 0; i < 8; i++)
         if(!isfinite(violation[i]))
            summary[9] = (float)DPCC_STATUS_NUMERIC;
      if(!isfinite(returns[pair]))
         summary[9] = (float)DPCC_STATUS_NUMERIC;
      for(size_t i = 0; i < ((size_t)H + 1) * 24; i++)
         if(!isfinite(states[pair * ((size_t)H + 1) * 24 + i]))
            summary[9] = (float)DPCC_STATUS_NUMERIC;
      if(summary[9] != 0.0f)
         continue;
      if(!DPCCInteger(violation[0], -1.0f, (float)H - 1.0f) ||
         !DPCCInteger(violation[1], -1.0f, 15.0f) ||
         !DPCCInteger(violation[6], -1.0f, 3.0f) ||
         !DPCCInteger(violation[7], -1.0f, (float)H - 1.0f) ||
         violation[2] < 0.0f || violation[3] < 0.0f ||
         (violation[5] != 0.0f && violation[5] != 3.0f && violation[5] != 5.0f))
        {
         summary[9] = (float)DPCC_STATUS_NUMERIC;
         continue;
        }
      if(violation[5] == (float)DPCC_STATUS_NUMERIC)
         summary[9] = (float)DPCC_STATUS_NUMERIC;

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

      if(violation[0] >= 0.0f || violation[5] != 0.0f || violation[2] != 0.0f || violation[3] != 0.0f)
         summary[0] = 0.0f;
      summary[1] = fmax(summary[1], violation[2]);
      summary[2] += violation[3];
      if(violation[0] >= 0.0f)
        {
         if(violation[1] < 0.0f || violation[6] < 0.0f)
           {
            summary[9] = (float)DPCC_STATUS_NUMERIC;
            continue;
           }
         const int earlier = summary[4] < 0.0f || violation[0] < summary[4] ||
            (violation[0] == summary[4] && (violation[6] < summary[5] ||
             (violation[6] == summary[5] && ((float)k < summary[6] ||
              ((float)k == summary[6] && violation[1] < summary[7])))));
         if(earlier)
           {
            summary[4] = violation[0];
            summary[5] = violation[6];
            summary[6] = (float)k;
            summary[7] = violation[1];
            summary[8] = violation[7];
           }
        }
     }

Нарушение хотя бы в одном переданном сценарии делает план сценарно недопустимым. Поле 1 записи summary хранит наибольший нормированный показатель vmax. Поле 2 содержит vsum — сумму сценарных сводок, каждая из которых уже складывает максимумы по категориям ограничений. Это не денежные убытки. Вероятности сценариев здесь не используются. При изменении K или состава сценариев vsum меняет масштаб, поэтому сравнивать его между такими наборами напрямую нельзя. Прибыль в одном сценарии не компенсирует нехватку маржи в другом. Выбор по доходности выполняется отдельно, после проверки ограничений.

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

Теперь измерим изменение первоначального замысла. Отклонение каждого компонента от original разделим на его масштаб из action_scale и возведём в квадрат. Сумма по всем действиям даст квадрат нормированного евклидова расстояния. Запишем его в distance и поле 3 сводки для сравнения близких по нарушениям поправок.

   const size_t row = slot * H * 6;
   const size_t input_row = (slot / P) * H * 6;
   float distance = 0.0f;
   for(uint h = 0; h < H; h++)
      for(int a = 0; a < 6; a++)
        {
         const size_t i = (size_t)h * 6 + a;
         const float difference = (plans[row + i] - original[input_row + i]) / action_scale[a];
         distance += difference * difference;
        }
   summary[3] = distance;
   if(!isfinite(distance) || !isfinite(summary[1]) || !isfinite(summary[2]))
      summary[9] = (float)DPCC_STATUS_NUMERIC;
   if(summary[9] != 0.0f)
      summary[0] = 0.0f;
  }

Уменьшение объёма на 0,2 лота при масштабе 0,1 лота даёт вклад 4. Изменение защитного параметра на 2 при масштабе 2 добавляет 1. Фиксированные масштабы согласуют единицы и задают вес компонентов. Расстояние отсчитывается от первоначального плана, а не суммируется по промежуточным поправкам. Возврат компонента к исходному значению уменьшает его вклад. Неконечность расстояния или показателей нарушения даст системную ошибку и снимет допустимость. При корректной сводке следующий примитив определит дальнейшую обработку плана.

Кернел DPCCProjectionPrepare

Полученную оценку обработает DPCCProjectionPrepare. На стадии 1 он читает сводку исходного плана и определяет необходимость изменений. На стадии 2 получает новую оценку после приведения действий к локальным границам. Между вызовами нужен повторный сценарный расчёт: уменьшение объёма ещё не подтверждает допустимость всей последовательности. Разделение на стадии сохраняет связь между каждой рабочей версией и именно её оценкой.

__kernel void DPCCProjectionPrepare(__global float *current,
                                    __global const float *context,
                                    __global const float *summaries,
                                    __global float *controls,
                                    __global float *base_active,
                                    __global float *final_active,
                                    __global float *orders,
                                    uint M, uint H, uint stage, uint off_account,
                                    uint off_instrument, uint off_limits,
                                    uint max_eval, uint max_iter, uint lookback)
  {
   const uint m = (uint)get_global_id(0);
   if(m >= M)
      return;
   __global float *control = controls + (size_t)m * DPCC_PC_STRIDE;
   base_active[m] = 0.0f;
   if(control[DPCC_PC_ACTIVE] == 0.0f || control[DPCC_PC_SYSTEM] != 0.0f)
      return;
   if((stage != 1 && stage != 2) || max_eval == 0 || max_eval > 160 ||
      max_iter == 0 || max_iter > 16 || lookback > H)
     {
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
      return;
     }
   if(control[DPCC_PC_STAGE] != (float)stage)
      return;

Сначала обнулим элемент base_active и пропустим неактивные планы либо записи с системной ошибкой. Для остальных проверим параметры: max_eval допускает 1–160 оценок, max_iter — 1–16 принятых поправок, lookback не превышает H. Несовпадение запрошенной и сохранённой стадий также остановит дальнейшую обработку записи.

Подходящую сводку перенесём в control через DPCCProjectionBase. Вместе с допустимостью и оценками копируются шаг, фаза, сценарий, ограничение и связанное действие. Они занимают поля от DPCC_PC_H до DPCC_PC_SOURCE. Объединение системных кодов через fmax сохраняет ранее накопленную ошибку.

void DPCCProjectionBase(__global float *control, __global const float *summary)
  {
   control[DPCC_PC_FEASIBLE] = summary[0];
   control[DPCC_PC_VMAX] = summary[1];
   control[DPCC_PC_VSUM] = summary[2];
   control[DPCC_PC_DISTANCE] = summary[3];
   for(int i = 0; i < 5; i++)
      control[DPCC_PC_H + i] = summary[4 + i];
   control[DPCC_PC_SYSTEM] = fmax(control[DPCC_PC_SYSTEM], summary[9]);
  }
   __global const float *summary = summaries + (size_t)m * DPCC_PS_STRIDE;
   DPCCProjectionBase(control, summary);
   if(summary[11] != 1.0f)
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
   if(control[DPCC_PC_SYSTEM] != 0.0f)
      return;
   control[DPCC_PC_EVAL] += 1.0f;
   if(control[DPCC_PC_EVAL] > (float)max_eval)
     {
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
      return;
     }
   if(stage == 1)
     {
      control[DPCC_PC_ORIGINAL_OK] = summary[0];
      if(summary[0] == 1.0f)
        {
         control[DPCC_PC_IDENTITY] = 1.0f;
         control[DPCC_PC_VERIFIED] = 1.0f;
         control[DPCC_PC_STATUS] = 0.0f;
         control[DPCC_PC_STAGE] = 5.0f;
         return;
        }
      // Do not mutate the original without both canonical and final reserves.
      if((float)max_eval - control[DPCC_PC_EVAL] < 2.0f)
        {
         control[DPCC_PC_STAGE] = 5.0f;
         control[DPCC_PC_STATUS] = 2.0f;
         return;
        }

Перед использованием сводки проверим её активность и увеличим DPCC_PC_EVAL. Единица счётчика означает оценку полного плана на всех сценариях. Поэтому бюджет не зависит от числа отдельных запусков кернелов и не считает пары план–сценарий. Превышение max_eval указывает на несогласованность обработки и даёт численную ошибку.

На стадии 1 положительная оценка исходного плана устанавливает признаки неизменённого возврата и выполненной проверки, статус 0 и завершённую стадию 5. Действия сохраняются. Даже равнозначная перезапись встречных объёмов здесь лишняя: допустимое предложение должно вернуться в исходном представлении. Для плана с нарушениями сначала резервируем две оценки: после начального приведения действий и перед выдачей результата. При меньшем остатке сохраняем исходный план и завершаем обработку со статусом 2. Иначе переходим к изменению, оставляя ресурс для проверки его последствий.

Начнём с границ действий. Поле 20 записи limits содержит девятибитную маску ограничений. На каждом шаге вычислим нетто-объём q и защитные параметры его направления. Активный лимит должен ограничить модуль объёма значением limits[0]. Это верно только для конечного неотрицательного лимита. В показанном фрагменте проверяется маска, но не limits[0]. При отрицательном значении формула может даже увеличить объём. Поэтому нужна предварительная проверка всех используемых числовых лимитов.

      __global const float *limits = context + off_limits;
      __global const float *instrument = context + off_instrument;
      if(!DPCCInteger(limits[20], 0.0f, 511.0f))
        {
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
         return;
        }
      const uint mask = (uint)limits[20];
      const size_t row = (size_t)m * H * 6;
      for(uint h = 0; h < H; h++)
        {
         const size_t offset = row + (size_t)h * 6;
         float q = current[offset] - current[offset + 3];
         float tp = q > 0.0f ? current[offset + 1] : current[offset + 4];
         float sl = q > 0.0f ? current[offset + 2] : current[offset + 5];
         if(mask & 1U)
            q = copysign(fmin(fabs(q), limits[0]), q);
         tp = fmax(0.0f, tp);
         sl = fmax(0.0f, sl);
         if(q != 0.0f)
           {
            if(((mask & 128U) && instrument[7] <= 0.0f) ||
               ((mask & 256U) && instrument[6] <= 0.0f))
              {
               control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
               return;
              }
            if(mask & 128U)
               sl = clamp(sl, limits[7] / instrument[7], limits[8] / instrument[7]);
            if(mask & 256U)
               tp = clamp(tp, limits[9] / instrument[6], limits[10] / instrument[6]);
           }

Отрицательные защитные параметры заменим нулями. Для ненулевой позиции биты 128 и 256 задают диапазоны SL и TP. Границы в пунктах переводим в единицы действия через масштабы instrument. Код проверяет положительность делителей, но во фрагменте не показаны конечность границ и порядок «минимум — максимум» перед clamp. Эти условия требуется проверить до коррекции.

Полученные значения запишем через DPCCProjectionEncode. Функция обнуляет шесть компонентов и заполняет только выбранную ветвь: покупку при положительном q, продажу с положительным модулем при отрицательном. Нулевой q оставляет всю запись нулевой и задаёт нулевую целевую позицию. При наличии открытой сделки такое действие предусматривает её закрытие.

void DPCCProjectionEncode(float q, float tp, float sl, __private float *action)
  {
   for(int a = 0; a < 6; a++)
      action[a] = 0.0f;
   if(q > 0.0f)
     {
      action[0] = q;
      action[1] = tp;
      action[2] = sl;
     }
   else
      if(q < 0.0f)
        {
         action[3] = -q;
         action[4] = tp;
         action[5] = sl;
        }
  }
         float action[6];
         DPCCProjectionEncode(q, tp, sl, action);
         for(int a = 0; a < 6; a++)
           {
            if(!isfinite(action[a]))
              {
               control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
               return;
              }
            current[offset + a] = action[a];
           }
        }
      control[DPCC_PC_STAGE] = 2.0f;
      base_active[m] = 1.0f;
      return;
     }

Приведение не пересчитывает средства и маржу и не округляет объём к брокерскому шагу. Его последствия проверит новый сценарный расчёт. Для этого убедимся в конечности компонентов, запишем их в current, установим стадию 2 и включим план в base_active.

После повторной оценки стадия 2 получает диагностику уже изменённой версии. По оставшемуся нарушению DPCCProjectionOrder составит порядок исправлений. В orders на план отведено H элементов. Перед заполнением присвоим им −1 и обнулим число индексов с указателем текущего места.

void DPCCProjectionOrder(__global float *control, __global float *order, uint H, uint lookback)
  {
   control[DPCC_PC_COUNT] = 0.0f;
   control[DPCC_PC_CURSOR] = 0.0f;
   for(uint i = 0; i < H; i++)
      order[i] = -1.0f;
   const int h = (int)control[DPCC_PC_H];
   const int phase = (int)control[DPCC_PC_PHASE];
   if(h < 0 || h >= (int)H || phase < 0 || phase > 3 || lookback == 0)
      return;
   const int last = phase == 0 ? h - 1 : h;
   if(last < 0)
      return;
   const int source = (int)control[DPCC_PC_SOURCE];
   uint count = 0;
   if(source >= 0 && source <= last)
      order[count++] = (float)source;
   for(int u = last; u >= 0 && count < lookback; u--)
      if(u != source)
         order[count++] = (float)u;
   control[DPCC_PC_COUNT] = (float)count;
  }

Порядок учитывает момент проблемы. Фаза 0 наступает до действия бара, поэтому последний исправляемый шаг равен h − 1. Для фаз действия, внутрибарного пути и завершения бара — 1, 2 и 3 — доступен и h. Подходящее связанное действие ставим первым. Затем добавляем остальные предшествующие шаги в обратном порядке без повторов. Пусть донабор запланирован действием 2, а нарушение обнаружено перед действием 5. При lookback, равном 3, получим порядок 2, 4, 3. Сначала проверим связанное с экспозицией действие, затем ближайшие предшествующие шаги. Таким образом, lookback ограничивает количество индексов. Связанное действие может находиться дальше ближайших баров. Все действия ещё относятся к будущему плану; шаг 5 исключён из этой поправки.

   DPCCProjectionOrder(control, orders + (size_t)m * H, H, lookback);
   if(summary[0] == 1.0f || control[DPCC_PC_COUNT] == 0.0f ||
      control[DPCC_PC_EVAL] >= (float)max_eval - 1.0f)
     {
      control[DPCC_PC_STAGE] = 4.0f;
      final_active[m] = 1.0f;
     }
   else
      control[DPCC_PC_STAGE] = 3.0f;
  }

Допустимый план, пустой список или остаток только на одну оценку переводят обработку на стадию 4 с включённой final_active. Остальные планы переходят на стадию 3 — поиск. Список пуст, в частности, при нулевом lookback или нарушении до первого действия: подходящего шага внутри будущей последовательности нет.

Кернел DPCCProjectionProposals

Для выбранного шага DPCCProjectionProposals построит конкретные предложения. Один рабочий элемент обслуживает один план и последовательно готовит до девяти вариантов. Разные планы обрабатываются независимо, а параллельная оценка пар предложение–сценарий выполняется следующим финансовым расчётом.

__kernel void DPCCProjectionProposals(__global const float *current,
                                      __global const float *context,
                                      __global float *controls,
                                      __global const float *orders,
                                      __global float *proposals,
                                      __global float *proposal_active,
                                      uint M, uint H, uint off_account,
                                      uint off_instrument, uint off_limits,
                                      uint max_eval, uint max_iter)
  {
   const uint m = (uint)get_global_id(0);
   if(m >= M)
      return;
   __global float *control = controls + (size_t)m * DPCC_PC_STRIDE;
   const size_t row = (size_t)m * H * 6;
   for(uint p = 0; p < DPCC_PROPOSALS; p++)
     {
      const size_t slot = (size_t)m * DPCC_PROPOSALS + p;
      proposal_active[slot] = 0.0f;
      DPCCProjectionCopy(current, proposals, row, slot * H * 6, (size_t)H * 6);
     }
   control[DPCC_PC_PROPOSED] = 0.0f;
   if(control[DPCC_PC_STAGE] != 3.0f || control[DPCC_PC_SYSTEM] != 0.0f ||
      control[DPCC_PC_ACTIVE] != 1.0f)
      return;
   const int available = (int)max_eval - (int)control[DPCC_PC_EVAL] - 1;
   if(available <= 0 || control[DPCC_PC_ITER] >= (float)max_iter ||
      control[DPCC_PC_CURSOR] >= control[DPCC_PC_COUNT])
      return;
   const uint cursor = (uint)control[DPCC_PC_CURSOR];
   const float selected = orders[(size_t)m * H + cursor];
   if(!DPCCInteger(selected, 0.0f, (float)H - 1.0f))
     {
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
      return;
     }
   const uint u = (uint)selected;

Чтобы варианты начинались одинаково, выключим девять элементов proposal_active и скопируем current в слоты proposals. Индекс слота равен m × 9 + p. Даже пропущенный вариант сохраняет своё место, поэтому номера предложений, маски и будущие сводки остаются согласованными.

Доступные оценки получим из остатка бюджета за вычетом одной заключительной проверки. Нулевой остаток, предел принятых поправок или конец списка оставят предложения выключенными. Для продолжающегося поиска прочитаем u из orders и проверим целочисленность индекса в диапазоне от 0 до H − 1.

   __global const float *account = context + off_account;
   __global const float *instrument = context + off_instrument;
   __global const float *limits = context + off_limits;
   if(!DPCCInteger(limits[20], 0.0f, 511.0f))
     {
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
      return;
     }
   const uint mask = (uint)limits[20];
   const size_t action_offset = row + (size_t)u * 6;
   const float q = current[action_offset] - current[action_offset + 3];
   const float original_tp = current[action_offset + (q > 0.0f ? 1 : 4)];
   const float original_sl = current[action_offset + (q > 0.0f ? 2 : 5)];
   int permitted[DPCC_PROPOSALS];
   for(uint p = 0; p < DPCC_PROPOSALS; p++)
      permitted[p] = 0;
   int emitted = 0;
   for(uint p = 0; p < DPCC_PROPOSALS; p++)
     {
      float target = q;
      float tp = original_tp;
      float sl = original_sl;
      if(p < 4)
        {
         const float multiplier = p == 0 ? 0.75f : (p == 1 ? 0.5f : (p == 2 ? 0.25f : 0.0f));
         target = q * multiplier;
        }

Из выбранного действия извлечём нетто-объём q и защитные параметры. Перед каждым вариантом заново присвоим их target, tp и sl: поправки не накапливаются друг на друге. Первые три задают 75%, 50% и 25% объёма с сохранением направления, четвёртая — нулевую позицию. Для покупки на 0,8 лота получим цели 0,6, 0,4, 0,2 и 0 лотов. Это итоговые объёмы позиции; необходимую сделку относительно состояния счёта определит DPCCRollout.

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

      else
         if(p == 4)
           {
            if(u > 0)
              {
               const size_t previous = row + ((size_t)u - 1) * 6;
               target = current[previous] - current[previous + 3];
               tp = current[previous + (target > 0.0f ? 1 : 4)];
               sl = current[previous + (target > 0.0f ? 2 : 5)];
              }
            else
              {
               target = account[2];
               tp = 0.0f;
               sl = 0.0f;
               if(target != 0.0f)
                 {
                  if((account[17] == 1.0f && instrument[7] <= 0.0f) ||
                     (account[18] == 1.0f && instrument[6] <= 0.0f) || instrument[0] <= 0.0f)
                    {
                     control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
                     return;
                    }
                  const float reference = account[target > 0.0f ? 16 : 15];
                  if(account[17] == 1.0f)
                     sl = (target > 0.0f ? reference - account[4] : account[4] - reference) /
                          instrument[0] / instrument[7];
                  if(account[18] == 1.0f)
                     tp = (target > 0.0f ? account[5] - reference : reference - account[5]) /
                          instrument[0] / instrument[6];
                 }
              }
           }

Пятый вариант, с индексом 4, пробует отменить намеченное изменение цели. При u больше нуля он берёт нетто-объём, TP и SL предыдущего действия. Однако к выбранному шагу фактическая позиция могла закрыться по защитному уровню. Повтор прежней цели тогда способен вызвать новое открытие. Это проверит последующий сценарный расчёт.

На шаге 0 предыдущего действия нет. Объём берём из поля 2 записи account, а защитные параметры восстанавливаем по снимку счёта. Поля 17 и 18 обозначают наличие SL и TP. Для покупки используем Ask, для продажи — Bid. Расстояния переводим в единицы действия через пункт и масштаб инструмента. В отличие от DPCCProjectionPrepare, здесь не показано обнуление отрицательных защитных параметров. Если уровень оказался по неверную сторону котировки, предложение требует отдельной проверки. Одной конечности числа недостаточно.

         else
            if(p == 5 || p == 6)
              {
               if((mask & 128U) == 0)
                  continue;
               if(instrument[7] <= 0.0f)
                 {
                  control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
                  return;
                 }
               sl = limits[p == 5 ? 7 : 8] / instrument[7];
              }
            else
              {
               if((mask & 256U) == 0)
                  continue;
               if(instrument[6] <= 0.0f)
                 {
                  control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_INVALID;
                  return;
                 }
               tp = limits[p == 7 ? 9 : 10] / instrument[6];
              }

После объёмных предложений проверим защитные уровни. Варианты 5 и 6 ставят SL на границы диапазона, 7 и 8 — TP. Выключенный бит исключает соответствующую пару. Промежуточные уровни не перебираются. Все девять предложений меняют одно выбранное действие. При этом пятый вариант может заменить и объём, и оба защитных параметра. Согласованные изменения нескольких действий возможны только через последующие итерации, без гарантии успеха. Варианты записываем через DPCCProjectionEncode, проверяем конечность компонентов и передаём в DPCCProjectionDirection.

int DPCCProjectionDirection(float target, __global const float *account,
                            __global const float *instrument)
  {
   const float initial = account[2];
   const int mode = (int)instrument[29];
   if((int)instrument[11] == DPCC_CALC_COLLATERAL)
      return(target == initial);
   if(target == 0.0f)
      return(1);
   if(mode == 0)
      return(target == initial);
   if(mode == 3)
      return(((target > 0.0f && initial > 0.0f) || (target < 0.0f && initial < 0.0f)) &&
             fabs(target) <= fabs(initial));
   if(mode == 1 && target < 0.0f)
      return(initial < 0.0f && fabs(target) <= fabs(initial));
   if(mode == 2 && target > 0.0f)
      return(initial > 0.0f && fabs(target) <= fabs(initial));
   return(1);
  }

Фильтр сопоставляет цель с исходной позицией и режимом из поля 29 записи instrument. Залоговый расчёт пропускает только исходный объём. В остальных случаях нулевая цель проходит проверку. При запрете торговли ненулевая цель должна совпасть с исходной. Режим только закрытия допускает прежнее направление без увеличения модуля. Режимы только покупки или продажи разрешают сохранить либо сократить исходную противоположную позицию.

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

      float action[6];
      DPCCProjectionEncode(target, tp, sl, action);
      for(int a = 0; a < 6; a++)
         if(!isfinite(action[a]))
           {
            control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
            return;
           }
      if(!DPCCProjectionDirection(target, account, instrument))
         continue;
      const size_t slot = (size_t)m * DPCC_PROPOSALS + p;
      const size_t plan = slot * H * 6;
      for(int a = 0; a < 6; a++)
         proposals[plan + (size_t)u * 6 + a] = action[a];
      int duplicate = 1;
      for(size_t i = 0; i < (size_t)H * 6; i++)
         if(proposals[plan + i] != current[row + i])
           {
            duplicate = 0;
            break;
           }
      for(uint previous_p = 0; previous_p < p && !duplicate; previous_p++)
         if(permitted[previous_p])
           {
            duplicate = 1;
            const size_t previous_plan = ((size_t)m * DPCC_PROPOSALS + previous_p) * H * 6;
            for(size_t i = 0; i < (size_t)H * 6; i++)
               if(proposals[plan + i] != proposals[previous_plan + i])
                 {
                  duplicate = 0;
                  break;
                 }
           }
      if(duplicate)
         continue;

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

      permitted[p] = 1;
      if(emitted < available)
        {
         proposal_active[slot] = 1.0f;
         emitted++;
        }
     }
   control[DPCC_PC_PROPOSED] = (float)emitted;
  }

Уникальные варианты после фильтра направления отмечает permitted. Из них в proposal_active включаем предложения по возрастанию p до исчерпания доступных оценок. При малом бюджете ранние номера получают приоритет. Фактическое число включённых вариантов запишем в DPCC_PC_PROPOSED.

Пакет proposals и маска теперь готовы к сценарному расчёту. Его результаты сведёт DPCCProjectionSummary с P, равным 9. Выбор начнётся после готовности всех читаемых сводок; этот порядок обеспечивается последовательностью запусков.

Кернел DPCCProjectionSelect

Теперь одно действие имеет несколько оценённых поправок. Уменьшение объёма могло снизить нарушение, изменение SL — устранить его. Их сравнит DPCCProjectionSelect. Рабочий элемент обслуживает исходный план. Переменная best со значением −1 отмечает отсутствие выбора, а evaluated считает активные оценки. Планы вне поиска или с системной ошибкой пропускаются.

__kernel void DPCCProjectionSelect(__global const float *proposals,
                                   __global const float *proposal_active,
                                   __global const float *summaries,
                                   __global float *current,
                                   __global float *controls,
                                   __global float *orders,
                                   __global float *final_active,
                                   uint M, uint H, uint max_eval, uint max_iter, uint lookback,
                                   float improvement_atol, float distance_tie_atol)
  {
   const uint m = (uint)get_global_id(0);
   if(m >= M)
      return;
   __global float *control = controls + (size_t)m * DPCC_PC_STRIDE;
   if(control[DPCC_PC_STAGE] != 3.0f || control[DPCC_PC_SYSTEM] != 0.0f)
      return;
   int best = -1;
   uint evaluated = 0;
   for(uint p = 0; p < DPCC_PROPOSALS; p++)
     {
      const size_t slot = (size_t)m * DPCC_PROPOSALS + p;
      if(!DPCCBoolean(proposal_active[slot]))
        {
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
         return;
        }
      if(proposal_active[slot] == 0.0f)
         continue;
      evaluated++;
      __global const float *summary = summaries + slot * DPCC_PS_STRIDE;
      control[DPCC_PC_SYSTEM] = fmax(control[DPCC_PC_SYSTEM], summary[9]);
      if(summary[11] != 1.0f || summary[10] != (float)p)
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
      if(control[DPCC_PC_SYSTEM] != 0.0f)
         continue;

Проверим маску через DPCCBoolean и пропустим корректный ноль. У активного варианта поля 10 и 11 записи summary должны содержать номер p и единицу активности. Они подтверждают соответствие слоту; свежесть оценки обеспечивает порядок запусков. Системная ошибка любой активной сводки сохраняется независимо от уже найденной удачной поправки.

При согласованных данных DPCCProjectionRiskBetter проверяет улучшение относительно рабочего плана. Допустимый вариант улучшает недопустимый. Обратный переход и замена одного допустимого другим через этот фильтр не проходят. Для двух недопустимых вариантов сначала проверим, уменьшился ли максимум нарушений более чем на epsilon. Значение допуска задаёт improvement_atol.

int DPCCProjectionRiskBetter(__global const float *candidate, float feasible, float vmax,
                             float vsum, float epsilon)
  {
   if(candidate[0] == 1.0f)
      return(feasible == 0.0f);
   if(feasible == 1.0f)
      return(0);
   return(candidate[1] < vmax - epsilon ||
          (fabs(candidate[1] - vmax) <= epsilon && candidate[2] < vsum - epsilon));
  }

Когда разница vmax укладывается в epsilon, сравниваем vsum. Его уменьшение тоже должно превысить допуск. Один параметр improvement_atol применяется к максимуму и сумме, хотя масштабы этих показателей различаются, а сумма зависит от K. Допуск требует проверки на выбранном наборе сценариев. При близких максимумах правило может принять небольшое увеличение vmax ради снижения vsum. Поэтому худшее нарушение не обязано уменьшаться на каждой принятой поправке. Но любой положительный показатель по-прежнему исключает сценарную допустимость.

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

int DPCCProjectionPrefer(__global const float *candidate, __global const float *best,
                         float epsilon, float distance_epsilon)
  {
   if(candidate[0] != best[0])
      return(candidate[0] > best[0]);
   if(candidate[0] == 0.0f)
     {
      if(DPCCProjectionRiskBetter(candidate, best[0], best[1], best[2], epsilon))
         return(1);
      if(DPCCProjectionRiskBetter(best, candidate[0], candidate[1], candidate[2], epsilon))
         return(0);
      if(fabs(candidate[1] - best[1]) > epsilon || fabs(candidate[2] - best[2]) > epsilon)
         return(0);
     }
   // Stable p is retained on a distance tie because the scan is ascending.
   return(candidate[3] < best[3] - distance_epsilon);
  }

Для смены варианта нормированное расстояние до исходного плана должно сократиться более чем на distance_epsilon. Этот допуск задаёт distance_tie_atol. Разница в пределах допуска сохраняет прежний выбор. Обход по возрастанию номера оставляет меньший индекс при равенстве. Получаем один целый вариант; усреднение поправок здесь не выполняется.

      if(!DPCCProjectionRiskBetter(summary, control[DPCC_PC_FEASIBLE], control[DPCC_PC_VMAX],
                                   control[DPCC_PC_VSUM], improvement_atol))
         continue;
      if(best < 0 || DPCCProjectionPrefer(summary,
            summaries + ((size_t)m * DPCC_PROPOSALS + (uint)best) * DPCC_PS_STRIDE,
            improvement_atol, distance_tie_atol))
         best = (int)p;
     }
   control[DPCC_PC_EVAL] += (float)evaluated;
   if(control[DPCC_PC_EVAL] > (float)max_eval - 1.0f ||
      control[DPCC_PC_PROPOSED] != (float)evaluated)
      control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
   if(control[DPCC_PC_SYSTEM] != 0.0f)
      return;

Перед обновлением плана прибавим evaluated к DPCC_PC_EVAL. Само число прочитанных оценок должно совпасть с DPCC_PC_PROPOSED, а бюджет — сохранить одну заключительную проверку. Несогласованность остановит обновление с системной ошибкой. Так выбранное изменение связывается с действительно оценённым пакетом. После проверки скопируем вариант целиком в current и перенесём оценку через DPCCProjectionBase. Увеличим число принятых поправок, сохраним номер предложения и перестроим порядок действий. Сокращение раннего донабора могло устранить первое нарушение и сделать следующим проблемным местом более поздний шаг. Поэтому список для следующей попытки формируем заново, уже по последствиям принятого изменения.

   if(best >= 0)
     {
      const size_t slot = (size_t)m * DPCC_PROPOSALS + (uint)best;
      DPCCProjectionCopy(proposals, current, slot * H * 6, (size_t)m * H * 6, (size_t)H * 6);
      DPCCProjectionBase(control, summaries + slot * DPCC_PS_STRIDE);
      control[DPCC_PC_ITER] += 1.0f;
      control[DPCC_PC_LAST_P] = (float)best;
      DPCCProjectionOrder(control, orders + (size_t)m * H, H, lookback);
     }
   else
      control[DPCC_PC_CURSOR] += 1.0f;
   if(control[DPCC_PC_FEASIBLE] == 1.0f ||
      control[DPCC_PC_ITER] >= (float)max_iter ||
      control[DPCC_PC_EVAL] >= (float)max_eval - 1.0f ||
      control[DPCC_PC_CURSOR] >= control[DPCC_PC_COUNT])
     {
      control[DPCC_PC_STAGE] = 4.0f;
      final_active[m] = 1.0f;
     }
  }

Без улучшения рабочая версия сохраняется, а DPCC_PC_CURSOR переходит к следующему действию. Счётчик DPCC_PC_ITER учитывает только принятые поправки и здесь не растёт. Оценки неудачных предложений уже израсходованы. Раздельные счётчики показывают затраты поиска и число изменений последовательности. Достигнутая допустимость, предел поправок, остаток только на заключительную оценку или конец списка переводят план на стадию 4 с включённой final_active. Иначе новый вызов DPCCProjectionProposals продолжит поиск. Даже найденный допустимый вариант перед выдачей проходит отдельный заключительный расчёт.

Кернел DPCCProjectionPublish

После проверки нужно передать дальше оценённую последовательность с её статусом. DPCCProjectionPublish получает original, current и заключительные сводки final_summaries. Он записывает план в result, величину коррекции в correction_cost, индивидуальный статус в candidate_status и сведения о поиске в diagnostics.

__kernel void DPCCProjectionPublish(__global const float *original,
                                    __global const float *current,
                                    __global const float *final_summaries,
                                    __global float *controls,
                                    __global float *result,
                                    __global float *correction_cost,
                                    __global float *candidate_status,
                                    __global float *diagnostics,
                                    uint M, uint H, uint max_eval)
  {
   const uint m = (uint)get_global_id(0);
   if(m >= M)
      return;
   __global float *control = controls + (size_t)m * DPCC_PC_STRIDE;
   const size_t row = (size_t)m * H * 6;
   const size_t diagnostic = (size_t)m * DPCC_PD_STRIDE;
   correction_cost[m] = 0.0f;
   candidate_status[m] = 2.0f;
   for(int i = 0; i < DPCC_PD_STRIDE; i++)
      diagnostics[diagnostic + i] = 0.0f;
   if(control[DPCC_PC_SYSTEM] != 0.0f)
      return;

Сначала обнулим correction_cost и диагностику, присвоив candidate_status значение 2. Это начальный код, а не доказательство исчерпания поиска. При ненулевом DPCC_PC_SYSTEM публикация прекращается. В таком случае result может хранить данные прошлого запуска, а код 2 — остаться без уточнения причины. Поэтому чтение результата всегда начинаем с проверки системного статуса текущего расчёта.

   if(control[DPCC_PC_ACTIVE] == 1.0f && control[DPCC_PC_STAGE] == 4.0f)
     {
      __global const float *summary = final_summaries + (size_t)m * DPCC_PS_STRIDE;
      DPCCProjectionBase(control, summary);
      if(summary[11] != 1.0f)
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
      control[DPCC_PC_EVAL] += 1.0f;
      if(control[DPCC_PC_EVAL] > (float)max_eval)
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
      if(control[DPCC_PC_SYSTEM] != 0.0f)
         return;
      control[DPCC_PC_STATUS] = summary[0] == 1.0f ? 0.0f : 2.0f;
      control[DPCC_PC_VERIFIED] = 1.0f;
      control[DPCC_PC_STAGE] = 5.0f;
     }
   else
      if(control[DPCC_PC_ACTIVE] == 1.0f && control[DPCC_PC_STAGE] != 5.0f)
        {
         control[DPCC_PC_SYSTEM] = (float)DPCC_STATUS_NUMERIC;
         return;
        }

Для активного плана на стадии 4 перенесём заключительную сводку, проверим её активность и учтём оценку в бюджете. Допустимость даст статус 0, оставшиеся нарушения — 2. В обоих случаях DPCC_PC_VERIFIED станет равным 1, стадия — 5. Факт выполненной проверки сохраняется независимо от её успешности. Уже завершённый план пропустит чтение сводки. Так проходят допустимый исходный вход и ранний возврат из-за недостаточного бюджета. Другая стадия активного плана означает преждевременную выдачу и получает системную ошибку.

   DPCCProjectionCopy(control[DPCC_PC_IDENTITY] == 1.0f ? original : current,
                       result, row, row, (size_t)H * 6);
   if(control[DPCC_PC_ACTIVE] == 1.0f)
     {
      correction_cost[m] = control[DPCC_PC_IDENTITY] == 1.0f ? 0.0f : control[DPCC_PC_DISTANCE];
      candidate_status[m] = control[DPCC_PC_STATUS];
     }
   diagnostics[diagnostic] = control[DPCC_PC_EVAL];
   diagnostics[diagnostic + 1] = control[DPCC_PC_ITER];
   diagnostics[diagnostic + 2] = control[DPCC_PC_VMAX];
   diagnostics[diagnostic + 3] = control[DPCC_PC_VSUM];
   for(int i = 0; i < 5; i++)
      diagnostics[diagnostic + 4 + i] = control[DPCC_PC_H + i];
   diagnostics[diagnostic + 9] = control[DPCC_PC_LAST_P];
   diagnostics[diagnostic + 10] = control[DPCC_PC_ACTIVE];
   diagnostics[diagnostic + 11] = control[DPCC_PC_VERIFIED];
  }

Признак неизменённого возврата выбирает original, иначе передаём current. Название correction_cost сохранено в программном интерфейсе. Буфер хранит квадрат нормированного расстояния между действиями, а не комиссии, проскальзывание или денежный результат. У активного плана после финальной проверки статус 0 означает сценарную допустимость, статус 2 — оставшиеся нарушения. Но код 2 используется также для неактивного слота и может сохраняться при системной ошибке. Без DPCC_PC_SYSTEM, активности и DPCC_PC_VERIFIED он не объясняет причину отказа. План со статусом 2 нельзя передавать на исполнение.

Сопровождающая диагностика объясняет итог поиска. Поля 0 и 1 содержат число оценок и принятых поправок, поля 2 и 3 — максимум и сумму нарушений. Поля 4–8 задают шаг, фазу, сценарий, ограничение и связанное действие. Поле 9 указывает номер последнего принятого предложения, поля 10 и 11 — активность и факт проверки. По ним видно, сколько изменений принято и где сохранилась проблема.

Кернел DPCCProjectionReduce

Для сведения индивидуальных результатов используем DPCCProjectionReduce. Только рабочий элемент с индексом 0 читает controls и записывает значение в aggregate. Предыдущие обновления должны завершиться до чтения. Этот порядок обеспечивают зависимости запусков; внутри кернела ожидания нет.

__kernel void DPCCProjectionReduce(__global const float *controls,
                                   __global float *aggregate, uint M, uint mode)
  {
   if(get_global_id(0) != 0)
      return;
   float system = 0.0f;
   int searching = 0;
   int exhausted = 0;
   for(uint m = 0; m < M; m++)
     {
      __global const float *control = controls + (size_t)m * DPCC_PC_STRIDE;
      const float status = control[DPCC_PC_SYSTEM];
      if(!isfinite(status) || (status != 0.0f && status != 3.0f && status != 5.0f))
         system = (float)DPCC_STATUS_NUMERIC;
      else
         system = fmax(system, status);
      if(control[DPCC_PC_ACTIVE] == 1.0f)
        {
         if(control[DPCC_PC_STAGE] == 3.0f)
            searching = 1;
         if(control[DPCC_PC_STATUS] == 2.0f)
            exhausted = 1;
         if(mode == 1 && control[DPCC_PC_STAGE] != 5.0f)
            system = (float)DPCC_STATUS_NUMERIC;
        }
     }
   if(mode > 1)
      system = (float)DPCC_STATUS_INVALID;
   aggregate[0] = system != 0.0f ? system : (mode == 0 ? (float)searching : (exhausted ? 2.0f : 0.0f));
  }

Параметр mode задаёт режим объединения. Значение больше 1 даёт ошибку входа 3. Коды системных статусов 3 и 5 имеют приоритет; неизвестное или неконечное значение трактуется как численная ошибка. При mode = 0 агрегат 1 означает, что хотя бы один активный план находится на стадии поиска 3. Ноль означает лишь отсутствие таких планов. Он возможен и до заключительной оценки, поэтому сам по себе не разрешает публикацию или исполнение. Для продолжения цикла нужно учитывать стадии отдельных планов и порядок вызовов MQL5.

В режиме 1 все активные планы должны достичь стадии 5. Если системных ошибок нет, наличие хотя бы одного активного плана со статусом 2 даёт общий результат 2; иначе возвращается 0. Полностью неактивный пакет тоже может дать 0. Следовательно, нулевой агрегат не подтверждает существование пригодного плана. Его проверяют по индивидуальному candidate_status, маске активности и признаку завершённой оценки. DPCCProjectionReduce сообщает состояние пакета, но не выбирает торговое решение по доходности.


Заключение

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

Вместе с действиями получаем величину коррекции и индивидуальный статус. Счётчики и диагностика показывают затраченные оценки, принятые поправки и оставшиеся нарушения. Допустимый исходный план возвращается без изменений. Для остальных различаем найденный допустимый вариант и завершение поиска без успеха. Совместную работу этих операций организует MQL5-программа. Её устройство остаётся за рамками текущей статьи.

Мы подготовили примитивы ограниченного локального поиска поправок торгового плана. Бюджет и фиксированные предложения не охватывают всех возможных изменений. Первое нарушение служит подсказкой, но не доказательством причины. Найденный вариант проверен только на заданных сценариях при принятой модели исполнения. Его экономическую привлекательность предстоит оценивать отдельно. Неудачный поиск не доказывает отсутствия сценарно допустимого решения. В следующей статье продолжим адаптацию DPCC к задачам финансовых рынков.


Ссылки

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

Рассмотренные операции находятся в NeuroNet.cl. Для связи с остальными частями разработки ниже приведено назначение файлов по спецификации проекта. Программы подготовки моделей и работы в тестере перечислены как ориентир; их устройство и результаты испытаний в эту статью не входят.

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

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

Прикрепленные файлы |
MQL5.zip (7502.18 KB)
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Нейронная сеть на практике: Перцептрон Нейронная сеть на практике: Перцептрон
В этой статье перцептрон представлен как основа нейронной сети, а также подробно описана его реализация на MQL5. Мы объясним, что такое функции активации и их производные, в чём разница между прямым распространением и обратным распространением, а также как используется функция потерь наименьших квадратов и градиент. Вы научитесь обучать веса и смещения, проверять результат с помощью тестовых скриптов и настраивать скорость обучения для достижения стабильной сходимости.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Адаптивный индикатор Malaysian Engulfing (Часть 2): Оптимизированный диапазон баров ретеста Адаптивный индикатор Malaysian Engulfing (Часть 2): Оптимизированный диапазон баров ретеста
В статье к индикатору Malaysian Engulfing добавляется самоадаптивный слой: диапазон баров ретеста оптимизируется с помощью ограниченного полного перебора с оценкой по MFE и MAE. В статье подробно описаны модель данных, вспомогательные процедуры и реализация на MQL5, которая собирает исторические сетапы, рассчитывает MFE/MAE и выбирает лучший параметр. Читатели узнают, как отказаться от ручной настройки и запускать индикатор с параметрами, подходящими для конкретного контекста, на разных символах и таймфреймах.