preview
Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)

Оптимизатор конкурирующего роя — Competitive Swarm Optimizer (CSO)

MetaTrader 5Трейдинг |
65 0
Andrey Dik
Andrey Dik

Содержание

  1. Введение
  2. Реализация алгоритма
  3. Результаты тестов
  4. Выводы


Введение

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

Причина этой патологии в классическом PSO хорошо известна: каждый агент на каждом шаге напрямую притягивается к глобальному лидеру. Информация о лучшем решении мгновенно распространяется по всей популяции — и мгновенно же убивает исследовательскую активность. Наша проверяемая гипотеза: если убрать прямое притяжение к глобальному лидеру и заменить его локальным соревнованием между случайными парами агентов, удастся ли сохранить разнообразие популяции достаточно долго, чтобы алгоритм не застревал при фиксированном бюджете вычислений?

Именно такой механизм предлагает Competitive Swarm Optimizer (CSO), представленный в 2014 году Ран Чэном и Яочу Цзинем. В каждом эпохе агенты случайно разбиваются на пары, проигравший учится у победителя и мягко притягивается к центру роя — единственному глобальному ориентиру, — а победитель не трогается вовсе. Информация о хороших решениях распространяется не напрямую, а через цепочку встреч.

Чтобы проверить гипотезу, мы реализовали CSO в MQL5 в виде класса, совместимого с унифицированным тестовым стендом, и провели воспроизводимые эксперименты на стандартном наборе функций: Hilly, Forest и Megacity в трёх режимах размерности — 5, 25 и 500 координат. Критерии оценки — качество решения при фиксированном числе обращений к целевой функции и стабильность результата по повторным прогонам. Дополнительно в статье представлена модернизация тестового стенда: визуализация работы алгоритмов переведена в трёхмерное пространство, что позволяет наглядно наблюдать движение популяции непосредственно на поверхности тестовой функции.


Реализация алгоритма

Представьте турнир по шахматам с необычным правилом: после каждого тура проигравший обязан пересмотреть свой стиль игры, а победитель не меняет ничего — он уже доказал свою состоятельность. Именно так работает CSO. Популяция из "m" агентов блуждает в пространстве поиска, но не все сразу — на каждой эпохе агенты разбиваются на случайные пары, внутри каждой пары выявляется победитель и проигравший, и только проигравший обновляет своё положение. Победитель замирает и ждёт следующего тура.
 
Такой подход отличается от классического PSO, где каждый агент на каждом шаге притягивается к глобальному лидеру. В CSO информация о лучших решениях распространяется постепенно, через цепочку встреч — как слухи в большом коллективе. Это замедляет схождение, но радикально улучшает исследование пространства, особенно в задачах высокой размерности.
 
В начале работы "m" агентов равномерно и случайно рассыпаются по допустимой области поиска. Каждый агент "i" имеет позицию "x_i" — вектор координат в пространстве решений, и скорость "v_i" — вектор направления и темпа движения, изначально равный нулю. Все скорости в самом начале нулевые: агенты не знают, куда двигаться, пока не состоится первое соревнование.

Пример. Пусть m = 6 агентов ищут максимум функции одной переменной на отрезке [0, 10]. После инициализации их позиции могут выглядеть так: 

Агент | Позиция x | Фитнес f(x) | Скорость v = 0 для всех.

A        | 1.2            | 3.1             
B        | 4.7            | 8.9             
C        | 3.3            | 6.4             
D        | 7.8            | 5.2             
E        | 9.1            | 2.0             
F        | 5.5            | 9.1             

Центр роя. Перед каждым туром вычисляется центр роя — среднее положение всех агентов: x̄ = (1/m) * Σ x_i
 
Центр — это не лучшее решение и не лидер, это просто геометрическая середина популяции. Он служит мягким глобальным ориентиром, препятствующим разбеганию агентов по углам пространства.
 
Пример. Для шести агентов выше: x̄ = (1.2 + 4.7 + 3.3 + 7.8 + 9.1 + 5.5) / 6 = 31.6 / 6 ≈ 5.27
 
Попарные соревнования. Агенты случайным образом перемешиваются и разбиваются на ⌊m/2⌋ пар. В каждой паре сравниваются значения фитнеса: агент с лучшим результатом становится победителем и не изменяется, агент с худшим результатом становится проигравшим и обязан учиться. Случайное разбиение на пары:
 
Пара | Агенты | Фитнесы    | Победитель | Проигравший
1      | A vs D    | 3.1 vs 5.2   | D                 | A
2      | C vs F    | 6.4 vs 9.1   | F                 | C
3      | B vs E    | 8.9 vs 2.0   | B                 | E

Агенты D, F, B замирают. Агенты A, C, E будут обновлены.
 
Обновление проигравших. Это сердце алгоритма. Проигравший агент получает новую скорость из трёх слагаемых, каждое из которых отвечает за свой аспект движения.
 
Формула скорости: v_l(t+1) = r1 · v_l(t)  +  r2 ·  (x_w(t) − x_l(t))  +  φ · r3 · (x̄(t) − x_l(t))
 
Формула позиции: x_l(t+1) = x_l(t) + v_l(t+1); где r1, r2, r3 — случайные числа из [0, 1], независимо сгенерированные для каждой координаты, индекс l обозначает проигравшего, w — победителя.
 
Слагаемое 1 — инерция: r1 ·  v_l(t). Агент сохраняет часть своей прежней скорости. Случайный коэффициент "r1" делает это сохранение стохастическим: иногда агент почти полностью следует старому курсу, иногда почти забывает его. Инерция придаёт движению плавность и помогает преодолевать небольшие локальные барьеры. Например, агент "A" имел скорость v = 0 (первый эпох), поэтому первое слагаемое обнуляется. Со второго эпоха инерция начинает играть роль.
  
Слагаемое 2 — социальное притяжение к победителю: r2 ·  (x_w(t) − x_l(t)). Проигравший тянется к победителю своей пары. Разность позиций задаёт направление, случайный коэффициент "r2" регулирует решимость — насколько сильно агент хочет дотянуться до лучшего соседа. Это локальное социальное обучение: агент учится не у абстрактного глобального лидера, а у конкретного, только что превзошедшего его соперника. Агент A (x = 1.2) проиграл агенту D (x = 7.8). Тяга к победителю: 7.8 − 1.2 = 6.6. Если r2 = 0.4, вклад этого слагаемого составит 0.4 × 6.6 = 2.64 — агент A сдвинется в сторону D.
 
Слагаемое 3 — глобальное притяжение к центру: φ · r3 ·  (x̄(t) − x_l(t)). Параметр "φ" — единственный настраиваемый коэффициент алгоритма. Он определяет, насколько сильно весь рой стягивает проигравшего к своей середине. При φ = 0 этот член исчезает, и алгоритм работает в режиме чистого попарного обучения без какой-либо централизации. При больших "φ" популяция быстрее концентрируется, но рискует потерять разнообразие. Например, центр роя x̄ ≈ 5.27, агент "A" находится в x = 1.2. Расстояние до центра: 5.27 − 1.2 = 4.07. При φ = 0.1 и r3 = 0.7 вклад составит 0.1 × 0.7 × 4.07 ≈ 0.28.
 
Итоговый расчёт для агента "A" (первый эпох, v = 0): v_A = 0  +  0.4 × 6.6  +  0.1 × 0.7 × 4.07  =  0 + 2.64 + 0.28  =  2.92; x_A = 1.2 + 2.92 = 4.12. Агент "A" переместился с позиции 1.2 на позицию 4.12 — ближе и к победителю D, и к центру роя.
 
Граничный контроль. После обновления позиция каждого проигравшего проверяется на выход за границы допустимой области. Если агент выскочил за предел, он возвращается к ближайшей границе:
 
x_l = min(max(x_l, l), u); где "l" и "u" — векторы нижних и верхних границ поиска соответственно.
 
Роль параметра "φ". Выбор "φ" существенно влияет на характер поиска. В оригинальной статье авторы рекомендуют адаптировать его к размерности задачи: для задач с разделимыми функциями и высокой размерностью — значения 0.1–0.2, для задач с сильной взаимозависимостью переменных — 0.05–0.1, при размерности ниже 500 — φ = 0. Интуиция здесь проста: в высоких размерностях агентам труднее самостоятельно найти верное направление, и коллективный центр роя служит полезным ориентиром; в низких размерностях его притяжение лишь мешает свободному исследованию. 

CSO

Рисунок 1. Иллюстрация работы алгоритма CSO

Иллюстрация состоит из четырёх блоков, идущих по логике алгоритма:

  • Step 1 Initialization — шесть агентов разбросаны случайно, скорости равны нулю.
  • Step 2 Swarm Center — те же агенты, пунктирные спицы сходятся к центру роя x̄.
  • Step 3 Pairing & Contest — три пары, победители выделены зелёным с галочкой, проигравшие красным.
  • Step 4 Loser Velocity Update — конкретный пример агента A: зелёная стрелка тянет к победителю D, жёлтая — к центру, синяя итоговая стрелка показывает новую позицию A′; рядом формула с цветовой разбивкой трёх слагаемых.

Полный цикл работы алгоритма:

Инициализация: рассеять агентов случайно, v = 0
 
ПОКА бюджет вычислений не исчерпан:
 
  1. Оценить фитнес всех агентов
  2. Вычислить центр роя x̄
  3. Случайно перемешать агентов, сформировать пары
  4. ДЛЯ каждой пары (a, b):
       определить победителя w и проигравшего l
       обновить v_l по формуле скорости
       обновить x_l = x_l + v_l
       применить граничный контроль
  5. Запомнить глобальный лучший результат
 
Вернуть лучшую найденную позицию

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

Переходим к написанию кода алгоритма CSO. Алгоритм реализован в виде класса "C_AO_CSO", наследующего базовый класс "C_AO". Архитектура позволяет встраивать CSO в единый фреймворк тестирования популяционных алгоритмов без изменения тестового стенда: внешний код взаимодействует с алгоритмом через три стандартных метода — Init, Moving и Revision. Класс содержит один публичный параметр "phi" — коэффициент притяжения к центру роя, и набор приватных массивов, обеспечивающих работу алгоритма:

  • V — плоский двумерный массив скоростей всех агентов, хранящийся в строчном порядке. Доступ к скорости агента по координате "c" осуществляется через вспомогательный метод Idx(i, c), возвращающий индекс i * coords + c.
  • Vmax — массив максимально допустимых скоростей по каждой оси. Инициализируется как половина диапазона поиска по соответствующей координате и используется для ограничения скорости после каждого обновления.
  • center — вектор центра роя, пересчитываемый каждый эпох как среднее арифметическое текущих позиций всех агентов.
  • rlist — вспомогательный массив индексов, перемешиваемый методом Shuffle перед каждым формированием пар.
  • nPairs — число пар, равное половине размера популяции. При нечётном popSize последний агент в текущем эпохе не участвует в соревновании.
Конструктор устанавливает значения по умолчанию и регистрирует параметры в массиве "params", через который тестовый стенд передаёт настройки извне. 

Метод "SetParams" считывает значения из массива "params" в поля класса. Вызывается тестовым стендом после ручного изменения params[i].val, чтобы синхронизировать рабочие переменные с новыми настройками.

//————————————————————————————————————————————————————————————————————
class C_AO_CSO : public C_AO
{
public: //----------------------------------------------------------
  ~C_AO_CSO () { }
  C_AO_CSO ()
  {
    ao_name = "CSO";
    ao_desc = "Competitive Swarm Optimizer";
    ao_link = "https://www.mql5.com/ru/articles/21727";

    popSize = 50;
    phi     = 0.2;

    ArrayResize (params, 2);
    params [0].name = "popSize";
    params [0].val = popSize;
    params [1].name = "phi";
    params [1].val = phi;
  }

  void SetParams ()
  {
    popSize = (int)params [0].val;
    phi     =      params [1].val;
  }

  bool Init (const double &rangeMinP  [],
             const double &rangeMaxP  [],
             const double &rangeStepP [],
             const int     epochsP = 0);

  void Moving   ();
  void Revision ();

  //------------------------------------------------------------------
  double phi; // коэффициент притяжения к центру роя φ ∈ [0, 1]

private: //---------------------------------------------------------
  double V       []; // скорости частиц       [popSize * coords]
  double Vmax    []; // ограничение скорости  [coords]
  double center  []; // центр роя             [coords]
  int    rlist   []; // вспомогательный массив перемешивания [popSize]
  int    nPairs;     // число пар = popSize / 2
  bool   initialized;

  int    Idx     (int i, int c)
  {
    return i * coords + c;
  }
  double Clamp   (double v, double lo, double hi)
  {
    return v < lo ? lo : v > hi ? hi : v;
  }
  void   Shuffle (); // Fisher-Yates перемешивание rlist
};
//————————————————————————————————————————————————————————————————————

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

Вычисляется число пар nPairs = popSize / 2. Выделяются массивы "V", "Vmax", "center" и "rlist".

Для каждой координаты c рассчитывается Vmax [c] = (rangeMax [c] - rangeMin [c]) * 0.5. Ограничение скорости половиной диапазона — стандартная практика в PSO-подобных алгоритмах: оно предотвращает ситуацию, когда агент за один шаг перелетает через всё пространство поиска.

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

//————————————————————————————————————————————————————————————————————
bool C_AO_CSO::Init (const double &rangeMinP  [],
                     const double &rangeMaxP  [],
                     const double &rangeStepP [],
                     const int     epochsP = 0)
{
  if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false;

  //------------------------------------------------------------------
  nPairs = popSize / 2; // нечётный агент не участвует в соревновании

  ArrayResize (V,      popSize * coords);
  ArrayResize (Vmax,   coords);
  ArrayResize (center, coords);
  ArrayResize (rlist,  popSize);

  // Vmax = половина диапазона по каждой оси
  for (int c = 0; c < coords; c++) Vmax [c] = (rangeMax [c] - rangeMin [c]) * 0.5;

  // Начальные позиции — случайные; скорости = 0 (как в оригинальной статье)
  for (int i = 0; i < popSize; i++)
  {
    for (int c = 0; c < coords; c++)
    {
      a [i].cP [c] = u.RNDfromCI (rangeMin [c], rangeMax [c]);
      V [Idx (i, c)] = 0.0;
    }
    a [i].fB = -DBL_MAX;
  }

  initialized = false;
  return true;
}
//————————————————————————————————————————————————————————————————————

Метод "Moving" отвечает за подготовку позиций агентов к внешней оценке фитнеса. Его задача проста: скопировать текущие рабочие координаты "cP" каждого агента в координаты "c", попутно применив квантование через "SeInDiSp" — функцию, которая выравнивает значение на допустимую сетку с учётом шага "rangeStep". Если шаг равен нулю, квантование не производится.

В CSO метод "Moving" однофазный: каждый эпох все агенты просто предъявляют свои текущие позиции "cP" для оценки. Никакого разбиения на фазы здесь нет — это делает структуру алгоритма особенно прозрачной.

//————————————————————————————————————————————————————————————————————
// Moving: копируем текущие позиции cP → c для внешней оценки фитнеса
void C_AO_CSO::Moving ()
{
  for (int i = 0; i < popSize; i++) for (int c = 0; c < coords; c++) a [i].c [c] = u.SeInDiSp (a [i].cP [c], rangeMin [c], rangeMax [c], rangeStep [c]);
}
//————————————————————————————————————————————————————————————————————

Revision — главный метод алгоритма, выполняемый после того, как внешний цикл вычислил фитнес для всех агентов. Он состоит из четырёх последовательных этапов.

Этап 1. Обновление лучших результатов. Для каждого агента проверяется, превысил ли его текущий фитнес a[i].f личный рекорд a[i].fB. Если да — личный рекорд обновляется и запоминается позиция. Аналогично обновляется глобальный лучший результат "fB" и вектор "cB". 

Этап 2. Вычисление центра роя. Массив "center" обнуляется, затем в цикле по всем агентам и координатам накапливается сумма текущих позиций "cP". После деления на "popSize" получается среднее положение роя. Обратите внимание: центр вычисляется именно по "cP", а не по "c" — то есть по тем позициям, которые будут использоваться для движения. Не по тем, которые были переданы на оценку в "Moving". При синхронной архитектуре (Moving всегда копирует cP → c) разница несущественна, но семантически верно считать центр по рабочим координатам.

Этап 3. Перемешивание и формирование пар. Вызывается метод "Shuffle", который случайным образом переставляет индексы 0..popSize-1 в массиве "rlist". После перемешивания первая половина "rlist" содержит индексы одних участников пар, вторая половина — других. Пара "k" образована агентами rlist [k] и rlist [k + nPairs].

Этап 4. Попарные соревнования и обновление проигравших. Для каждой пары определяются победитель "w" и проигравший "l": поскольку задача максимизации, проигравшим считается агент с меньшим значением фитнеса. Затем для каждой координаты "c" вычисляются три независимых случайных числа "r1", "r2", "r3" из [0, 1] и применяется формула скорости:

 vNew = r1 * V [idx] + r2 * (cP [w][c] - cP [l][c]) + phi * r3 * (center [c] - cP [l][c])

Полученная скорость ограничивается диапазоном [-Vmax [c], Vmax [c]], после чего вычисляется новая позиция:

 xNew = cP [l][c] + vNew

Позиция также ограничивается границами поиска. Скорость и позиция победителя "w" не изменяются.

Важная деталь реализации: обновление позиции проигравшего происходит сразу в массиве "cP", то есть в середине цикла по парам. Это означает, что если один и тот же агент в данном эпохе теоретически мог бы оказаться проигравшим в нескольких парах (что невозможно при корректном перемешивании, поскольку каждый агент входит ровно в одну пару), его обновлённая позиция уже была бы видна другим парам. При правильном перемешивании эта тонкость не создаёт проблем.

//————————————————————————————————————————————————————————————————————
// Revision:
//   1. Обновить личные / глобальный лучшие
//   2. Вычислить центр роя
//   3. Сформировать случайные пары, определить победителей / проигравших
//   4. Обновить скорости и позиции проигравших (формулы из статьи)
void C_AO_CSO::Revision ()
{
  //--- 1. Обновление лучших ----------------------------------------
  for (int i = 0; i < popSize; i++)
  {
    if (a [i].f > a [i].fB)
    {
      a [i].fB = a [i].f;
      ArrayCopy (a [i].cB, a [i].c, 0, 0, coords);
    }
    if (a [i].f > fB)
    {
      fB = a [i].f;
      ArrayCopy (cB, a [i].c, 0, 0, coords);
    }
  }

  initialized = true;

  //--- 2. Центр роя: center = (1/m) * Σ cP_i ---------------------
  ArrayInitialize (center, 0.0);
  for (int i = 0; i < popSize; i++) for (int c = 0; c < coords; c++) center [c] += a [i].cP [c];
  for (int c = 0; c < coords; c++) center [c] /= (double)popSize;

  //--- 3. Случайные пары ------------------------------------------
  Shuffle ();

  //--- 4. Попарные соревнования и обновление проигравших ----------
  for (int k = 0; k < nPairs; k++)
  {
    int ai = rlist [k];
    int bi = rlist [k + nPairs];

    // Максимизация: проигравший = агент с меньшим фитнесом
    int li = (a [ai].f < a [bi].f) ? ai : bi; // loser
    int wi = (a [ai].f < a [bi].f) ? bi : ai; // winner

    // Формула скорости (Eq. 3 из статьи):
    // v_l(t+1) = r1⊙v_l(t) + r2⊙(x_w(t)−x_l(t)) + φ·r3⊙(x̄(t)−x_l(t))
    //
    // Формула позиции:
    // x_l(t+1) = x_l(t) + v_l(t+1)
    for (int c = 0; c < coords; c++)
    {
      double r1 = u.RNDfromCI (0.0, 1.0);
      double r2 = u.RNDfromCI (0.0, 1.0);
      double r3 = u.RNDfromCI (0.0, 1.0);

      int idx = Idx (li, c);

      double vNew =          r1 * V [idx]
                             +        r2 * (a [wi].cP [c] - a [li].cP [c])
                             + phi  * r3 * (center [c] - a [li].cP [c]);

      vNew = Clamp (vNew, -Vmax [c], Vmax [c]);

      double xNew = Clamp (a [li].cP [c] + vNew, rangeMin [c], rangeMax [c]);

      V [idx] = vNew;
      a [li].cP [c] = xNew;
      // Позиция победителя cP[wi] не изменяется
    }
  }
}
//————————————————————————————————————————————————————————————————————

Метод "Shuffle" реализует алгоритм Фишера — Йетса для равновероятного перемешивания массива целых чисел. Сначала "rlist" заполняется последовательностью 0, 1, 2, ..., popSize-1. Затем в цикле от конца к началу каждый элемент меняется местами со случайным элементом из ещё не обработанного префикса. Результат — равномерная случайная перестановка, гарантирующая, что разбиение на пары каждый эпох полностью непредсказуемо.

Генератор случайных чисел вызывается через u.RNDfromCI (0.0, 1.0), где "u" — экземпляр вспомогательного класса C_AO_Utilities из базового класса. Индекс "j" ограничивается сверху значением "i", чтобы избежать выхода за границы массива при округлении вещественного числа до целого. 

Вспомогательные методы:

Idx (i, c) — возвращает линейный индекс в одномерном массиве "V" для агента "i" и координаты "c". 

Clamp (v, lo, hi) — ограничивает значение "v" диапазоном [lo, hi]. Применяется как к скоростям, так и к позициям после обновления.

//————————————————————————————————————————————————————————————————————
// Fisher-Yates перемешивание массива rlist
void C_AO_CSO::Shuffle ()
{
  for (int i = 0; i < popSize; i++) rlist [i] = i;
  for (int i = popSize - 1; i > 0; i--)
  {
    int j = (int)(u.RNDfromCI (0.0, 1.0) * (i + 1));
    if (j > i) j = i;
    int tmp    = rlist [i];
    rlist [i] = rlist [j];
    rlist [j] = tmp;
  }
}
//————————————————————————————————————————————————————————————————————


Результаты тестов

CSO|Competitive Swarm Optimizer|50.0|0.2|
=============================
5 Hilly's; Func runs: 10000; result: 0.9029155571810208
25 Hilly's; Func runs: 10000; result: 0.6188702055480547
500 Hilly's; Func runs: 10000; result: 0.29830471226794253
=============================
5 Forest's; Func runs: 10000; result: 0.9998176263259818
25 Forest's; Func runs: 10000; result: 0.6458134365888859
500 Forest's; Func runs: 10000; result: 0.2397479908923888
=============================
5 Megacity's; Func runs: 10000; result: 0.633846153846154
25 Megacity's; Func runs: 10000; result: 0.4055384615384615
500 Megacity's; Func runs: 10000; result: 0.12852307692307804
=============================
All score: 4.87338 (54.15%)

Визуализация работы алгоритма CSO. Наша визуализация претерпела изменения, благодаря реализации в 3D. Теперь мы можем увидеть не только функции с цветовой градацией в 3D, но и трекеры лучших решений на каждом запуске фитнес-функции. Ниже разберем код реализации трехмерного стенда.

Hilly

CSO на тестовой функции Hilly

Forest

CSO на тестовой функции Forest

Megacity

CSO на тестовой функции Megacity

Rastigin

CSO на тестовой функции Rostrigin

Ackley

CSO на тестовой функции Ackley

Класс C_TestStand3D — трёхмерный тест-стенд метаэвристик. Наряду с классическим двумерным тест-стендом (C_TestStand), отображающим ландшафт функции в виде тепловой карты и графика сходимости, в инструментарий серии добавлен трёхмерный визуализатор C_TestStand3D. Он накрывает левую квадратную панель двумерного стенда интерактивной DirectX-поверхностью и работает параллельно с ним: правая панель с графиком сходимости остаётся видна сквозь двумерный канвас в неизменном виде.

Поверхность функции строится методом BuildSurface(), принимающим объект "C_Function" и параметр разрешения сетки (по умолчанию 80 вершин на ось, допустимый диапазон 10–200). Цветовая схема — CS_COLD_TO_HOT: синий соответствует минимуму функции, красный — максимуму и визуально согласуется с тепловой картой двумерного стенда. Агенты популяции передаются методом SetAgents() в виде массива пар (x, y) и проецируются на поверхность вручную через матрицу вид × проекция, поскольку двумерные примитивы CCanvas3D не участвуют в конвейере DirectX. Лучший агент выделяется белым кружком увеличенного радиуса.

Принципиальным отличием трёхмерного стенда от двумерного является наличие трекера лучшего решения — инструмента, недоступного в плоском представлении. Трекер записывает мировые координаты лучшего агента на каждой эпохе в кольцевой буфер ёмкостью 600 точек и отображает накопленную историю в виде непрерывной золотой линии (#FFC828). Прозрачность линии нарастает от начала трека к его концу: старые позиции отрисовываются почти прозрачными (alpha ≈ 0x30), новые — насыщенными (alpha ≈ 0xE0), что позволяет с первого взгляда оценить направление и скорость движения лучшего решения по ландшафту. Для визуальной толщины каждый отрезок дублируется с вертикальным смещением в один пиксель. Трек автоматически сбрасывается при смене тестовой функции (BuildSurface) и в начале каждого повторного прогона (ResetTrail), поэтому на экране всегда отображается история только текущего независимого теста.

Пока запланировано, что камера будет управляться перетаскиванием левой кнопкой мыши (вращение по азимуту и наклону), колесом и двойным щелчком (сброс к позиции по умолчанию), в данный момент картинка неподвижна, в коде также запланировано место на будущие изменения в текущей реализации. Матрицы LookAt и перспективной проекции реализованы портативными хелперами _LookAtLH и _PerspLH без зависимости от внешних DX-утилит, поскольку MQL5 требует передачи структур DXVector3 исключительно по ссылке и не поддерживает поля матриц в нотации _11/_12. Цвет агентов формируется функцией _HSL, равномерно распределяющей оттенки спектра от синего (первый агент) до красного (последний) при насыщенности S = 1 и яркости L = 0.5.

//+------------------------------------------------------------------+
//|                                                  TestStand3D.mqh |
//|  3D-панель визуализации для тест-стенда метаэвристик             |
//|                                                                  |
//|  Порядок #include в скрипте:                                     |
//|    1. TestStandFunctions.mqh  — C_TestStand (2D-стенд)           |
//|    2. TestFunctions.mqh       — C_Function, GenerateDataFixedSize|
//|    3. TestStand3D.mqh         — этот файл                        |
//|    4. алгоритм оптимизации                                       |
//+------------------------------------------------------------------+

#include <Canvas\Canvas3D.mqh>
#include <Canvas\DX\DXSurface.mqh>
#include <Math\AOs\TestFunctions.mqh>

//--- Параметры камеры (можно менять)
#define S3D_DIST_DEF      35.0    // дистанция по умолчанию
#define S3D_DIST_MIN      12.0    // минимальный zoom-in
#define S3D_DIST_MAX      80.0    // максимальный zoom-out
#define S3D_PITCH_DEF     0.45f   // наклон камеры по умолчанию (~26°)
#define S3D_FOV           0.5236f // поле зрения = π/6 (30°)
//--- Полуразмеры поверхности в мировых координатах
#define S3D_SX            10.0f
#define S3D_SY             5.0f
#define S3D_SZ            10.0f

//--- Фоновый цвет 3D-панели
#define S3D_BG            0xFF0D0D1A

//+------------------------------------------------------------------+
//| S3DTrailPt — точка трека лучшего решения (мировые координаты)    |
//+------------------------------------------------------------------+
struct S3DTrailPt
{
  float wx, wy, wz;
};

//+------------------------------------------------------------------+
//| S3DAgent — агент популяции (мировые координаты + цвет)           |
//+------------------------------------------------------------------+
struct S3DAgent
{
  float             wx, wy, wz;
  uint              clr;        // ARGB
  bool              is_best;    // лучший агент — особая отрисовка
};

//+------------------------------------------------------------------+
//| C_TestStand3D                                                    |
//|                                                                  |
//| Отображает левую (H×H) панель тест-стенда как интерактивную      |
//| 3D-поверхность функции с проекцией агентов и треком лучшего.     |
//|                                                                  |
//| Управление (EA-контекст, события через OnChartEvent):            |
//|   LMB-drag      — вращение камеры                                |
//|   Scroll        — zoom                                           |
//|   Double-click  — сброс камеры к позиции по умолчанию            |
//+------------------------------------------------------------------+
class C_TestStand3D
{
  //--- DX-объекты
  CCanvas3D  m_cv;
  CDXSurface m_surf;

  //--- параметры канваса
  string m_name;
  int    m_x, m_y, m_w, m_h;
  bool   m_surf_ok;
  bool   m_vis;
  bool   m_shutdown_done;
  int    m_grid;

  //--- параметры камеры
  float  m_pitch;      // наклон (вокруг X)
  float  m_yaw;        // азимут (вокруг Y)
  double m_dist;       // расстояние от центра сцены
  int    m_mx, m_my;   // предыдущая позиция мыши (локальная)

  //--- матрица вид×проекция для ручного проецирования агентов на экран
  DXMatrix m_vp;

  //--- диапазон домена функции (для маппинга агентов в мировые координаты)
  double m_fxMin, m_fxMax, m_fyMin, m_fyMax;

  //--- агенты текущей эпохи
  S3DAgent m_ag [];
  int      m_nAg;

  //--- трек лучшего решения (кольцевой буфер)
  S3DTrailPt m_trail [];
  int        m_trailLen;
  int        m_trailMax;

public:
  C_TestStand3D  ();
  ~C_TestStand3D ()
  {
    Shutdown ();
  }

  //--- жизненный цикл
  bool              Init (const string name, int x, int y, int w, int h);
  void              Shutdown ();

  //--- контент
  // grid — вершин по одной оси (10..200); вызывать при смене функции
  bool              BuildSurface (C_Function &func, int grid = 80);
  // args[] = [x0,y0, x1,y1, …] (count пар); best_idx = -1 → без выделения
  void              SetAgents (double &args [], int count,
                               C_Function &func, int best_idx = -1);
  // один кадр рендера
  void              Redraw ();
  // сброс трека лучшего (вызывать в начале каждого нового теста)
  void              ResetTrail ()
  {
    m_trailLen = 0;
  }

  //--- события (маршрутизировать из OnChartEvent в EA)
  void              OnMouseMove (int chart_x, int chart_y, uint flags);
  void              OnMouseWheel (double delta);
  void              OnDblClick ();
  // dt в секундах; в скрипте передавать фиксированный виртуальный шаг
  void              OnTimer (double dt);

  //--- видимость и размер
  void              Show (bool v);
  bool              IsVisible () const
  {
    return m_vis;
  }
  string            GetName ()   const
  {
    return m_name;
  }
  void              Resize (int new_w, int new_h);

private:
  //--- пересчёт матриц камеры и VP
  void              _CamUpdate ();
  //--- проекция мировой точки → экранные пиксели; false = за кадром
  bool              _Project (float wx, float wy, float wz, int &sx, int &sy);
  //--- отрисовка трека лучшего, агентов
  void              _DrawTrail ();
  void              _DrawAgents ();

  //--- матричные хелперы (portable, без внешних DX-функций)
  // Внимание: DXMatrix в MQL5 хранит элементы как m[row][col]
  //           DXVector3 передаётся только по ссылке
  static void       _LookAtLH (DXMatrix &M, DXVector3 &eye, DXVector3 &tgt, DXVector3 &up);
  static void       _PerspLH (DXMatrix &M, float fov, float aspect, float zn, float zf);
  static void       _MulMat (DXMatrix &C, DXMatrix &A, DXMatrix &B);

  //--- радужный цвет по индексу агента: h_deg ∈ [0,360] → ARGB (S=1, L=0.5)
  static uint       _HSL (int h_deg);
};

//+------------------------------------------------------------------+
C_TestStand3D::C_TestStand3D ()
  : m_surf_ok (false), m_vis (false), m_shutdown_done (false),
    m_nAg (0), m_grid (80),
    m_pitch (S3D_PITCH_DEF), m_yaw (0.0f), m_dist (S3D_DIST_DEF),
    m_mx (-1), m_my (-1),
    m_fxMin (0), m_fxMax (1), m_fyMin (0), m_fyMax (1),
    m_trailLen (0), m_trailMax (600)
{
  ArrayResize (m_trail, m_trailMax);
}

//+------------------------------------------------------------------+
bool C_TestStand3D::Init (const string name, int x, int y, int w, int h)
{
  m_name = name;
  m_x = x;
  m_y = y;
  m_w = w;
  m_h = h;

  ResetLastError ();
  if (!m_cv.CreateBitmapLabel (m_name, m_x, m_y, m_w, m_h,
                               COLOR_FORMAT_ARGB_NORMALIZE))
  {
    PrintFormat ("C_TestStand3D::Init – ошибка CreateBitmapLabel: %d",
                 GetLastError ());
    return false;
  }

  m_cv.ProjectionMatrixSet (S3D_FOV, (float)m_w / m_h, 0.1f, 200.0f);

  DXVector3 tgt = DXVector3 (0.0f, 0.0f, 0.0f);
  DXVector3 up  = DXVector3 (0.0f, 1.0f, 0.0f);
  m_cv.ViewTargetSet (tgt);
  m_cv.ViewUpDirectionSet (up);

  m_cv.LightColorSet (DXColor (1.0f, 0.97f, 0.88f, 0.32f));
  m_cv.AmbientColorSet (DXColor (0.82f, 0.88f, 1.0f, 0.72f));

  _CamUpdate ();

  ObjectSetInteger (0, m_name, OBJPROP_HIDDEN,     true);
  ObjectSetInteger (0, m_name, OBJPROP_SELECTABLE, false);
  return true;
}

//+------------------------------------------------------------------+
void C_TestStand3D::Shutdown ()
{
  if (m_shutdown_done) return;
  m_shutdown_done = true;
  m_surf.Shutdown ();
  // m_cv.Destroy() не вызываем — деструктор CCanvas3D сделает сам.
  // Двойной вызов Destroy() приводил к Abnormal termination.
  ObjectDelete (0, m_name);
}

//+------------------------------------------------------------------+
bool C_TestStand3D::BuildSurface (C_Function &func, int grid)
{
  m_grid = (int)MathMax (10.0, MathMin (200.0, (double)grid));

  m_fxMin = func.GetMinRangeX ();
  m_fxMax = func.GetMaxRangeX ();
  m_fyMin = func.GetMinRangeY ();
  m_fyMax = func.GetMaxRangeY ();

  double buf [];
  if (!GenerateDataFixedSize (m_grid, m_grid, func, buf))
  {
    Print ("C_TestStand3D::BuildSurface – GenerateDataFixedSize failed");
    return false;
  }

  const float VAL_RANGE = 1.0f;   // C_Function нормализует значения в [0,1]
  DXVector3 bMin = DXVector3 (-S3D_SX, -S3D_SY, -S3D_SZ);
  DXVector3 bMax = DXVector3 (S3D_SX,  S3D_SY,  S3D_SZ);
  DXVector2 uv   = DXVector2 (1.0f, 1.0f);

  if (!m_surf_ok)
  {
    if (!m_surf.Create (m_cv.DXDispatcher (), m_cv.InputScene (),
                        buf, (uint)m_grid, (uint)m_grid,
                        VAL_RANGE, bMin, bMax, uv,
                        CDXSurface::SF_TWO_SIDED | CDXSurface::SF_USE_NORMALS))
    {
      Print ("C_TestStand3D::BuildSurface – CDXSurface::Create failed");
      return false;
    }
    // CDXSurface::Create не принимает цветовую схему → сразу Update,
    // иначе первая функция отображается без градиента (серая).
    m_surf.Update (buf, (uint)m_grid, (uint)m_grid,
                   VAL_RANGE, bMin, bMax, uv,
                   CDXSurface::SF_TWO_SIDED | CDXSurface::SF_USE_NORMALS,
                   CDXSurface::CS_COLD_TO_HOT);
    m_surf.SpecularColorSet (DXColor (1.0f, 1.0f, 1.0f, 0.55f));
    m_cv.ObjectAdd (&m_surf);
    m_surf_ok = true;
  }
  else
  {
    m_surf.Update (buf, (uint)m_grid, (uint)m_grid,
                   VAL_RANGE, bMin, bMax, uv,
                   CDXSurface::SF_TWO_SIDED | CDXSurface::SF_USE_NORMALS,
                   CDXSurface::CS_COLD_TO_HOT);
  }

  m_nAg      = 0;
  m_trailLen = 0;   // трек сбрасывается при смене функции
  return true;
}

//+------------------------------------------------------------------+
void C_TestStand3D::SetAgents (double &args [], int count,
                               C_Function &func, int best_idx)
{
  m_nAg = MathMax (0, count);
  if (m_nAg == 0) return;

  ArrayResize (m_ag, m_nAg);

  double rx = m_fxMax - m_fxMin;
  if (rx < 1e-9) rx = 1e-9;
  double ry = m_fyMax - m_fyMin;
  if (ry < 1e-9) ry = 1e-9;

  for (int i = 0; i < m_nAg; i++)
  {
    // клампинг в допустимый диапазон
    double ax = MathMax (m_fxMin, MathMin (m_fxMax, args [i * 2]));
    double ay = MathMax (m_fyMin, MathMin (m_fyMax, args [i * 2 + 1]));

    // маппинг: домен функции → мировые координаты поверхности
    float wx = (float)(-S3D_SX + (ax - m_fxMin) / rx * 2.0 * S3D_SX);
    float wz = (float)(-S3D_SZ + (ay - m_fyMin) / ry * 2.0 * S3D_SZ);

    // высота: нормализованное значение функции [0,1] → мировая Y + отступ
    double fv = MathMax (0.0, MathMin (1.0, func.Core (ax, ay)));
    float  wy = (float)(-S3D_SY + fv * 2.0 * S3D_SY) + 0.22f;

    m_ag [i].wx      = wx;
    m_ag [i].wy      = wy;
    m_ag [i].wz      = wz;
    m_ag [i].is_best = (i == best_idx);
    // радужный цвет: синий (i=0) → красный (i=last)
    m_ag [i].clr     = _HSL ((int)(270.0 * i / MathMax (1, m_nAg - 1)));

    // добавить позицию лучшего в трек
    if (i == best_idx && best_idx >= 0)
    {
      if (m_trailLen < m_trailMax)
      {
        m_trail [m_trailLen].wx = wx;
        m_trail [m_trailLen].wy = wy;
        m_trail [m_trailLen].wz = wz;
        m_trailLen++;
      }
      else
      {
        // буфер полон — сдвиг на 1, старейшая точка теряется
        for (int t = 0; t < m_trailMax - 1; t++) m_trail [t] = m_trail [t + 1];
        m_trail [m_trailMax - 1].wx = wx;
        m_trail [m_trailMax - 1].wy = wy;
        m_trail [m_trailMax - 1].wz = wz;
      }
    }
  }
}

//+------------------------------------------------------------------+
void C_TestStand3D::Redraw ()
{
  if (!m_vis || !m_surf_ok) return;
  m_cv.Render (DX_CLEAR_COLOR | DX_CLEAR_DEPTH, S3D_BG);
  _DrawTrail ();    // трек под агентами
  _DrawAgents ();
  m_cv.Update ();
}

//+------------------------------------------------------------------+
void C_TestStand3D::_CamUpdate ()
{
  // Вращаем базовую позицию (0, 0, -dist) вокруг X (pitch), затем Y (yaw)
  DXVector4 cam = DXVector4 (0.0f, 0.0f, -(float)m_dist, 1.0f);
  DXVector4 lgt = DXVector4 (0.25f, -0.45f, 1.0f, 0.0f);
  DXMatrix  rot;

  DXMatrixRotationX (rot, m_pitch);
  DXVec4Transform   (cam, cam, rot);
  DXVec4Transform   (lgt, lgt, rot);

  DXMatrixRotationY (rot, m_yaw);
  DXVec4Transform (cam, cam, rot);
  DXVec4Transform (lgt, lgt, rot);

  DXVector3 eye = DXVector3 (cam);
  DXVector3 ld  = DXVector3 (lgt);
  DXVector3 tgt = DXVector3 (0.0f, 0.0f, 0.0f);
  DXVector3 up  = DXVector3 (0.0f, 1.0f, 0.0f);

  // Обновляем все параметры камеры, чтобы CCanvas3D пересчитал view-матрицу
  m_cv.ViewPositionSet (eye);
  m_cv.ViewTargetSet (tgt);
  m_cv.ViewUpDirectionSet (up);
  m_cv.LightDirectionSet (ld);

  // Строим VP-матрицу для ручного проецирования агентов в _Project()
  DXMatrix view, proj;
  _LookAtLH (view, eye, tgt, up);
  _PerspLH (proj, S3D_FOV, (float)m_w / m_h, 0.1f, 200.0f);
  _MulMat (m_vp, view, proj);
}

//+------------------------------------------------------------------+
bool C_TestStand3D::_Project (float wx, float wy, float wz,
                              int &sx, int &sy)
{
  DXVector4 p = DXVector4 (wx, wy, wz, 1.0f);
  DXVec4Transform (p, p, m_vp);
  if (p.w < 0.01f) return false;

  float nx = p.x / p.w;
  float ny = p.y / p.w;
  if (nx < -1.1f || nx > 1.1f || ny < -1.1f || ny > 1.1f) return false;

  sx = (int)((nx + 1.0f) * 0.5f * m_w);
  sy = (int)((1.0f - ny) * 0.5f * m_h);
  return true;
}

//+------------------------------------------------------------------+
//| _DrawTrail                                                       |
//| Золотая линия (#FFC828) по истории позиций лучшего агента.      |
//| Градиент прозрачности: старые точки alpha≈0x30, новые — 0xE0.   |
//| Двойная линия (смещение +1px по Y) — визуальная толщина.        |
//+------------------------------------------------------------------+
void C_TestStand3D::_DrawTrail ()
{
  if (m_trailLen < 2) return;

  int  px = 0, py = 0, cx = 0, cy = 0;
  bool hasPrev = false;

  for (int t = 0; t < m_trailLen; t++)
  {
    if (!_Project (m_trail [t].wx, m_trail [t].wy, m_trail [t].wz, cx, cy))
    {
      hasPrev = false;   // разрыв при выходе точки за viewport
      continue;
    }

    if (!hasPrev)
    {
      px = cx;
      py = cy;
      hasPrev = true;
      continue;
    }

    double ratio = (double)t / (double)(m_trailLen - 1);
    uint   alpha = (uint)(0x30 + ratio * 0xB0);   // 0x30 → 0xE0

    uint clr  = (alpha        << 24) | 0x00FFC828;
    uint clrT = ((alpha >> 1) << 24) | 0x00FFC828;  // тень +1px

    m_cv.LineAA (px, py,     cx, cy,     clr);
    m_cv.LineAA (px, py + 1, cx, cy + 1, clrT);

    px = cx;
    py = cy;
  }
}

//+------------------------------------------------------------------+
//| _DrawAgents                                                      |
//| Обычный агент: цветной кружок r=4 + чёрная обводка.             |
//| Лучший агент: белый кружок r=7 + жирная тень.                   |
//+------------------------------------------------------------------+
void C_TestStand3D::_DrawAgents ()
{
  for (int i = 0; i < m_nAg; i++)
  {
    int sx, sy;
    if (!_Project (m_ag [i].wx, m_ag [i].wy, m_ag [i].wz, sx, sy)) continue;

    if (m_ag [i].is_best)
    {
      m_cv.FillCircle (sx, sy, 7, 0xFFFFFFFF);
      m_cv.Circle     (sx, sy, 7, 0xFF000000);
      m_cv.Circle     (sx, sy, 8, 0xFF000000);
      m_cv.Circle     (sx, sy, 9, 0x88000000);
    }
    else
    {
      m_cv.FillCircle (sx, sy, 4, m_ag [i].clr);
      m_cv.Circle     (sx, sy, 4, 0xFF000000);
      m_cv.Circle     (sx, sy, 5, 0x55000000);
    }
  }
}

//+------------------------------------------------------------------+
void C_TestStand3D::OnMouseMove (int chart_x, int chart_y, uint flags)
{
  int lx = chart_x - m_x;
  int ly = chart_y - m_y;

  if ((flags & 1) == 1)   // LMB нажата
  {
    if (m_mx >= 0)
    {
      m_yaw   += (lx - m_mx) / 240.0f;
      m_pitch += (ly - m_my) / 240.0f;
      // ограничение наклона ±86°
      float pMax = (float)(DX_PI * 0.48);
      if (m_pitch < -pMax) m_pitch = -pMax;
      if (m_pitch >  pMax) m_pitch =  pMax;
      _CamUpdate ();
    }
    m_mx = lx;
    m_my = ly;
  }
  else
  {
    m_mx = -1;
    m_my = -1;
  }
}

//+------------------------------------------------------------------+
void C_TestStand3D::OnMouseWheel (double delta)
{
  m_dist *= (1.0 - delta * 0.0012);
  m_dist  = MathMax (S3D_DIST_MIN, MathMin (S3D_DIST_MAX, m_dist));
  _CamUpdate ();
}

//+------------------------------------------------------------------+
void C_TestStand3D::OnDblClick ()
{
  m_pitch = S3D_PITCH_DEF;
  m_yaw   = 0.0f;
  m_dist  = S3D_DIST_DEF;
  _CamUpdate ();
}

//+------------------------------------------------------------------+
void C_TestStand3D::OnTimer (double dt)
{
  // зарезервировано для будущей реализации авто-вращения
}

//+------------------------------------------------------------------+
void C_TestStand3D::Show (bool v)
{
  m_vis = v;
  ObjectSetInteger (0, m_name, OBJPROP_HIDDEN, !v);
  ChartRedraw ();
}

//+------------------------------------------------------------------+
void C_TestStand3D::Resize (int new_w, int new_h)
{
  if (new_w == m_w && new_h == m_h) return;
  m_w = new_w;
  m_h = new_h;
  m_cv.Resize (m_w, m_h);
  DXContextSetSize (m_cv.DXContext (), m_w, m_h);
  m_cv.ProjectionMatrixSet (S3D_FOV, (float)m_w / m_h, 0.1f, 200.0f);
  _CamUpdate ();
}

//══════════════════════════════════════════════════════════════════
//  Матричные хелперы
//
//  DXMatrix в MQL5 хранит элементы как m[row][col], НЕ как _11/_12
//  DXVector3 передаётся только по ссылке (объекты — только ref в MQL5)
//  Все матрицы — row-major, left-handed (DirectX конвенция)
//══════════════════════════════════════════════════════════════════

//--- LookAt LH: эквивалент D3DXMatrixLookAtLH
void C_TestStand3D::_LookAtLH (DXMatrix &M, DXVector3 &eye, DXVector3 &tgt, DXVector3 &up)
{
  // z = normalize(tgt - eye)  — ось «вперёд»
  float zx = tgt.x - eye.x, zy = tgt.y - eye.y, zz = tgt.z - eye.z;
  float zl = (float)MathSqrt (zx * zx + zy * zy + zz * zz);
  if (zl < 1e-7f) zl = 1e-7f;
  zx /= zl;
  zy /= zl;
  zz /= zl;

  // x = normalize(cross(up, z))  — ось «вправо»
  float xx = up.y * zz - up.z * zy;
  float xy = up.z * zx - up.x * zz;
  float xz = up.x * zy - up.y * zx;
  float xl = (float)MathSqrt (xx * xx + xy * xy + xz * xz);
  if (xl < 1e-7f) xl = 1e-7f;
  xx /= xl;
  xy /= xl;
  xz /= xl;

  // y = cross(z, x)  — ортогонализованная ось «вверх»
  float yx = zy * xz - zz * xy;
  float yy = zz * xx - zx * xz;
  float yz = zx * xy - zy * xx;

  // трансляционная часть (dot-products позиции камеры с осями)
  float dx = -(xx * eye.x + xy * eye.y + xz * eye.z);
  float dy = -(yx * eye.x + yy * eye.y + yz * eye.z);
  float dz = -(zx * eye.x + zy * eye.y + zz * eye.z);

  M.m [0] [0] = xx;
  M.m [0] [1] = yx;
  M.m [0] [2] = zx;
  M.m [0] [3] = 0.0f;
  M.m [1] [0] = xy;
  M.m [1] [1] = yy;
  M.m [1] [2] = zy;
  M.m [1] [3] = 0.0f;
  M.m [2] [0] = xz;
  M.m [2] [1] = yz;
  M.m [2] [2] = zz;
  M.m [2] [3] = 0.0f;
  M.m [3] [0] = dx;
  M.m [3] [1] = dy;
  M.m [3] [2] = dz;
  M.m [3] [3] = 1.0f;
}

//--- Perspective LH: эквивалент D3DXMatrixPerspectiveFovLH
void C_TestStand3D::_PerspLH (DXMatrix &M,
                              float fov, float aspect, float zn, float zf)
{
  float ys = 1.0f / (float)MathTan (fov * 0.5);
  float xs = ys / aspect;
  float zr = zf / (zf - zn);

  M.m [0] [0] = xs;
  M.m [0] [1] = 0.0f;
  M.m [0] [2] = 0.0f;
  M.m [0] [3] = 0.0f;
  M.m [1] [0] = 0.0f;
  M.m [1] [1] = ys;
  M.m [1] [2] = 0.0f;
  M.m [1] [3] = 0.0f;
  M.m [2] [0] = 0.0f;
  M.m [2] [1] = 0.0f;
  M.m [2] [2] = zr;
  M.m [2] [3] = 1.0f;
  M.m [3] [0] = 0.0f;
  M.m [3] [1] = 0.0f;
  M.m [3] [2] = -zn * zr;
  M.m [3] [3] = 0.0f;
}

//--- C = A × B (row-major)
void C_TestStand3D::_MulMat (DXMatrix &C, DXMatrix &A, DXMatrix &B)
{
  C.m [0] [0] = A.m [0] [0] * B.m [0] [0] + A.m [0] [1] * B.m [1] [0] + A.m [0] [2] * B.m [2] [0] + A.m [0] [3] * B.m [3] [0];
  C.m [0] [1] = A.m [0] [0] * B.m [0] [1] + A.m [0] [1] * B.m [1] [1] + A.m [0] [2] * B.m [2] [1] + A.m [0] [3] * B.m [3] [1];
  C.m [0] [2] = A.m [0] [0] * B.m [0] [2] + A.m [0] [1] * B.m [1] [2] + A.m [0] [2] * B.m [2] [2] + A.m [0] [3] * B.m [3] [2];
  C.m [0] [3] = A.m [0] [0] * B.m [0] [3] + A.m [0] [1] * B.m [1] [3] + A.m [0] [2] * B.m [2] [3] + A.m [0] [3] * B.m [3] [3];
  C.m [1] [0] = A.m [1] [0] * B.m [0] [0] + A.m [1] [1] * B.m [1] [0] + A.m [1] [2] * B.m [2] [0] + A.m [1] [3] * B.m [3] [0];
  C.m [1] [1] = A.m [1] [0] * B.m [0] [1] + A.m [1] [1] * B.m [1] [1] + A.m [1] [2] * B.m [2] [1] + A.m [1] [3] * B.m [3] [1];
  C.m [1] [2] = A.m [1] [0] * B.m [0] [2] + A.m [1] [1] * B.m [1] [2] + A.m [1] [2] * B.m [2] [2] + A.m [1] [3] * B.m [3] [2];
  C.m [1] [3] = A.m [1] [0] * B.m [0] [3] + A.m [1] [1] * B.m [1] [3] + A.m [1] [2] * B.m [2] [3] + A.m [1] [3] * B.m [3] [3];
  C.m [2] [0] = A.m [2] [0] * B.m [0] [0] + A.m [2] [1] * B.m [1] [0] + A.m [2] [2] * B.m [2] [0] + A.m [2] [3] * B.m [3] [0];
  C.m [2] [1] = A.m [2] [0] * B.m [0] [1] + A.m [2] [1] * B.m [1] [1] + A.m [2] [2] * B.m [2] [1] + A.m [2] [3] * B.m [3] [1];
  C.m [2] [2] = A.m [2] [0] * B.m [0] [2] + A.m [2] [1] * B.m [1] [2] + A.m [2] [2] * B.m [2] [2] + A.m [2] [3] * B.m [3] [2];
  C.m [2] [3] = A.m [2] [0] * B.m [0] [3] + A.m [2] [1] * B.m [1] [3] + A.m [2] [2] * B.m [2] [3] + A.m [2] [3] * B.m [3] [3];
  C.m [3] [0] = A.m [3] [0] * B.m [0] [0] + A.m [3] [1] * B.m [1] [0] + A.m [3] [2] * B.m [2] [0] + A.m [3] [3] * B.m [3] [0];
  C.m [3] [1] = A.m [3] [0] * B.m [0] [1] + A.m [3] [1] * B.m [1] [1] + A.m [3] [2] * B.m [2] [1] + A.m [3] [3] * B.m [3] [1];
  C.m [3] [2] = A.m [3] [0] * B.m [0] [2] + A.m [3] [1] * B.m [1] [2] + A.m [3] [2] * B.m [2] [2] + A.m [3] [3] * B.m [3] [2];
  C.m [3] [3] = A.m [3] [0] * B.m [0] [3] + A.m [3] [1] * B.m [1] [3] + A.m [3] [2] * B.m [2] [3] + A.m [3] [3] * B.m [3] [3];
}

//--- HSL→ARGB при S=1, L=0.5 (чистый спектр)
uint C_TestStand3D::_HSL (int h_deg)
{
  double hue = h_deg / 360.0;
  uint   ch [3];
  double off [3];
  off [0] = 1.0 / 3.0;
  off [1] = 0.0;
  off [2] = -1.0 / 3.0;

  for (int i = 0; i < 3; i++)
  {
    double vh = hue + off [i];
    if (vh < 0.0) vh += 1.0;
    if (vh > 1.0) vh -= 1.0;

    double v;
    if     (6.0 * vh < 1.0) v = 6.0 * vh;
    else if (2.0 * vh < 1.0) v = 1.0;
    else if (3.0 * vh < 2.0) v = (2.0 / 3.0 - vh) * 6.0;
    else                     v = 0.0;

    ch [i] = (uint)(v * 255.0);
  }

  return 0xFF000000 | (ch [0] << 16) | (ch [1] << 8) | ch [2];
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+

Тестовый скрипт претерпел минимально необходимые изменения, сохранив полную обратную совместимость с двумерным стендом и не затронув логику работы алгоритма оптимизации.

Порядок директив #include стал строго фиксированным. Заголовочный файл TestStand3D.mqh подключается третьим — после TestStandFunctions.mqh и TestFunctions.mqh, поскольку внутри него используются типы "C_Function" и функция "GenerateDataFixedSize", объявленные в предшествующих заголовках. Алгоритм оптимизации подключается последним, уже после всех стендов.

К входным параметрам добавлены два новых: Use3D_P (булев флаг, разрешающий или запрещающий трёхмерную визуализацию) и Grid3D_P (разрешение сетки поверхности, допустимый диапазон 20–150). Оба параметра вынесены в отдельную группу TestStand_5 вместе с уже существующими DelayInMS_P и Video_P, что не нарушает привычный вид диалога входных параметров.

В OnStart после инициализации двумерного стенда создаётся объект C_TestStand3D через указатель. Указатель намеренно объявлен как "NULL" и заполняется только при Use3D_P = true, поэтому при отключённой трёхмерной визуализации накладные расходы равны нулю. Если Init () трёхмерного стенда возвращает ошибку — например, при отсутствии аппаратной поддержки DirectX — указатель немедленно освобождается и обнуляется, а скрипт продолжает работу в штатном двумерном режиме. При каждой смене тестовой функции вызывается BuildSurface (), перестраивающий поверхность и сбрасывающий трек, после чего стенд делается видимым через Show(true).

Функция "FuncTests" получила дополнительный параметр — указатель C_TestStand3D *st3, допускающий значение "NULL". Весь связанный с ним код защищён проверкой if (st3 != NULL), поэтому сигнатура функции остаётся совместимой с любым скриптом, не использующим трёхмерный стенд. Буфер координат агентов ag3D [] выделяется один раз после ao.Init (), а не на каждой эпохе. Там же, сразу после вычисления agCnt, вызывается st3. ResetTrail () — сброс трека в начале каждого независимого повторного теста. Трекер на экране всегда отображает историю только одного текущего прогона, а не накопленную смесь всех предыдущих.

Внутри эпохального цикла, после завершения двумерной отрисовки, добавлен блок трёхмерной визуализации. Он заполняет буфер ag3D координатами c [0] и c [1] каждого агента, находит индекс лучшего по значению фитнеса линейным проходом и передаёт всё это в SetAgents (), после чего вызывает Redraw (). Двумерная отрисовка при этом не затрагивается и выполняется в том же виде, что и в оригинальном скрипте.

Финализация организована в соответствии с особенностями жизненного цикла "CCanvas3D". Трёхмерный стенд скрывается через Show (false) и уничтожается через "delete", при этом явный вызов Shutdown () намеренно отсутствует: деструктор класса вызывает его самостоятельно через guard-флаг, и двойной вызов Destroy () приводил бы к аварийному завершению скрипта с сообщением Abnormal termination. Двумерный стенд, напротив, требует явного вызова ST.Canvas.Destroy () перед завершением — без него растровый объект остаётся на графике после окончания работы скрипта.

//────────────────────────────────────────────────────────────────────
void OnStart ()
{
  //--- создать и настроить алгоритм
  C_AO *AO = new C_AO_CSO ();
  AO.params [0].val = PopSize_P;
  AO.params [1].val = Phi_P;
  AO.SetParams ();
  Print (AO.GetName (), "|", AO.GetDesc (), "|", AO.GetParams ());

  //--- 2D стенд (ширина 750, высота 375 — стандарт)
  C_TestStand ST;
  ST.Init (750, 375);

  //--- 3D стенд
  //    Накрывает левую H×H панель 2D-стенда (тепловая карта функции).
  //    Правая панель со сходимостью остаётся видна через 2D-канвас.
  //    ST.H = высота тест-стенда (= WscrFunc/HscrFunc = H-2 = 373).
  C_TestStand3D *ST3 = NULL;
  if (Use3D_P)
  {
    ST3 = new C_TestStand3D ();
    int side = ST.H;  // квадратная левая панель
    if (!ST3.Init ("__3DView__", 5, 30, side, side))
    {
      Print ("C_TestStand3D: Init failed — 3D-режим отключён");
      delete ST3;
      ST3 = NULL;
    }
  }

  //==================================================================
  double allScore = 0.0;
  double allTests = 0.0;

  if (Function1 != NONE_Func)
  {
    C_Function *F = SelectFunction (Function1);
    if (F != NULL)
    {
      Print ("=============================");
      ST.CanvasErase ();
      // При смене функции пересчитать 3D-поверхность
      if (ST3 != NULL)
      {
        ST3.BuildSurface (*F, Grid3D_P);
        ST3.Show (true);
      }
      FuncTests (AO, ST, ST3, *F, Test1FuncRuns_P, clrLime,      allScore, allTests);
      FuncTests (AO, ST, ST3, *F, Test2FuncRuns_P, clrAqua,      allScore, allTests);
      FuncTests (AO, ST, ST3, *F, Test3FuncRuns_P, clrOrangeRed, allScore, allTests);
      delete F;
    }
  }

  if (Function2 != NONE_Func)
  {
    C_Function *F = SelectFunction (Function2);
    if (F != NULL)
    {
      Print ("=============================");
      ST.CanvasErase ();
      if (ST3 != NULL)
      {
        ST3.BuildSurface (*F, Grid3D_P);
        ST3.Show (true);
      }
      FuncTests (AO, ST, ST3, *F, Test1FuncRuns_P, clrLime,      allScore, allTests);
      FuncTests (AO, ST, ST3, *F, Test2FuncRuns_P, clrAqua,      allScore, allTests);
      FuncTests (AO, ST, ST3, *F, Test3FuncRuns_P, clrOrangeRed, allScore, allTests);
      delete F;
    }
  }

  if (Function3 != NONE_Func)
  {
    C_Function *F = SelectFunction (Function3);
    if (F != NULL)
    {
      Print ("=============================");
      ST.CanvasErase ();
      if (ST3 != NULL)
      {
        ST3.BuildSurface (*F, Grid3D_P);
        ST3.Show (true);
      }
      FuncTests (AO, ST, ST3, *F, Test1FuncRuns_P, clrLime,      allScore, allTests);
      FuncTests (AO, ST, ST3, *F, Test2FuncRuns_P, clrAqua,      allScore, allTests);
      FuncTests (AO, ST, ST3, *F, Test3FuncRuns_P, clrOrangeRed, allScore, allTests);
      delete F;
    }
  }

  //--- финализация 3D: Shutdown() вызовется в деструкторе через guard,
  //    явный вызов убран чтобы не было двойного Destroy → Abnormal termination
  if (ST3 != NULL)
  {
    ST3.Show (false);
    delete ST3;
  }

  //--- финализация 2D: явно удаляем объект с графика,
  //    иначе картинка застывает после окончания скрипта
  ST.Canvas.Destroy ();

  delete AO;

  Print ("=============================");
  if (allTests > 0.0) Print ("All score: ", DoubleToString (allScore, 5), " (", DoubleToString (allScore * 100.0 / allTests, 2), "%)");
}
//────────────────────────────────────────────────────────────────────

//────────────────────────────────────────────────────────────────────
void FuncTests (C_AO          &ao,
                C_TestStand   &st,
                C_TestStand3D *st3,        // может быть NULL
                C_Function    &f,
                const int      funcCount,
                const color    clrConv,
                double        &allScore,
                double        &allTests)
{
  if (funcCount <= 0) return;
  allTests++;

  //--- нарисовать тепловую карту функции один раз за серию тестов
  if (Video_P)
  {
    st.DrawFunctionGraph (f);
    st.SendGraphToCanvas ();
    st.MaxMinDr (f);
    st.Update ();
  }

  int    xConv      = 0;
  int    yConv      = 0;
  double aveResult  = 0.0;
  int    params     = funcCount * 2;
  int    epochCount = NumbTestFuncRuns_P / (int)ao.params [0].val;

  //--- диапазоны аргументов
  double rangeMin  [], rangeMax  [], rangeStep [];
  ArrayResize (rangeMin,  params);
  ArrayResize (rangeMax,  params);
  ArrayResize (rangeStep, params);

  for (int i = 0; i < funcCount; i++)
  {
    rangeMin  [i * 2] = f.GetMinRangeX ();
    rangeMax  [i * 2] = f.GetMaxRangeX ();
    rangeStep [i * 2] = ArgumentStep_P;
    rangeMin  [i * 2 + 1] = f.GetMinRangeY ();
    rangeMax  [i * 2 + 1] = f.GetMaxRangeY ();
    rangeStep [i * 2 + 1] = ArgumentStep_P;
  }

  // буфер (x,y) пар для 3D — будет заполнен после ao.Init()
  double ag3D [];

  //--- повторные тесты
  for (int test = 0; test < NumberRepetTest_P; test++)
  {
    if (!ao.Init (rangeMin, rangeMax, rangeStep, epochCount)) break;

    // agCnt вычисляем ПОСЛЕ ao.Init(), чтобы ao.a был правильно размечен.
    // До Init() ArraySize(ao.a) может быть 0 → цикл фитнеса не выполнится
    // → ao.fB = -DBL_MAX → сумма за 10 повторов переполняется до -inf.
    int agCnt = ArraySize (ao.a);
    if (st3 != NULL) ArrayResize (ag3D, agCnt * 2);
    // сбросить трек: каждый новый тест начинается с чистого листа
    if (st3 != NULL) st3.ResetTrail ();

    for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++)
    {
      if (DelayInMS_P > 0) Sleep (DelayInMS_P);
      Comment (epochCNT);

      ao.Moving ();

      //--- оценка фитнеса (как в оригинале)
      for (int set = 0; set < ArraySize (ao.a); set++) ao.a [set].f = f.CalcFunc (ao.a [set].c);

      ao.Revision ();

      //─── 2D визуализация (без изменений) ───────────────────
      if (Video_P)
      {
        st.SendGraphToCanvas ();
        for (int i = 0; i < ArraySize (ao.a); i++) st.PointDr (ao.a [i].c, f, 1, 1, funcCount, false);
        st.PointDr (ao.cB, f, 1, 1, funcCount, true);
        st.MaxMinDr (f);
        xConv = (int)st.Scale (epochCNT,  1, epochCount,
                               st.H + 2, st.W - 3, false);
        yConv = (int)st.Scale (ao.fB,
                               f.GetMinFunValue (), f.GetMaxFunValue (),
                               2, st.H - 2, true);
        st.Canvas.FillCircle (xConv, yConv, 1, COLOR2RGB (clrConv));
        st.Update ();
      }

      //─── 3D визуализация ───────────────────────────────────
      if (st3 != NULL)
      {
        // В скрипте каждая эпоха = один кадр анимации.
        // Реальное время не подходит (алгоритм работает мгновенно).
        // Используем фиксированный виртуальный шаг — 1/30 с на кадр.
        st3.OnTimer (0.033);

        for (int i = 0; i < agCnt; i++)
        {
          ag3D [i * 2] = ao.a [i].c [0];
          ag3D [i * 2 + 1] = ao.a [i].c [1];
        }
        int    bestIdx = 0;
        double bestF   = ao.a [0].f;
        for (int i = 1; i < agCnt; i++) if (ao.a [i].f > bestF)
          {
            bestF = ao.a [i].f;
            bestIdx = i;
          }
        st3.SetAgents (ag3D, agCnt, f, bestIdx);
        st3.Redraw ();
      }

    } // epochCNT

    aveResult += ao.fB;
  } // test

  aveResult /= (double)NumberRepetTest_P;
  Print (funcCount, " ", f.GetFuncName (), "'s; Func runs: ", NumbTestFuncRuns_P, "; result: ",      aveResult);
  allScore += aveResult;
}
//+------------------------------------------------------------------+


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

AO
Description
Hilly
Hilly
Final
Forest
Forest
Final
Megacity (discrete)
Megacity
Final
Final
Result
% of
MAX
10 p (5 F) 50 p (25 F) 1000 p (500 F) 10 p (5 F) 50 p (25 F) 1000 p (500 F) 10 p (5 F) 50 p (25 F) 1000 p (500 F)
1 ANS across neighbourhood search 0,94948 0,84776 0,43857 2,23581 1,00000 0,92334 0,39988 2,32323 0,70923 0,63477 0,23091 1,57491 6,134 68,15
2 CLA code lock algorithm (joo) 0,95345 0,87107 0,37590 2,20042 0,98942 0,91709 0,31642 2,22294 0,79692 0,69385 0,19303 1,68380 6,107 67,86
3 AMOm animal migration ptimization M 0,90358 0,84317 0,46284 2,20959 0,99001 0,92436 0,46598 2,38034 0,56769 0,59132 0,23773 1,39675 5,987 66,52
4 (P+O)ES (P+O) evolution strategies 0,92256 0,88101 0,40021 2,20379 0,97750 0,87490 0,31945 2,17185 0,67385 0,62985 0,18634 1,49003 5,866 65,17
5 CTA comet tail algorithm (joo) 0,95346 0,86319 0,27770 2,09435 0,99794 0,85740 0,33949 2,19484 0,88769 0,56431 0,10512 1,55712 5,846 64,96
6 TETA time evolution travel algorithm (joo) 0,91362 0,82349 0,31990 2,05701 0,97096 0,89532 0,29324 2,15952 0,73462 0,68569 0,16021 1,58052 5,797 64,41
7 SDSm stochastic diffusion search M 0,93066 0,85445 0,39476 2,17988 0,99983 0,89244 0,19619 2,08846 0,72333 0,61100 0,10670 1,44103 5,709 63,44
8 ECBO enhanced_colliding_bodies_optimization 0,93479 0,75747 0,32471 2,01697 0,97436 0,77446 0,23037 1,97919 0,88923 0,58061 0,15224 1,62208 5,618 62,43
9 BOAm billiards optimization algorithm M 0,95757 0,82599 0,25235 2,03590 1,00000 0,90036 0,30502 2,20538 0,73538 0,52523 0,09563 1,35625 5,598 62,19
10 AAm archery algorithm M 0,91744 0,70876 0,42160 2,04780 0,92527 0,75802 0,35328 2,03657 0,67385 0,55200 0,23738 1,46323 5,548 61,64
11 ESG evolution of social groups (joo) 0,99906 0,79654 0,35056 2,14616 1,00000 0,82863 0,13102 1,95965 0,82333 0,55300 0,04725 1,42358 5,529 61,44
12 SIA simulated isotropic annealing (joo) 0,95784 0,84264 0,41465 2,21513 0,98239 0,79586 0,20507 1,98332 0,68667 0,49300 0,09053 1,27020 5,469 60,76
13 EOm extremal_optimization_M 0,76166 0,77242 0,31747 1,85155 0,99999 0,76751 0,23527 2,00277 0,74769 0,53969 0,14249 1,42987 5,284 58,71
14 BBO biogeography based optimization 0,94912 0,69456 0,35031 1,99399 0,93820 0,67365 0,25682 1,86867 0,74615 0,48277 0,17369 1,40261 5,265 58,50
15 ACS artificial cooperative search 0,75547 0,74744 0,30407 1,80698 1,00000 0,88861 0,22413 2,11274 0,69077 0,48185 0,13322 1,30583 5,226 58,06
16 DA dialectical algorithm 0,86183 0,70033 0,33724 1,89940 0,98163 0,72772 0,28718 1,99653 0,70308 0,45292 0,16367 1,31967 5,216 57,95
17 BHAm black hole algorithm M 0,75236 0,76675 0,34583 1,86493 0,93593 0,80152 0,27177 2,00923 0,65077 0,51646 0,15472 1,32195 5,196 57,73
18 ASO anarchy society optimization 0,84872 0,74646 0,31465 1,90983 0,96148 0,79150 0,23803 1,99101 0,57077 0,54062 0,16614 1,27752 5,178 57,54
19 RFO royal flush optimization (joo) 0,83361 0,73742 0,34629 1,91733 0,89424 0,73824 0,24098 1,87346 0,63154 0,50292 0,16421 1,29867 5,089 56,55
20 AOSm atomic orbital search M 0,80232 0,70449 0,31021 1,81702 0,85660 0,69451 0,21996 1,77107 0,74615 0,52862 0,14358 1,41835 5,006 55,63
21 TSEA turtle shell evolution algorithm (joo) 0,96798 0,64480 0,29672 1,90949 0,99449 0,61981 0,22708 1,84139 0,69077 0,42646 0,13598 1,25322 5,004 55,60
22 BSA backtracking_search_algorithm 0,97309 0,54534 0,29098 1,80941 0,99999 0,58543 0,21747 1,80289 0,84769 0,36953 0,12978 1,34700 4,959 55,10
23 DE differential evolution 0,95044 0,61674 0,30308 1,87026 0,95317 0,78896 0,16652 1,90865 0,78667 0,36033 0,02953 1,17653 4,955 55,06
24 SRA successful restaurateur algorithm (joo) 0,96883 0,63455 0,29217 1,89555 0,94637 0,55506 0,19124 1,69267 0,74923 0,44031 0,12526 1,31480 4,903 54,48
25 BO bonobo_optimizer 0,77565 0,63805 0,32908 1,74278 0,88088 0,76344 0,25573 1,90005 0,61077 0,49846 0,14246 1,25169 4,895 54,38
26 CRO chemical reaction optimisation 0,94629 0,66112 0,29853 1,90593 0,87906 0,58422 0,21146 1,67473 0,75846 0,42646 0,12686 1,31178 4,892 54,36
27 CSO competitive_swarm_optimizer 0,90291 0,61887 0,29830 1,82008 0,99982 0,64581 0,23975 1,88538 0,63384 0,40553 0,12852 1,16789 4,873 54,15
28 BIO blood inheritance optimization (joo) 0,81568 0,65336 0,30877 1,77781 0,89937 0,65319 0,21760 1,77016 0,67846 0,47631 0,13902 1,29378 4,842 53,80
29 DOA dream_optimization_algorithm 0,85556 0,70085 0,37280 1,92921 0,73421 0,48905 0,24147 1,46473 0,77231 0,47354 0,18561 1,43146 4,825 53,62
30 BSA bird swarm algorithm 0,89306 0,64900 0,26250 1,80455 0,92420 0,71121 0,24939 1,88479 0,69385 0,32615 0,10012 1,12012 4,809 53,44
31 DEA dolphin_echolocation_algorithm 0,75995 0,67572 0,34171 1,77738 0,89582 0,64223 0,23941 1,77746 0,61538 0,44031 0,15115 1,20684 4,762 52,91
32 HS harmony search 0,86509 0,68782 0,32527 1,87818 0,99999 0,68002 0,09590 1,77592 0,62000 0,42267 0,05458 1,09725 4,751 52,79
33 SSG saplings sowing and growing 0,77839 0,64925 0,39543 1,82308 0,85973 0,62467 0,17429 1,65869 0,64667 0,44133 0,10598 1,19398 4,676 51,95
34 BCOm bacterial chemotaxis optimization M 0,75953 0,62268 0,31483 1,69704 0,89378 0,61339 0,22542 1,73259 0,65385 0,42092 0,14435 1,21912 4,649 51,65
35 ABO african buffalo optimization 0,83337 0,62247 0,29964 1,75548 0,92170 0,58618 0,19723 1,70511 0,61000 0,43154 0,13225 1,17378 4,634 51,49
36 (PO)ES (PO) evolution strategies 0,79025 0,62647 0,42935 1,84606 0,87616 0,60943 0,19591 1,68151 0,59000 0,37933 0,11322 1,08255 4,610 51,22
37 FBA fractal-based Algorithm 0,79000 0,65134 0,28965 1,73099 0,87158 0,56823 0,18877 1,62858 0,61077 0,46062 0,12398 1,19537 4,555 50,61
38 TSm tabu search M 0,87795 0,61431 0,29104 1,78330 0,92885 0,51844 0,19054 1,63783 0,61077 0,38215 0,12157 1,11449 4,536 50,40
39 BSO brain storm optimization 0,93736 0,57616 0,29688 1,81041 0,93131 0,55866 0,23537 1,72534 0,55231 0,29077 0,11914 0,96222 4,498 49,98
40 WOAm wale optimization algorithm M 0,84521 0,56298 0,26263 1,67081 0,93100 0,52278 0,16365 1,61743 0,66308 0,41138 0,11357 1,18803 4,476 49,74
41 AEFA artificial electric field algorithm 0,87700 0,61753 0,25235 1,74688 0,92729 0,72698 0,18064 1,83490 0,66615 0,11631 0,09508 0,87754 4,459 49,55
42 AEO artificial ecosystem-based optimization algorithm 0,91380 0,46713 0,26470 1,64563 0,90223 0,43705 0,21400 1,55327 0,66154 0,30800 0,28563 1,25517 4,454 49,49
43 CAm camel algorithm M 0,78684 0,56042 0,35133 1,69859 0,82772 0,56041 0,24336 1,63149 0,64846 0,33092 0,13418 1,11356 4,444 49,37
44 ACOm ant colony optimization M 0,88190 0,66127 0,30377 1,84693 0,85873 0,58680 0,15051 1,59604 0,59667 0,37333 0,02472 0,99472 4,438 49,31
45 CMAES covariance_matrix_adaptation_evolution_strategy 0,76258 0,72089 0,00000 1,48347 0,82056 0,79616 0,00000 1,61672 0,75846 0,49077 0,00000 1,24923 4,349 48,33
RW random walk 0,48754 0,32159 0,25781 1,06694 0,37554 0,21944 0,15877 0,75375 0,27969 0,14917 0,09847 0,52734 2,348 26,09


Выводы

Мы реализовали Competitive Swarm Optimizer в MQL5, формализовали единственную формулу обновления скорости с тремя независимыми случайными компонентами, разобрали все критичные детали реализации — нулевую инициализацию скоростей, ограничение "Vmax", алгоритм Фишера–Йетса для честного перемешивания пар — и провели воспроизводимые тесты на стандартном наборе бенчмарков. По итогам CSO занял 27-е место из 45 лучших оптимизационных методов. CSO — хорошее напоминание о том, что в метаэвристической оптимизации простота не порок. Двадцать седьмое место из сорока пяти при минимальном коде и одном параметре — это честный результат честного алгоритма.

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

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

Тем не менее CSO остаётся одним из наиболее концептуально чистых алгоритмов в серии. Идея соревнования, при которой победитель не трогается, а проигравший учится у конкретного соперника и у коллективного центра — элегантна и интуитивно понятна. Алгоритм легко объяснить, легко реализовать, легко отлаживать.

Практический вывод: CSO имеет смысл как надёжный базовый инструмент там, где важна предсказуемость и прозрачность, а не максимальное качество решения. Единственный параметр "phi" делает настройку тривиальной: для низкоразмерных задач достаточно phi = 0.1, для высокоразмерных — значения 0.1–0.3. Для задач, где требуется выжать максимум из фиксированного бюджета вычислений на мультимодальных или дискретных ландшафтах, стоит обратиться к методам из верхней половины рейтинга. Очевидный путь улучшения CSO — добавить механизм личных рекордов по аналогии с PSO, сохранив при этом соревновательную схему формирования пар. Исходный код и протокол тестирования открыты для воспроизведения и дальнейших исследований.

tab

Рисунок 2. Цветовая градация алгоритмов по соответствующим тестам

chart

Рисунок 3. Гистограмма результатов тестирования алгоритмов (по шкале от 0 до 100, чем больше, тем лучше, где 100 — максимально возможный теоретический результат, в архиве скрипт для расчета рейтинговой таблицы)


Плюсы и минусы алгоритма CSO

Плюсы:

  1. Один дополнительный параметр кроме популяции.
  2. Простая и понятная концепция.

Минусы:

  1. Заметных минусов не выявлено.

К статье прикреплён архив с актуальными версиями кодов алгоритмов. Автор статьи не несёт ответственности за абсолютную точность в описании канонических алгоритмов, во многие из них были внесены изменения для улучшения поисковых возможностей. Выводы и суждения, представленные в статьях, основываются на результатах проведённых экспериментов.



Программы, используемые в статье

# Имя Тип Описание
1 #C_AO.mqh
Включаемый файл
Родительский класс популяционных алгоритмов оптимизации
2 #C_AO_enum.mqh
Включаемый файл
Перечисление популяционных алгоритмов оптимизации
3 TestFunctions.mqh
Включаемый файл
Библиотека тестовых функций
4
TestStandFunctions.mqh
Включаемый файл
Библиотека функций тестового стенда
5 TestStand3D.mqh Включаемый файл 3D-панель визуализации для тестового стенда 
6 Utilities.mqh
Включаемый файл
Библиотека вспомогательных функций
7 CalculationTestResults.mqh
Включаемый файл
Скрипт для расчета результатов в сравнительную таблицу
8 Testing AOs.mq5
Скрипт Единый испытательный стенд для всех популяционных алгоритмов оптимизации
9 Simple use of population optimization algorithms.mq5
Скрипт
Простой пример использования популяционных алгоритмов оптимизации без визуализации
10 Test_CSO.mq5
Скрипт Испытательный стенд для CSO
Прикрепленные файлы |
CSO.zip (435.41 KB)
Преодоление ограничений машинного обучения (Часть 7): Автоматический выбор стратегии Преодоление ограничений машинного обучения (Часть 7): Автоматический выбор стратегии
В этой статье показано, как автоматически определять потенциально прибыльные торговые стратегии с помощью MetaTrader 5. Решения "белого ящика", основанные на неконтролируемой матричной факторизации, быстрее настраиваются, лучше поддаются интерпретации и предоставляют четкие рекомендации относительно того, какие стратегии следует сохранить. Решения "черного ящика", хотя и требуют больше времени, лучше подходят для сложных рыночных условий, которые подходы "белого ящика" могут не учитывать. Присоединяйтесь к нашему обсуждению того, как наши торговые стратегии могут помочь нам тщательно подбирать прибыльные стратегии при любых обстоятельствах.
Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание) Нейросети в трейдинге: Оптимизация Cross-Attention для анализа длинных последовательностей рынка (Окончание)
В статье рассматривается практическая реализация архитектуры STCA с интеграцией механизмов OneTrans для совместной обработки временных рядов и контекстных признаков рынка. Описаны особенности построения модели, алгоритмы прямого прохода и накопления исторического состояния. Отдельное внимание уделено процессу обучения и результатам тестирования на реальных данных, демонстрирующим поведение модели в рыночных условиях.
Возможности Мастера MQL5, которые вам нужно знать (Часть 73): Использование паттернов Ишимоку и ADX-Wilder Возможности Мастера MQL5, которые вам нужно знать (Часть 73): Использование паттернов Ишимоку и ADX-Wilder
Индикатор Ишимоку (Ichimoku-Kinko-Hyo) и осциллятор ADX-Wilder — это взаимодополняющая пара, которую можно использовать в составе MQL5-советника. Индикатор Ишимоку многогранен, однако в данной статье мы будем использовать его в первую очередь для определения уровней поддержки и сопротивления. Мы также применим ADX для определения тренда. Как обычно, мы используем Мастер MQL5 для построения паттернов и тестирования потенциала, который может иметь эта пара индикаторов.
Автоматизация торговых стратегий на MQL5 (Часть 22): Создание системы зонального восстановления для трендовой торговли по индикатору Envelopes Автоматизация торговых стратегий на MQL5 (Часть 22): Создание системы зонального восстановления для трендовой торговли по индикатору Envelopes
Мы разработаем систему зонального восстановления (Zone Recovery System), интегрированную со стратегией трендовой торговли на основе конвертов (Envelopes trend-trading strategy) на MQL5. Также мы опишем архитектуру использования индикаторов RSI и конвертов для инициирования сделок и управления зональным восстановлением с целью минимизации потерь. На практике и в ходе тестирования мы продемонстрируем, как создать эффективную автоматизированную торговую систему для динамичных рынков.